diff --git a/groups/Rakefile b/groups/Rakefile new file mode 100644 index 000000000..cffd19f0c --- /dev/null +++ b/groups/Rakefile @@ -0,0 +1,10 @@ +# 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 diff --git a/groups/app/apis/sys_api.rb b/groups/app/apis/sys_api.rb new file mode 100644 index 000000000..f52f9e7ef --- /dev/null +++ b/groups/app/apis/sys_api.rb @@ -0,0 +1,25 @@ +# 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 SysApi < ActionWebService::API::Base + api_method :projects, + :expects => [], + :returns => [[Project]] + api_method :repository_created, + :expects => [:string, :string], + :returns => [:int] +end diff --git a/groups/app/controllers/account_controller.rb b/groups/app/controllers/account_controller.rb new file mode 100644 index 000000000..b9224c158 --- /dev/null +++ b/groups/app/controllers/account_controller.rb @@ -0,0 +1,177 @@ +# 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 AccountController < ApplicationController + layout 'base' + 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, :only => [:login, :lost_password, :register, :activate] + + # Show user's account + def show + @user = User.find_active(params[:id]) + @custom_values = @user.custom_values.find(:all, :include => :custom_field) + + # 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 + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Login request and validation + def login + if request.get? + # Logout user + self.logged_user = nil + else + # Authenticate user + user = User.try_to_login(params[:username], params[:password]) + if 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 + redirect_back_or_default :controller => 'my', :action => 'page' + else + flash.now[:error] = l(:notice_account_invalid_creditentials) + end + end + rescue User::OnTheFlyCreationFailure + flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.' + 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? + if request.get? + @user = User.new(:language => Setting.default_language) + @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } + else + @user = User.new(params[:user]) + @user.admin = false + @user.login = params[:user][:login] + @user.status = User::STATUS_REGISTERED + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, + :customized => @user, + :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) } + @user.custom_values = @custom_values + case Setting.self_registration + when '1' + # Email activation + 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' + end + when '3' + # Automatic activation + @user.status = User::STATUS_ACTIVE + if @user.save + flash[:notice] = l(:notice_account_activated) + redirect_to :action => 'login' + end + else + # Manual activation by the administrator + if @user.save + # Sends an email to the administrators + Mailer.deliver_account_activation_request(@user) + flash[:notice] = l(:notice_account_pending) + redirect_to :action => 'login' + 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 logged_user=(user) + if user && user.is_a?(User) + User.current = user + session[:user_id] = user.id + else + User.current = User.anonymous + session[:user_id] = nil + end + end +end diff --git a/groups/app/controllers/admin_controller.rb b/groups/app/controllers/admin_controller.rb new file mode 100644 index 000000000..e002f3a27 --- /dev/null +++ b/groups/app/controllers/admin_controller.rb @@ -0,0 +1,86 @@ +# 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 + layout 'base' + before_filter :require_admin + + helper :sort + include SortHelper + + def index + @no_configuration_data = Redmine::DefaultData::Loader::no_data? + end + + def projects + sort_init 'name', 'asc' + sort_update + + @status = params[:status] ? params[:status].to_i : 0 + conditions = nil + conditions = ["status=?", @status] unless @status == 0 + + @project_count = Project.count(:conditions => conditions) + @project_pages = Paginator.new self, @project_count, + per_page_option, + params['page'] + @projects = Project.find :all, :order => sort_clause, + :conditions => conditions, + :limit => @project_pages.items_per_page, + :offset => @project_pages.current.offset + + render :action => "projects", :layout => false if request.xhr? + 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), + :rmagick_available => Object.const_defined?(:Magick) + } + @plugins = Redmine::Plugin.registered_plugins + end +end diff --git a/groups/app/controllers/application.rb b/groups/app/controllers/application.rb new file mode 100644 index 000000000..abf621641 --- /dev/null +++ b/groups/app/controllers/application.rb @@ -0,0 +1,222 @@ +# 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 ApplicationController < ActionController::Base + before_filter :user_setup, :check_if_login_required, :set_localization + filter_parameter_logging :password + + include Redmine::MenuManager::MenuController + helper Redmine::MenuManager::MenuHelper + + REDMINE_SUPPORTED_SCM.each do |scm| + require_dependency "repository/#{scm.underscore}" + end + + def current_role + @current_role ||= User.current.role_for_project(@project) + 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 + def find_current_user + if session[:user_id] + # existing session + (User.find_active(session[:user_id]) rescue nil) + elsif cookies[:autologin] && Setting.autologin? + # auto-login feature + User.find_by_autologin_key(cookies[:autologin]) + elsif params[:key] && accept_key_auth_actions.include?(params[:action]) + # RSS key authentication + User.find_by_rss_key(params[:key]) + 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 + User.current.language = nil unless User.current.logged? + lang = begin + if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym + User.current.language + elsif request.env['HTTP_ACCEPT_LANGUAGE'] + accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first + if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym + User.current.language = accept_lang + end + end + rescue + nil + end || Setting.default_language + set_language_if_valid(lang) + end + + def require_login + if !User.current.logged? + store_location + redirect_to :controller => "account", :action => "login" + return false + end + true + end + + def require_admin + return unless require_login + if !User.current.admin? + render_403 + return false + end + true + end + + # Authorize the user for the requested action + def authorize(ctrl = params[:controller], action = params[:action]) + allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project) + allowed ? true : (User.current.logged? ? render_403 : require_login) + 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 + + # store current uri in session. + # return to this location by calling redirect_back_or_default + def store_location + session[:return_to_params] = params + end + + # move to the last store_location call or to the passed default one + def redirect_back_or_default(default) + if session[:return_to_params].nil? + redirect_to default + else + redirect_to session[:return_to_params] + session[:return_to_params] = nil + end + 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 :nothing => true, :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 = [] + 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) + attached << a unless a.new_record? + 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 + 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 diff --git a/groups/app/controllers/attachments_controller.rb b/groups/app/controllers/attachments_controller.rb new file mode 100644 index 000000000..4e87e5442 --- /dev/null +++ b/groups/app/controllers/attachments_controller.rb @@ -0,0 +1,39 @@ +# 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 AttachmentsController < ApplicationController + layout 'base' + before_filter :find_project, :check_project_privacy + + def download + # images are sent inline + send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), + :type => @attachment.content_type, + :disposition => (@attachment.image? ? 'inline' : 'attachment') + rescue + # in case the disk file was deleted + render_404 + end + +private + def find_project + @attachment = Attachment.find(params[:id]) + @project = @attachment.project + rescue + render_404 + end +end diff --git a/groups/app/controllers/auth_sources_controller.rb b/groups/app/controllers/auth_sources_controller.rb new file mode 100644 index 000000000..b830f1970 --- /dev/null +++ b/groups/app/controllers/auth_sources_controller.rb @@ -0,0 +1,83 @@ +# 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 + layout 'base' + 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 diff --git a/groups/app/controllers/boards_controller.rb b/groups/app/controllers/boards_controller.rb new file mode 100644 index 000000000..5bf4499bd --- /dev/null +++ b/groups/app/controllers/boards_controller.rb @@ -0,0 +1,86 @@ +# 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 + layout 'base' + 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 + sort_init "#{Message.table_name}.updated_on", "desc" + sort_update + + @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}", + :include => [:author, {:last_reply => :author}], + :limit => @topic_pages.items_per_page, + :offset => @topic_pages.current.offset + render :action => 'show', :layout => !request.xhr? + 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]) + case params[:position] + when 'highest'; @board.move_to_top + when 'higher'; @board.move_higher + when 'lower'; @board.move_lower + when 'lowest'; @board.move_to_bottom + end if params[:position] + 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 diff --git a/groups/app/controllers/custom_fields_controller.rb b/groups/app/controllers/custom_fields_controller.rb new file mode 100644 index 000000000..1e1c988d9 --- /dev/null +++ b/groups/app/controllers/custom_fields_controller.rb @@ -0,0 +1,87 @@ +# 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 CustomFieldsController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + @custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name } + @tab = params[:tab] || 'IssueCustomField' + render :action => "list", :layout => false if request.xhr? + end + + def new + case params[:type] + when "IssueCustomField" + @custom_field = IssueCustomField.new(params[:custom_field]) + @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids] + when "UserCustomField" + @custom_field = UserCustomField.new(params[:custom_field]) + when "ProjectCustomField" + @custom_field = ProjectCustomField.new(params[:custom_field]) + else + redirect_to :action => 'list' + return + end + if request.post? and @custom_field.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list', :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]) + if @custom_field.is_a? IssueCustomField + @custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : [] + end + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list', :tab => @custom_field.class.name + end + @trackers = Tracker.find(:all, :order => 'position') + end + + def move + @custom_field = CustomField.find(params[:id]) + case params[:position] + when 'highest' + @custom_field.move_to_top + when 'higher' + @custom_field.move_higher + when 'lower' + @custom_field.move_lower + when 'lowest' + @custom_field.move_to_bottom + end if params[:position] + redirect_to :action => 'list', :tab => @custom_field.class.name + end + + def destroy + @custom_field = CustomField.find(params[:id]).destroy + redirect_to :action => 'list', :tab => @custom_field.class.name + rescue + flash[:error] = "Unable to delete custom field" + redirect_to :action => 'list' + end +end diff --git a/groups/app/controllers/documents_controller.rb b/groups/app/controllers/documents_controller.rb new file mode 100644 index 000000000..7e732b9b6 --- /dev/null +++ b/groups/app/controllers/documents_controller.rb @@ -0,0 +1,102 @@ +# 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 + layout 'base' + 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 + 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) + Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') + redirect_to :action => 'index', :project_id => @project + end + end + + def edit + @categories = Enumeration::get_values('DCAT') + 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 download + @attachment = @document.attachments.find(params[:attachment_id]) + @attachment.increment_download + send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), + :type => @attachment.content_type + rescue + render_404 + 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 + + def destroy_attachment + @document.attachments.find(params[:attachment_id]).destroy + 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 diff --git a/groups/app/controllers/enumerations_controller.rb b/groups/app/controllers/enumerations_controller.rb new file mode 100644 index 000000000..7a7f1685a --- /dev/null +++ b/groups/app/controllers/enumerations_controller.rb @@ -0,0 +1,85 @@ +# 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 + layout 'base' + before_filter :require_admin + + 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 + @enumeration = Enumeration.new(:opt => params[:opt]) + end + + def create + @enumeration = Enumeration.new(params[:enumeration]) + if @enumeration.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list', :opt => @enumeration.opt + else + render :action => 'new' + end + end + + def edit + @enumeration = Enumeration.find(params[:id]) + end + + def update + @enumeration = Enumeration.find(params[:id]) + if @enumeration.update_attributes(params[:enumeration]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list', :opt => @enumeration.opt + else + render :action => 'edit' + end + end + + def move + @enumeration = Enumeration.find(params[:id]) + case params[:position] + when 'highest' + @enumeration.move_to_top + when 'higher' + @enumeration.move_higher + when 'lower' + @enumeration.move_lower + when 'lowest' + @enumeration.move_to_bottom + end if params[:position] + redirect_to :action => 'index' + end + + def destroy + Enumeration.find(params[:id]).destroy + flash[:notice] = l(:notice_successful_delete) + redirect_to :action => 'list' + rescue + flash[:error] = "Unable to delete enumeration" + redirect_to :action => 'list' + end +end diff --git a/groups/app/controllers/issue_categories_controller.rb b/groups/app/controllers/issue_categories_controller.rb new file mode 100644 index 000000000..a73935b4f --- /dev/null +++ b/groups/app/controllers/issue_categories_controller.rb @@ -0,0 +1,53 @@ +# 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 + layout 'base' + 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 diff --git a/groups/app/controllers/issue_relations_controller.rb b/groups/app/controllers/issue_relations_controller.rb new file mode 100644 index 000000000..cb0ad552a --- /dev/null +++ b/groups/app/controllers/issue_relations_controller.rb @@ -0,0 +1,59 @@ +# 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 + layout 'base' + before_filter :find_project, :authorize + + def new + @relation = IssueRelation.new(params[:relation]) + @relation.issue_from = @issue + @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 diff --git a/groups/app/controllers/issue_statuses_controller.rb b/groups/app/controllers/issue_statuses_controller.rb new file mode 100644 index 000000000..d0712e7c3 --- /dev/null +++ b/groups/app/controllers/issue_statuses_controller.rb @@ -0,0 +1,85 @@ +# 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 + layout 'base' + 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 move + @issue_status = IssueStatus.find(params[:id]) + case params[:position] + when 'highest' + @issue_status.move_to_top + when 'higher' + @issue_status.move_higher + when 'lower' + @issue_status.move_lower + when 'lowest' + @issue_status.move_to_bottom + end if params[:position] + redirect_to :action => 'list' + 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 diff --git a/groups/app/controllers/issues_controller.rb b/groups/app/controllers/issues_controller.rb new file mode 100644 index 000000000..84b95741e --- /dev/null +++ b/groups/app/controllers/issues_controller.rb @@ -0,0 +1,420 @@ +# 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 IssuesController < ApplicationController + layout 'base' + menu_item :new_issue, :only => :new + + before_filter :find_issue, :only => [:show, :edit, :destroy_attachment] + before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] + before_filter :find_project, :only => [:new, :update_form, :preview] + before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu] + before_filter :find_optional_project, :only => [:index, :changes] + accept_key_auth :index, :changes + + helper :journals + helper :projects + include ProjectsHelper + helper :custom_fields + include CustomFieldsHelper + helper :ifpdf + include IfpdfHelper + helper :issue_relations + include IssueRelationsHelper + helper :watchers + include WatchersHelper + helper :attachments + include AttachmentsHelper + helper :queries + helper :sort + include SortHelper + include IssuesHelper + + def index + sort_init "#{Issue.table_name}.id", "desc" + sort_update + retrieve_query + if @query.valid? + limit = per_page_option + respond_to do |format| + format.html { } + format.atom { } + 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 => sort_clause, + :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], + :conditions => @query.statement, + :limit => limit, + :offset => @issue_pages.current.offset + respond_to do |format| + format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } + format.atom { render_feed(@issues, :title => l(:label_issue_plural)) } + format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } + format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') } + 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 + sort_init "#{Issue.table_name}.id", "desc" + sort_update + retrieve_query + 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 + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } + @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? + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + @activities = Enumeration::get_values('ACTI') + @priorities = Enumeration::get_values('IPRI') + respond_to do |format| + format.html { render :template => 'issues/show.rhtml' } + format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } + format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } + 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 = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue]) + @issue.project = @project + @issue.author = User.current + @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first) + if @issue.tracker.nil? + flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.' + render :nothing => true, :layout => true + return + end + + default_status = IssueStatus.default + unless default_status + flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' + render :nothing => true, :layout => true + return + end + @issue.status = default_status + @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq + + if request.get? || request.xhr? + @issue.start_date ||= Date.today + @custom_values = @issue.custom_values.empty? ? + @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } : + @issue.custom_values + 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 + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, + :customized => @issue, + :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) } + @issue.custom_values = @custom_values + if @issue.save + 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 + return + end + end + @priorities = Enumeration::get_values('IPRI') + 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) + @activities = Enumeration::get_values('ACTI') + @priorities = Enumeration::get_values('IPRI') + @custom_values = [] + @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + + @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.get? + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } + else + # Update custom fields if user has :edit permission + if @edit_allowed && params[:custom_fields] + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } + @issue.custom_values = @custom_values + end + attachments = attach_files(@issue, params[:attachments]) + attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} + if @issue.save + # Log spend time + if current_role.allowed_to?(:log_time) + @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) + @time_entry.attributes = params[:time_entry] + @time_entry.save + end + if !journal.new_record? + # Only send notification if something was actually changed + flash[:notice] = l(:notice_successful_update) + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + end + redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) + end + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash.now[:error] = l(:notice_locking_conflict) + 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 : Enumeration.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]) + + 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? + # Don't save any change to the issue if the user is not authorized to apply the requested status + if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save + # Send notification for each issue (if changed) + Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') + else + # 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, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) + end + redirect_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 => current_role.id}).collect(&:new_status).compact.uniq + 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), :order => 'name') + else + User.current.memberships.each {|m| @allowed_projects << m.project if m.role.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 = [] + @issues.each do |issue| + unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) + end + if unsaved_issue_ids.empty? + flash[:notice] = l(:notice_successful_update) unless @issues.empty? + else + flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) + end + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + 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 destroy_attachment + a = @issue.attachments.find(params[:attachment_id]) + a.destroy + journal = @issue.init_journal(User.current) + journal.details << JournalDetail.new(:property => 'attachment', + :prop_key => a.id, + :old_value => a.filename) + journal.save + redirect_to :action => 'show', :id => @issue + end + + def 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) + @assignables = @issue.assignable_users + @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + end + projects = @issues.collect(&:project).compact.uniq + @project = projects.first if projects.size == 1 + + @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), + :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@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)) + } + + @priorities = Enumeration.get_values('IPRI').reverse + @statuses = IssueStatus.find(:all, :order => 'position') + @back = 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 + return true unless params[:project_id] + @project = Project.find(params[:project_id]) + authorize + 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} + 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 + session[:query] = {:project_id => @query.project_id, :filters => @query.filters} + else + @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] + @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) + @query.project = @project + end + end + end +end diff --git a/groups/app/controllers/journals_controller.rb b/groups/app/controllers/journals_controller.rb new file mode 100644 index 000000000..758b8507f --- /dev/null +++ b/groups/app/controllers/journals_controller.rb @@ -0,0 +1,41 @@ +# 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 + layout 'base' + 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? + 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 diff --git a/groups/app/controllers/members_controller.rb b/groups/app/controllers/members_controller.rb new file mode 100644 index 000000000..a1706e601 --- /dev/null +++ b/groups/app/controllers/members_controller.rb @@ -0,0 +1,62 @@ +# 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 + layout 'base' + before_filter :find_member, :except => :new + before_filter :find_project, :only => :new + before_filter :authorize + + def new + @project.members << Member.new(params[:member]) if request.post? + respond_to do |format| + format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project } + format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } + 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'} } + end + end + end + + def destroy + @member.destroy + 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 + +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 diff --git a/groups/app/controllers/messages_controller.rb b/groups/app/controllers/messages_controller.rb new file mode 100644 index 000000000..97cb2c3bf --- /dev/null +++ b/groups/app/controllers/messages_controller.rb @@ -0,0 +1,108 @@ +# 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 + layout 'base' + menu_item :boards + before_filter :find_board, :only => [:new, :preview] + before_filter :find_message, :except => [:new, :preview] + before_filter :authorize, :except => :preview + + verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } + + helper :attachments + include AttachmentsHelper + + # Show a topic and its replies + def show + @replies = @topic.children + @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 + 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? + attach_files(@reply, params[:attachments]) + end + redirect_to :action => 'show', :id => @topic + end + + # Edit a message + def edit + 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.update_attributes(params[:message]) + attach_files(@message, params[:attachments]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @topic + end + end + + # Delete a messages + def destroy + @message.destroy + redirect_to @message.parent.nil? ? + { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : + { :action => 'show', :id => @message.parent } + 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 diff --git a/groups/app/controllers/my_controller.rb b/groups/app/controllers/my_controller.rb new file mode 100644 index 000000000..ff3393e90 --- /dev/null +++ b/groups/app/controllers/my_controller.rb @@ -0,0 +1,161 @@ +# 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 MyController < ApplicationController + helper :issues + + layout 'base' + before_filter :require_login + + 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 + }.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(v), k]} + 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] + 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] + # 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] + group_items = params["list-#{group}"] + 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 + 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 diff --git a/groups/app/controllers/news_controller.rb b/groups/app/controllers/news_controller.rb new file mode 100644 index 000000000..c9ba6b991 --- /dev/null +++ b/groups/app/controllers/news_controller.rb @@ -0,0 +1,109 @@ +# 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 + layout 'base' + before_filter :find_news, :except => [:new, :index, :preview] + before_filter :find_project, :only => :new + 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) + Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added') + 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 + 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 diff --git a/groups/app/controllers/projects_controller.rb b/groups/app/controllers/projects_controller.rb new file mode 100644 index 000000000..b71ec1ecd --- /dev/null +++ b/groups/app/controllers/projects_controller.rb @@ -0,0 +1,433 @@ +# 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 ProjectsController < ApplicationController + layout 'base' + 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, :activity ] + before_filter :find_optional_project, :only => :activity + before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ] + before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] + accept_key_auth :activity, :calendar + + helper :sort + include SortHelper + helper :custom_fields + include CustomFieldsHelper + helper :ifpdf + include IfpdfHelper + helper :issues + helper IssuesHelper + helper :queries + include QueriesHelper + helper :repositories + include RepositoriesHelper + include ProjectsHelper + + def index + list + render :action => 'list' unless request.xhr? + end + + # Lists visible projects + def list + projects = Project.find :all, + :conditions => Project.visible_by(User.current), + :include => :parent + @project_tree = projects.group_by {|p| p.parent || p} + @project_tree.each_key {|p| @project_tree[p] -= [p]} + end + + # Add a new project + def add + @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') + @project = Project.new(params[:project]) + if request.get? + @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) } + @project.trackers = Tracker.all + @project.is_public = Setting.default_projects_public? + @project.enabled_module_names = Redmine::AccessControl.available_project_modules + else + @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] + @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } + @project.custom_values = @custom_values + @project.enabled_module_names = params[:enabled_modules] + if @project.save + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'admin', :action => 'projects' + end + end + end + + # Show @project + def show + @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") + @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} + @subprojects = @project.active_children + @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?) + Issue.visible_by(User.current) do + @open_issues_by_tracker = Issue.count(:group => :tracker, + :include => [:project, :status, :tracker], + :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false]) + @total_issues_by_tracker = Issue.count(:group => :tracker, + :include => [:project, :status, :tracker], + :conditions => cond) + end + 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 + @root_projects = Project.find(:all, + :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], + :order => 'name') + @custom_fields = IssueCustomField.find(:all) + @issue_category ||= IssueCategory.new + @member ||= @project.members.new + @trackers = Tracker.all + @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } + @repository ||= @project.repository + @wiki ||= @project.wiki + end + + # Edit @project + def edit + if request.post? + if params[:custom_fields] + @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) } + @project.custom_values = @custom_values + end + @project.attributes = params[:project] + if @project.save + 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 :controller => 'admin', :action => 'projects' + end + + def unarchive + @project.unarchive if request.post? && !@project.active? + redirect_to :controller => 'admin', :action => 'projects' + 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', '' + 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? + @version = @project.versions.find_by_id(params[:version_id]) + attachments = attach_files(@version, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added') + redirect_to :controller => 'projects', :action => 'list_files', :id => @project + end + @versions = @project.versions.sort + end + + def list_files + sort_init "#{Attachment.table_name}.filename", "asc" + sort_update + @versions = @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; rescue; end + end + + @date_to ||= Date.today + 1 + @date_from = @date_to - @days + + @event_types = %w(issues news files documents changesets wiki_pages messages) + if @project + @event_types.delete('wiki_pages') unless @project.wiki + @event_types.delete('changesets') unless @project.repository + @event_types.delete('messages') unless @project.boards.any? + # only show what the user is allowed to view + @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') + end + @scope = @event_types.select {|t| params["show_#{t}"]} + # default events if none is specified in parameters + @scope = (@event_types - %w(wiki_pages messages))if @scope.empty? + + @events = [] + + if @scope.include?('issues') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions) + + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{JournalDetail.table_name}.prop_key = 'status_id' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions) + end + + if @scope.include?('news') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions) + end + + if @scope.include?('files') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Attachment.find(:all, :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", + :conditions => cond.conditions) + end + + if @scope.include?('documents') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Document.find(:all, :include => :project, :conditions => cond.conditions) + + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Attachment.find(:all, :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", + :conditions => cond.conditions) + end + + if @scope.include?('wiki_pages') + 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" + + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions) + end + + if @scope.include?('changesets') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions) + end + + if @scope.include?('messages') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions) + end + + @events_by_day = @events.group_by(&:event_date) + + respond_to do |format| + format.html { render :layout => false if request.xhr? } + format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") } + end + end + + def calendar + @trackers = @project.rolled_up_trackers + retrieve_selected_tracker_ids(@trackers) + + 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) + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') + events = [] + @project.issues_with_subprojects(@with_subprojects) do + events += Issue.find(:all, + :include => [:tracker, :status, :assigned_to, :priority, :project], + :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] + ) unless @selected_tracker_ids.empty? + events += Version.find(:all, :include => :project, + :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) + end + @calendar.events = events + + render :layout => false if request.xhr? + end + + def gantt + @trackers = @project.rolled_up_trackers + retrieve_selected_tracker_ids(@trackers) + + if params[:year] and params[:year].to_i >0 + @year_from = params[:year].to_i + if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12 + @month_from = params[:month].to_i + else + @month_from = 1 + end + else + @month_from ||= Date.today.month + @year_from ||= Date.today.year + end + + zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i + @zoom = (zoom > 0 && zoom < 5) ? zoom : 2 + months = (params[:months] || User.current.pref[:gantt_months]).to_i + @months = (months > 0 && months < 25) ? months : 6 + + # Save gantt paramters 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 + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') + + @events = [] + @project.issues_with_subprojects(@with_subprojects) do + @events += Issue.find(:all, + :order => "start_date, due_date", + :include => [:tracker, :status, :assigned_to, :priority, :project], + :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] + ) unless @selected_tracker_ids.empty? + @events += Version.find(:all, :include => :project, + :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to]) + end + @events.sort! {|x,y| x.start_date <=> y.start_date } + + if params[:format]=='pdf' + @options_for_rfpdf ||= {} + @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf" + render :template => "projects/gantt.rfpdf", :layout => false + elsif params[:format]=='png' && respond_to?('gantt_image') + image = gantt_image(@events, @date_from, @months, @zoom) + image.format = 'PNG' + send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") + else + render :template => "projects/gantt.rhtml" + end + 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 diff --git a/groups/app/controllers/queries_controller.rb b/groups/app/controllers/queries_controller.rb new file mode 100644 index 000000000..da2c4a2c8 --- /dev/null +++ b/groups/app/controllers/queries_controller.rb @@ -0,0 +1,81 @@ +# 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 + layout 'base' + 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 (@query.project && current_role.allowed_to?(:manage_public_queries)) || 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] + + 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 (@query.project && current_role.allowed_to?(:manage_public_queries)) || 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 diff --git a/groups/app/controllers/reports_controller.rb b/groups/app/controllers/reports_controller.rb new file mode 100644 index 000000000..338059a50 --- /dev/null +++ b/groups/app/controllers/reports_controller.rb @@ -0,0 +1,237 @@ +# 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 + layout 'base' + 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 = Enumeration::get_values('IPRI') + @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.active_children + @data = issues_by_subproject + @report_title = l(:field_subproject) + render :template => "reports/issue_report_details" + else + @trackers = @project.trackers + @versions = @project.versions.sort + @priorities = Enumeration::get_values('IPRI') + @categories = @project.issue_categories + @assignees = @project.members.collect { |m| m.user } + @authors = @project.members.collect { |m| m.user } + @subprojects = @project.active_children + 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 + + def delays + @trackers = Tracker.find(:all) + if request.get? + @selected_tracker_ids = @trackers.collect {|t| t.id.to_s } + else + @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array + end + @selected_tracker_ids ||= [] + @raw = + ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total + FROM issue_histories a, issue_histories b, issues i + WHERE a.status_id =5 + AND a.issue_id = b.issue_id + AND a.issue_id = i.id + AND i.tracker_id in (#{@selected_tracker_ids.join(',')}) + AND b.id = ( + SELECT min( c.id ) + FROM issue_histories c + WHERE b.issue_id = c.issue_id ) + GROUP BY delay") unless @selected_tracker_ids.empty? + @raw ||=[] + + @x_from = 0 + @x_to = 0 + @y_from = 0 + @y_to = 0 + @sum_total = 0 + @sum_delay = 0 + @raw.each do |r| + @x_to = [r['delay'].to_i, @x_to].max + @y_to = [r['total'].to_i, @y_to].max + @sum_total = @sum_total + r['total'].to_i + @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i + 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, #{Enumeration.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.active_children.collect{|p| p.id}.join(',')}) + group by s.id, s.is_closed, i.project_id") if @project.active_children.any? + @issues_by_subproject ||= [] + end +end diff --git a/groups/app/controllers/repositories_controller.rb b/groups/app/controllers/repositories_controller.rb new file mode 100644 index 000000000..64eb05793 --- /dev/null +++ b/groups/app/controllers/repositories_controller.rb @@ -0,0 +1,314 @@ +# 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 'SVG/Graph/Bar' +require 'SVG/Graph/BarHorizontal' +require 'digest/sha1' + +class ChangesetNotFound < Exception; end +class InvalidRevisionParam < Exception; end + +class RepositoriesController < ApplicationController + layout 'base' + menu_item :repository + before_filter :find_repository, :except => :edit + before_filter :find_project, :only => :edit + before_filter :authorize + accept_key_auth :revisions + + def edit + @repository = @project.repository + if !@repository + @repository = Repository.factory(params[:repository_scm]) + @repository.project = @project + end + if request.post? + @repository.attributes = params[:repository] + @repository.save + end + render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'} + end + + def destroy + @repository.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' + end + + def show + # check if new revisions have been committed in the repository + @repository.fetch_changesets if Setting.autofetch_changesets? + # root entries + @entries = @repository.entries('', @rev) + # latest changesets + @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") + show_error_not_found unless @entries || @changesets.any? + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + end + + def browse + @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 + render :action => 'browse' + end + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + end + + def changes + @entry = @repository.scm.entry(@path, @rev) + show_error_not_found and return unless @entry + @changesets = @repository.changesets_for_path(@path) + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + 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) + + 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.scm.entry(@path, @rev) + show_error_not_found and return unless @entry + + # If the entry is a dir, show the browser + browse and return if @entry.is_dir? + + @content = @repository.scm.cat(@path, @rev) + show_error_not_found and return unless @content + if 'raw' == params[:format] || @content.is_binary_data? + # Force the download if it's a binary file + 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 + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + end + + def annotate + @annotate = @repository.scm.annotate(@path, @rev) + render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + end + + def revision + @changeset = @repository.changesets.find_by_revision(@rev) + raise ChangesetNotFound unless @changeset + @changes_count = @changeset.changes.size + @changes_pages = Paginator.new self, @changes_count, 150, params['page'] + @changes = @changeset.changes.find(:all, + :limit => @changes_pages.items_per_page, + :offset => @changes_pages.current.offset) + + respond_to do |format| + format.html + format.js {render :layout => false} + end + rescue ChangesetNotFound + show_error_not_found + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + end + + def diff + @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, @diff_type) + show_error_not_found unless @diff + end + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) + 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 + + REV_PARAM_RE = %r{^[a-f0-9]*$} + + 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] + @rev_to = params[:rev_to] + raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE) + 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 + + def show_error_command_failed(msg) + render_error l(:error_scm_command_failed, msg) + 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 = [] + month_names = l(:actionview_datehelper_select_month_names_abbr).split(',') + 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]} + + graph = SVG::Graph::Bar.new( + :height => 300, + :width => 500, + :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.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 => 300, + :width => 500, + :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 diff --git a/groups/app/controllers/roles_controller.rb b/groups/app/controllers/roles_controller.rb new file mode 100644 index 000000000..9fdd9701b --- /dev/null +++ b/groups/app/controllers/roles_controller.rb @@ -0,0 +1,116 @@ +# 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 + layout 'base' + 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 => 'list' + 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 => 'list' + end + @permissions = @role.setable_permissions + end + + def destroy + @role = Role.find(params[:id]) + @role.destroy + redirect_to :action => 'list' + rescue + flash[:error] = 'This role is in use and can not be deleted.' + redirect_to :action => 'index' + end + + def move + @role = Role.find(params[:id]) + case params[:position] + when 'highest' + @role.move_to_top + when 'higher' + @role.move_higher + when 'lower' + @role.move_lower + when 'lowest' + @role.move_to_bottom + end if params[:position] + redirect_to :action => 'list' + end + + def workflow + @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 => 'workflow', :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 + + 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 => 'list' + end + end +end diff --git a/groups/app/controllers/search_controller.rb b/groups/app/controllers/search_controller.rb new file mode 100644 index 000000000..f15653b63 --- /dev/null +++ b/groups/app/controllers/search_controller.rb @@ -0,0 +1,111 @@ +# 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 + layout 'base' + + 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? + + offset = nil + begin; offset = params[:offset].to_time if params[:offset]; rescue; end + + # quick jump to an issue + if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current)) + redirect_to :controller => "issues", :action => "show", :id => $1 + return + end + + if @project + # only show what the user is allowed to view + @object_types = %w(issues news documents changesets wiki_pages messages) + @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} + + @scope = @object_types.select {|t| params[t]} + @scope = @object_types if @scope.empty? + else + @object_types = @scope = %w(projects) + end + + # 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 = [] + limit = 10 + if @project + @scope.each do |s| + @results += s.singularize.camelcase.constantize.search(like_tokens, @project, + :all_words => @all_words, + :titles_only => @titles_only, + :limit => (limit+1), + :offset => offset, + :before => params[:previous].nil?) + 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 + operator = @all_words ? ' AND ' : ' OR ' + @results += Project.find(:all, + :limit => limit, + :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] + ) if @scope.include? 'projects' + # if only one project is found, user is redirected to its overview + redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 + 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 diff --git a/groups/app/controllers/settings_controller.rb b/groups/app/controllers/settings_controller.rb new file mode 100644 index 000000000..c7c8751dd --- /dev/null +++ b/groups/app/controllers/settings_controller.rb @@ -0,0 +1,55 @@ +# 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 + layout 'base' + 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) + 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] } + end + + def plugin + plugin_id = params[:id].to_sym + @plugin = Redmine::Plugin.registered_plugins[plugin_id] + if request.post? + Setting["plugin_#{plugin_id}"] = params[:settings] + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'plugin', :id => params[:id] + end + @partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial] + @settings = Setting["plugin_#{plugin_id}"] + end +end diff --git a/groups/app/controllers/sys_controller.rb b/groups/app/controllers/sys_controller.rb new file mode 100644 index 000000000..6065c2833 --- /dev/null +++ b/groups/app/controllers/sys_controller.rb @@ -0,0 +1,47 @@ +# 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 SysController < ActionController::Base + wsdl_service_name 'Sys' + web_service_api SysApi + web_service_scaffold :invoke + + before_invocation :check_enabled + + # Returns the projects list, with their repositories + def projects + Project.find(:all, :include => :repository) + end + + # Registers a repository for the given project identifier + # (Subversion specific) + def repository_created(identifier, url) + project = Project.find_by_identifier(identifier) + # Do not create the repository if the project has already one + return 0 unless project && project.repository.nil? + logger.debug "Repository for #{project.name} was created" + repository = Repository.factory('Subversion', :project => project, :url => url) + repository.save + repository.id || 0 + end + +protected + + def check_enabled(name, args) + Setting.sys_api_enabled? + end +end diff --git a/groups/app/controllers/timelog_controller.rb b/groups/app/controllers/timelog_controller.rb new file mode 100644 index 000000000..29c2635d6 --- /dev/null +++ b/groups/app/controllers/timelog_controller.rb @@ -0,0 +1,254 @@ +# 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 + layout 'base' + menu_item :issues + before_filter :find_project, :authorize + + 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 => Enumeration, + :label => :label_activity}, + 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", + :klass => Issue, + :label => :label_issue} + } + + # Add list and boolean custom fields as available criterias + @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| + @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.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 = "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 (%s)" % @project.project_condition(Setting.display_subprojects_issues?) + sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries) + sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)] + 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).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') } + end + end + + def details + sort_init 'spent_on', 'desc' + sort_update + + cond = ARCondition.new + cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) : + ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]) + + 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.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).read, :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 => Date.today) + @time_entry.attributes = params[:time_entry] + if request.post? and @time_entry.save + flash[:notice] = l(:notice_successful_update) + redirect_to(params[:back_url] || {:action => 'details', :project_id => @time_entry.project}) + return + end + @activities = Enumeration::get_values('ACTI') + 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 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 + + # 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.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1 + @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) + end +end diff --git a/groups/app/controllers/trackers_controller.rb b/groups/app/controllers/trackers_controller.rb new file mode 100644 index 000000000..3d7dbd5c5 --- /dev/null +++ b/groups/app/controllers/trackers_controller.rb @@ -0,0 +1,80 @@ +# 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 TrackersController < ApplicationController + layout 'base' + 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, :move ], :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' + end + @trackers = Tracker.find :all, :order => 'position' + 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' + end + end + + def move + @tracker = Tracker.find(params[:id]) + case params[:position] + when 'highest' + @tracker.move_to_top + when 'higher' + @tracker.move_higher + when 'lower' + @tracker.move_lower + when 'lowest' + @tracker.move_to_bottom + end if params[:position] + redirect_to :action => 'list' + 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 diff --git a/groups/app/controllers/users_controller.rb b/groups/app/controllers/users_controller.rb new file mode 100644 index 000000000..48fc6fade --- /dev/null +++ b/groups/app/controllers/users_controller.rb @@ -0,0 +1,113 @@ +# 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 UsersController < ApplicationController + layout 'base' + before_filter :require_admin + + helper :sort + include SortHelper + helper :custom_fields + include CustomFieldsHelper + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + sort_init 'login', 'asc' + sort_update + + @status = params[:status] ? params[:status].to_i : 1 + conditions = "status <> 0" + conditions = ["status=?", @status] unless @status == 0 + + @user_count = User.count(:conditions => conditions) + @user_pages = Paginator.new self, @user_count, + per_page_option, + params['page'] + @users = User.find :all,:order => sort_clause, + :conditions => conditions, + :limit => @user_pages.items_per_page, + :offset => @user_pages.current.offset + + render :action => "list", :layout => false if request.xhr? + end + + def add + if request.get? + @user = User.new(:language => Setting.default_language) + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } + 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 + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } + @user.custom_values = @custom_values + if @user.save + Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + end + end + @auth_sources = AuthSource.find(:all) + end + + def edit + @user = User.find(params[:id]) + if request.get? + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } + else + @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 + if params[:custom_fields] + @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } + @user.custom_values = @custom_values + end + if @user.update_attributes(params[:user]) + flash[:notice] = l(:notice_successful_update) + # Give a string to redirect_to otherwise it would use status param as the response code + redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page])) + end + end + @auth_sources = AuthSource.find(:all) + @roles = Role.find_all_givable + @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects + @membership ||= Member.new + end + + def edit_membership + @user = User.find(params[:id]) + @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user) + @membership.attributes = params[:membership] + if request.post? and @membership.save + flash[:notice] = l(:notice_successful_update) + end + redirect_to :action => 'edit', :id => @user and return + end + + def destroy_membership + @user = User.find(params[:id]) + if request.post? and Member.find(params[:membership_id]).destroy + flash[:notice] = l(:notice_successful_update) + end + redirect_to :action => 'edit', :id => @user and return + end +end diff --git a/groups/app/controllers/versions_controller.rb b/groups/app/controllers/versions_controller.rb new file mode 100644 index 000000000..aeb802ccb --- /dev/null +++ b/groups/app/controllers/versions_controller.rb @@ -0,0 +1,70 @@ +# 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 + layout 'base' + menu_item :roadmap + before_filter :find_project, :authorize + + 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 destroy + @version.destroy + redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project + rescue + flash[:error] = "Unable to delete version" + redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project + end + + def download + @attachment = @version.attachments.find(params[:attachment_id]) + @attachment.increment_download + send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), + :type => @attachment.content_type + rescue + render_404 + end + + def destroy_file + @version.attachments.find(params[:attachment_id]).destroy + flash[:notice] = l(:notice_successful_delete) + redirect_to :controller => 'projects', :action => 'list_files', :id => @project + end + + def status_by + respond_to do |format| + format.html { render :action => 'show' } + format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} } + end + end + +private + def find_project + @version = Version.find(params[:id]) + @project = @version.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/groups/app/controllers/watchers_controller.rb b/groups/app/controllers/watchers_controller.rb new file mode 100644 index 000000000..206dc0843 --- /dev/null +++ b/groups/app/controllers/watchers_controller.rb @@ -0,0 +1,49 @@ +# 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 + layout 'base' + before_filter :require_login, :find_project, :check_project_privacy + + def add + user = User.current + @watched.add_watcher(user) + respond_to do |format| + format.html { render :text => 'Watcher added.', :layout => true } + format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + end + end + + def remove + user = User.current + @watched.remove_watcher(user) + respond_to do |format| + format.html { render :text => 'Watcher removed.', :layout => true } + format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + 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 +end diff --git a/groups/app/controllers/welcome_controller.rb b/groups/app/controllers/welcome_controller.rb new file mode 100644 index 000000000..b4be7fb1c --- /dev/null +++ b/groups/app/controllers/welcome_controller.rb @@ -0,0 +1,25 @@ +# 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 + layout 'base' + + def index + @news = News.latest User.current + @projects = Project.latest User.current + end +end diff --git a/groups/app/controllers/wiki_controller.rb b/groups/app/controllers/wiki_controller.rb new file mode 100644 index 000000000..53c5ec53b --- /dev/null +++ b/groups/app/controllers/wiki_controller.rb @@ -0,0 +1,181 @@ +# 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 + layout 'base' + before_filter :find_wiki, :authorize + + verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index } + + helper :attachments + include AttachmentsHelper + + # 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 + @content = @page.content_for_version(params[:version]) + if params[:export] == 'html' + export = render_to_string :action => 'export', :layout => false + send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") + return + elsif params[:export] == 'txt' + send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") + return + end + render :action => 'show' + end + + # edit an existing page or a new one + def edit + @page = @wiki.find_or_new_page(params[:page]) + @page.content = WikiContent.new(:page => @page) if @page.new_record? + + @content = @page.content_for_version(params[:version]) + @content.text = "h1. #{@page.pretty_title}" if @content.text.blank? + # don't keep previous comment + @content.comments = nil + if request.post? + 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) + 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 + @page = @wiki.find_page(params[:page]) + @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 + + # show page history + def history + @page = @wiki.find_page(params[:page]) + + @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 + @page = @wiki.find_page(params[:page]) + @diff = @page.diff(params[:version], params[:version_from]) + render_404 unless @diff + end + + def annotate + @page = @wiki.find_page(params[:page]) + @annotate = @page.annotate(params[:version]) + end + + # remove a wiki page and its history + def destroy + @page = @wiki.find_page(params[:page]) + @page.destroy if @page + 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} + # 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]) + @attachements = page.attachments if page + @text = params[:content][:text] + render :partial => 'common/preview' + end + + def add_attachment + @page = @wiki.find_page(params[:page]) + attach_files(@page, params[:attachments]) + redirect_to :action => 'index', :page => @page.title + end + + def destroy_attachment + @page = @wiki.find_page(params[:page]) + @page.attachments.find(params[:attachment_id]).destroy + 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 +end diff --git a/groups/app/controllers/wikis_controller.rb b/groups/app/controllers/wikis_controller.rb new file mode 100644 index 000000000..6054abd9a --- /dev/null +++ b/groups/app/controllers/wikis_controller.rb @@ -0,0 +1,45 @@ +# 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 + layout 'base' + 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 diff --git a/groups/app/helpers/account_helper.rb b/groups/app/helpers/account_helper.rb new file mode 100644 index 000000000..ff18b76a4 --- /dev/null +++ b/groups/app/helpers/account_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/admin_helper.rb b/groups/app/helpers/admin_helper.rb new file mode 100644 index 000000000..1b41d8374 --- /dev/null +++ b/groups/app/helpers/admin_helper.rb @@ -0,0 +1,23 @@ +# 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 +end diff --git a/groups/app/helpers/application_helper.rb b/groups/app/helpers/application_helper.rb new file mode 100644 index 000000000..47a251053 --- /dev/null +++ b/groups/app/helpers/application_helper.rb @@ -0,0 +1,510 @@ +# 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 ApplicationHelper + include Redmine::WikiFormatting::Macros::Definitions + + def current_role + @current_role ||= User.current.role_for_project(@project) + end + + # 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 user's account page + def link_to_user(user) + user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous' + end + + def link_to_issue(issue, options={}) + link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, 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 show_and_goto_link(name, id, options={}) + onclick = "Element.show('#{id}'); " + onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") + onclick << "Element.scrollTo('#{id}'); " + onclick << "return false;" + link_to(name, "#", options.merge(: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_date(date) + return nil unless date + # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed) + @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) + date.strftime(@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 + if time.utc? + local = zone ? zone.adjust(time) : time.getlocal + else + local = zone ? zone.adjust(time.getutc) : time + end + @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) + @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) + include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) + end + + def html_hours(text) + text.gsub(%r{(\d+)\.(\d+)}, '\1.\2') + end + + def authoring(created, author) + time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) + l(:label_added_time_by, author || 'Anonymous', time_tag) + end + + def l_or_humanize(s) + l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize + end + + def day_name(day) + l(:general_day_names).split(',')[day-1] + end + + def month_name(month) + l(:actionview_datehelper_select_month_names).split(',')[month-1] + end + + def pagination_links_full(paginator, count=nil, options={}) + page_param = options.delete(:page_param) || :page + url_param = params.dup + # don't reuse params if filters are present + url_param.clear if url_param.has_key?(:set_filter) + + html = '' + html << link_to_remote(('« ' + l(:label_previous)), + {:update => 'content', + :url => url_param.merge(page_param => paginator.current.previous), + :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous + + html << (pagination_links_each(paginator, options) do |n| + link_to_remote(n.to_s, + {:url => {:params => url_param.merge(page_param => n)}, + :update => 'content', + :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(:params => url_param.merge(page_param => n))}) + end || '') + + html << ' ' + link_to_remote((l(:label_next) + ' »'), + {:update => 'content', + :url => url_param.merge(page_param => paginator.current.next), + :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next + + 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)}, + {:href => url_for(url_param.merge(:per_page => n))}) + end + links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil + end + + def breadcrumb(*args) + content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') + 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.compact.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 = nil + 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 + text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m| + style = $1 + filename = $6 + rf = Regexp.new(filename, Regexp::IGNORECASE) + # search for the picture in attachments + if found = attachments.detect { |att| att.filename =~ rf } + image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found + desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") + alt = desc.blank? ? nil : "(#{desc})" + "!#{style}#{image_url}#{alt}!" + else + "!#{style}#{filename}!" + end + end + end + + text = (Setting.text_formatting == 'textile') ? + Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } : + simple_format(auto_link(h(text))) + + # 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| "#{title}.html" } + when :anchor + # used for single-file wiki export + format_wiki_link = Proc.new {|project, title| "##{title}" } + else + format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) } + 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 + # 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)), + :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + title || page + 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 + text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\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(changeset.comments, 100)) + end + elsif sep == '#' + oid = oid.to_i + case prefix + when nil + if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current)) + link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid}, + :class => (issue.closed? ? 'issue closed' : 'issue'), + :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})") + link = content_tag('del', link) if issue.closed? + 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 + 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(changeset.comments, 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 => 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+/, "

"). # 2+ newline -> 2 br + gsub(/([^\n]\n)(?=[^\n])/, '\1
') # 1 newline -> br + end + + def error_messages_for(object_name, options = {}) + options = options.symbolize_keys + object = instance_variable_get("@#{object_name}") + if object && !object.errors.empty? + # build full_messages here with controller current language + full_messages = [] + object.errors.each do |attr, msg| + next if msg.nil? + msg = msg.first if msg.is_a? Array + if attr == "base" + full_messages << l(msg) + else + full_messages << "« " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " » " + l(msg) unless attr == "custom_values" + end + end + # retrieve custom values error messages + if object.errors[:custom_values] + object.custom_values.each do |v| + v.errors.each do |attr, msg| + next if msg.nil? + msg = msg.first if msg.is_a? Array + full_messages << "« " + v.custom_field.name + " » " + l(msg) + end + end + end + content_tag("div", + content_tag( + options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":" + ) + + content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }), + "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" + ) + else + "" + end + end + + def lang_options_for_select(blank=true) + (blank ? [["(auto)", ""]] : []) + + GLoc.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 + hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER']) + 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[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', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') + + (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') + + (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :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}.js") + + javascript_include_tag('calendar/calendar-setup') + + stylesheet_link_tag('calendar') + end + end + end + + def wikitoolbar_for(field_id) + return '' unless Setting.text_formatting == 'textile' + + help_link = l(:setting_text_formatting) + ': ' + + link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'), + :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"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/lang/jstoolbar-#{current_language}") + + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();") + 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 +end diff --git a/groups/app/helpers/attachments_helper.rb b/groups/app/helpers/attachments_helper.rb new file mode 100644 index 000000000..989cd3e66 --- /dev/null +++ b/groups/app/helpers/attachments_helper.rb @@ -0,0 +1,25 @@ +# 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 the links to a collection of attachments + def link_to_attachments(attachments, options = {}) + if attachments.any? + render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options} + end + end +end diff --git a/groups/app/helpers/auth_sources_helper.rb b/groups/app/helpers/auth_sources_helper.rb new file mode 100644 index 000000000..d47e9856a --- /dev/null +++ b/groups/app/helpers/auth_sources_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/boards_helper.rb b/groups/app/helpers/boards_helper.rb new file mode 100644 index 000000000..3719e0fe8 --- /dev/null +++ b/groups/app/helpers/boards_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/custom_fields_helper.rb b/groups/app/helpers/custom_fields_helper.rb new file mode 100644 index 000000000..61c8d6b36 --- /dev/null +++ b/groups/app/helpers/custom_fields_helper.rb @@ -0,0 +1,84 @@ +# 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', :label => :label_issue_plural}, + {:name => 'ProjectCustomField', :label => :label_project_plural}, + {:name => 'UserCustomField', :label => :label_user_plural} + ] + end + + # Return custom field html tag corresponding to its format + def custom_field_tag(custom_value) + custom_field = custom_value.custom_field + field_name = "custom_fields[#{custom_field.id}]" + field_id = "custom_fields_#{custom_field.id}" + + case custom_field.field_format + when "date" + text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) + + calendar_for(field_id) + when "text" + text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%' + when "bool" + check_box 'custom_value', 'value', :name => field_name, :id => field_id + when "list" + select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id + else + text_field 'custom_value', 'value', :name => field_name, :id => field_id + end + end + + # Return custom field label tag + def custom_field_label_tag(custom_value) + content_tag "label", custom_value.custom_field.name + + (custom_value.custom_field.is_required? ? " *" : ""), + :for => "custom_fields_#{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(custom_value) + custom_field_label_tag(custom_value) + custom_field_tag(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_YesNo(value == "1") + 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 diff --git a/groups/app/helpers/documents_helper.rb b/groups/app/helpers/documents_helper.rb new file mode 100644 index 000000000..7e96a6db3 --- /dev/null +++ b/groups/app/helpers/documents_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/enumerations_helper.rb b/groups/app/helpers/enumerations_helper.rb new file mode 100644 index 000000000..c0daf64d2 --- /dev/null +++ b/groups/app/helpers/enumerations_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/ifpdf_helper.rb b/groups/app/helpers/ifpdf_helper.rb new file mode 100644 index 000000000..2cfca1929 --- /dev/null +++ b/groups/app/helpers/ifpdf_helper.rb @@ -0,0 +1,85 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'iconv' +require 'rfpdf/chinese' + +module IfpdfHelper + + class IFPDF < FPDF + include GLoc + attr_accessor :footer_date + + def initialize(lang) + super() + set_language_if_valid lang + case current_language.to_s + when 'ja' + extend(PDF_Japanese) + AddSJISFont() + @font_for_content = 'SJIS' + @font_for_footer = 'SJIS' + when 'zh' + extend(PDF_Chinese) + AddGBFont() + @font_for_content = 'GB' + @font_for_footer = 'GB' + when 'zh-tw' + extend(PDF_Chinese) + AddBig5Font() + @font_for_content = 'Big5' + @font_for_footer = 'Big5' + else + @font_for_content = 'Arial' + @font_for_footer = 'Helvetica' + end + SetCreator("redMine #{Redmine::VERSION}") + SetFont(@font_for_content) + end + + def SetFontStyle(style, size) + SetFont(@font_for_content, style, size) + end + + def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='') + @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8') + # these quotation marks are not correctly rendered in the pdf + txt = txt.gsub(/[“â€]/, '"') if txt + txt = begin + # 0x5c char handling + txtar = txt.split('\\') + txtar << '' if txt[-1] == ?\\ + txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\") + rescue + txt + end || '' + super w,h,txt,border,ln,align,fill,link + end + + def Footer + SetFont(@font_for_footer, 'I', 8) + SetY(-15) + SetX(15) + Cell(0, 5, @footer_date, 0, 0, 'L') + SetY(-15) + SetX(-30) + Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C') + end + + end + +end diff --git a/groups/app/helpers/issue_categories_helper.rb b/groups/app/helpers/issue_categories_helper.rb new file mode 100644 index 000000000..0109e7fae --- /dev/null +++ b/groups/app/helpers/issue_categories_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/issue_relations_helper.rb b/groups/app/helpers/issue_relations_helper.rb new file mode 100644 index 000000000..377059d5f --- /dev/null +++ b/groups/app/helpers/issue_relations_helper.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/groups/app/helpers/issue_statuses_helper.rb b/groups/app/helpers/issue_statuses_helper.rb new file mode 100644 index 000000000..859b09911 --- /dev/null +++ b/groups/app/helpers/issue_statuses_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/issues_helper.rb b/groups/app/helpers/issues_helper.rb new file mode 100644 index 000000000..6013f1ec8 --- /dev/null +++ b/groups/app/helpers/issues_helper.rb @@ -0,0 +1,177 @@ +# 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 'csv' + +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) + ": #{h(issue.subject)}

" + + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
" + + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
" + + "#{@cached_label_assigned_to}: #{issue.assigned_to}
" + + "#{@cached_label_priority}: #{issue.priority.name}" + 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, + :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 '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 '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 = Enumeration.find_by_id(detail.value) and value = e.name if detail.value + e = Enumeration.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 + 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 + + 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? && Attachment.find_by_id(detail.prop_key) + # Link to the attachment if it has not been removed + value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key) + 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? + label + " " + l(:text_journal_changed, old_value, value) + else + label + " " + l(:text_journal_set_to, value) + end + when 'attachment' + "#{label} #{value} #{l(:label_added)}" + end + else + case detail.property + when 'attr', 'cf' + label + " " + l(:text_journal_deleted) + " (#{old_value})" + when 'attachment' + "#{label} #{old_value} #{l(:label_deleted)}" + end + end + end + + def issues_to_csv(issues, project = nil) + ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + export = StringIO.new + CSV::Writer.generate(export, 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_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, + 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.rewind + export + end +end diff --git a/groups/app/helpers/journals_helper.rb b/groups/app/helpers/journals_helper.rb new file mode 100644 index 000000000..234bfabc0 --- /dev/null +++ b/groups/app/helpers/journals_helper.rb @@ -0,0 +1,37 @@ +# 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) + if editable && !journal.notes.blank? + 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)) + content << content_tag('div', links.join(' '), :class => 'contextual') + end + content << textilizable(journal, :notes) + content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki')) + 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 diff --git a/groups/app/helpers/members_helper.rb b/groups/app/helpers/members_helper.rb new file mode 100644 index 000000000..fcf9c92e6 --- /dev/null +++ b/groups/app/helpers/members_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/messages_helper.rb b/groups/app/helpers/messages_helper.rb new file mode 100644 index 000000000..bf23275c3 --- /dev/null +++ b/groups/app/helpers/messages_helper.rb @@ -0,0 +1,28 @@ +# 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, 60)), :controller => 'messages', + :action => 'show', + :board_id => message.board_id, + :id => message.root, + :anchor => (message.parent_id ? "message-#{message.id}" : nil) + end +end diff --git a/groups/app/helpers/my_helper.rb b/groups/app/helpers/my_helper.rb new file mode 100644 index 000000000..9098f67bc --- /dev/null +++ b/groups/app/helpers/my_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/news_helper.rb b/groups/app/helpers/news_helper.rb new file mode 100644 index 000000000..28d36f31a --- /dev/null +++ b/groups/app/helpers/news_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/projects_helper.rb b/groups/app/helpers/projects_helper.rb new file mode 100644 index 000000000..ffbf25e83 --- /dev/null +++ b/groups/app/helpers/projects_helper.rb @@ -0,0 +1,194 @@ +# 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 format_activity_day(date) + date == Date.today ? l(:label_today).titleize : format_date(date) + end + + def format_activity_description(text) + h(truncate(text, 250)) + 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} + ] + tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} + end + + # Generates a gantt image + # Only defined if RMagick is avalaible + def gantt_image(events, date_from, months, zoom) + 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_date <= date_to ? i.due_date : date_to ) + i_done_date = i.start_date + ((i.due_date - 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 + end if Object.const_defined?(:Magick) +end diff --git a/groups/app/helpers/queries_helper.rb b/groups/app/helpers/queries_helper.rb new file mode 100644 index 000000000..a58c5d0ea --- /dev/null +++ b/groups/app/helpers/queries_helper.rb @@ -0,0 +1,53 @@ +# 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.sortable, :caption => column.caption, + :default_order => column.default_order) : + content_tag('th', column.caption) + 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 :done_ratio + progress_bar(value, :width => '80px') + else + h(value) + end + end + end + end +end diff --git a/groups/app/helpers/reports_helper.rb b/groups/app/helpers/reports_helper.rb new file mode 100644 index 000000000..c733a0634 --- /dev/null +++ b/groups/app/helpers/reports_helper.rb @@ -0,0 +1,36 @@ +# 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 diff --git a/groups/app/helpers/repositories_helper.rb b/groups/app/helpers/repositories_helper.rb new file mode 100644 index 000000000..22bdec9df --- /dev/null +++ b/groups/app/helpers/repositories_helper.rb @@ -0,0 +1,98 @@ +# 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 'coderay' +require 'coderay/helpers/file_type' +require 'iconv' + +module RepositoriesHelper + def syntax_highlight(name, content) + type = CodeRay::FileType[name] + type ? CodeRay.scan(content, type).html : h(content) + end + + def format_revision(txt) + txt.to_s[0,8] + 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) + end + + def scm_select_tag(repository) + container = [[]] + REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]} + select_tag('repository_scm', + options_for_select(container, 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?)) + + '
(http://, https://, svn://, file:///)') + + 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 +end diff --git a/groups/app/helpers/roles_helper.rb b/groups/app/helpers/roles_helper.rb new file mode 100644 index 000000000..ab3a7ff03 --- /dev/null +++ b/groups/app/helpers/roles_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/search_helper.rb b/groups/app/helpers/search_helper.rb new file mode 100644 index 000000000..ed2f40b69 --- /dev/null +++ b/groups/app/helpers/search_helper.rb @@ -0,0 +1,38 @@ +# 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? + regexp = Regexp.new "(#{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 + if i.even? + result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-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 +end diff --git a/groups/app/helpers/settings_helper.rb b/groups/app/helpers/settings_helper.rb new file mode 100644 index 000000000..f4ec5a7a7 --- /dev/null +++ b/groups/app/helpers/settings_helper.rb @@ -0,0 +1,27 @@ +# 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 => 'authentication', :partial => 'settings/authentication', :label => :label_authentication}, + {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking}, + {:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)}, + {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural} + ] + end +end diff --git a/groups/app/helpers/sort_helper.rb b/groups/app/helpers/sort_helper.rb new file mode 100644 index 000000000..f16ff3c7d --- /dev/null +++ b/groups/app/helpers/sort_helper.rb @@ -0,0 +1,160 @@ +# Helpers to sort tables using clickable column headers. +# +# Author: Stuart Rackham , March 2005. +# 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. +# - Icon image identifies 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 +# @items = Contact.find_all nil, sort_clause +# end +# +# Controller (using Pagination module): +# +# helper :sort +# include SortHelper +# +# def list +# sort_init 'last_name' +# sort_update +# @contact_pages, @items = paginate :contacts, +# :order_by => sort_clause, +# :per_page => 10 +# end +# +# View (table header in list.rhtml): +# +# +# +# <%= sort_header_tag('id', :title => 'Sort by contact ID') %> +# <%= sort_header_tag('last_name', :caption => 'Name') %> +# <%= sort_header_tag('phone') %> +# <%= sort_header_tag('address', :width => 200) %> +# +# +# +# - The ascending and descending sort icon images are sort_asc.png and +# sort_desc.png and reside in the application's images directory. +# - Introduces instance variables: @sort_name, @sort_default. +# - Introduces params :sort_key and :sort_order. +# +module SortHelper + + # Initializes the default sort column (default_key) and sort order + # (default_order). + # + # - default_key is a column attribute name. + # - default_order is 'asc' or 'desc'. + # - name is the name of the session hash entry that stores the sort state, + # defaults to '_sort'. + # + def sort_init(default_key, default_order='asc', name=nil) + @sort_name = name || params[:controller] + params[:action] + '_sort' + @sort_default = {:key => default_key, :order => default_order} + end + + # Updates the sort state. Call this in the controller prior to calling + # sort_clause. + # + def sort_update() + if params[:sort_key] + sort = {:key => params[:sort_key], :order => params[:sort_order]} + elsif session[@sort_name] + sort = session[@sort_name] # Previous sort. + else + sort = @sort_default + end + session[@sort_name] = sort + 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() + session[@sort_name][:key] + ' ' + session[@sort_name][:order] + 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. + # - A sort icon image is positioned to the right of the sort link. + # + def sort_link(column, caption, default_order) + key, order = session[@sort_name][:key], session[@sort_name][:order] + if key == column + if order.downcase == 'asc' + icon = 'sort_asc.png' + order = 'desc' + else + icon = 'sort_desc.png' + order = 'asc' + end + else + icon = nil + order = default_order + end + caption = titleize(Inflector::humanize(column)) unless caption + + sort_options = { :sort_key => column, :sort_order => order } + # don't reuse params if filters are present + url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options) + + link_to_remote(caption, + {:update => "content", :url => url_options}, + {:href => url_for(url_options)}) + + (icon ? nbsp(2) + image_tag(icon) : '') + end + + # Returns a table header tag with a sort link for the named column + # attribute. + # + # Options: + # :caption The displayed link name (defaults to titleized column name). + # :title The tag's 'title' attribute (defaults to 'Sort by :caption'). + # + # Other options hash entries generate additional table header tag attributes. + # + # Example: + # + # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %> + # + # Renders: + # + # + # Id + #   Sort_asc + # + # + def sort_header_tag(column, options = {}) + caption = options.delete(:caption) || titleize(Inflector::humanize(column)) + 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 + + private + + # Return n non-breaking spaces. + def nbsp(n) + ' ' * n + end + + # Return capitalized title. + def titleize(title) + title.split.map {|w| w.capitalize }.join(' ') + end + +end diff --git a/groups/app/helpers/timelog_helper.rb b/groups/app/helpers/timelog_helper.rb new file mode 100644 index 000000000..db13556a1 --- /dev/null +++ b/groups/app/helpers/timelog_helper.rb @@ -0,0 +1,135 @@ +# 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 + def select_hours(data, criteria, value) + data.select {|row| row[criteria] == value} + 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') + export = StringIO.new + CSV::Writer.generate(export, 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) + ] + csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + # csv lines + entries.each do |entry| + fields = [l_date(entry.spent_on), + 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, + entry.comments + ] + csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } + end + end + export.rewind + export + end + + def format_criteria_value(criteria, value) + value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format])) + end + + def report_to_csv(criterias, periods, hours) + export = StringIO.new + CSV::Writer.generate(export, 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.rewind + 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 diff --git a/groups/app/helpers/trackers_helper.rb b/groups/app/helpers/trackers_helper.rb new file mode 100644 index 000000000..89f92e333 --- /dev/null +++ b/groups/app/helpers/trackers_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/users_helper.rb b/groups/app/helpers/users_helper.rb new file mode 100644 index 000000000..250ed8ce8 --- /dev/null +++ b/groups/app/helpers/users_helper.rb @@ -0,0 +1,37 @@ +# 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 status_options_for_select(selected) + options_for_select([[l(:label_all), ''], + [l(:status_active), 1], + [l(:status_registered), 2], + [l(:status_locked), 3]], selected) + end + + def change_status_link(user) + url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]} + + 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' + else + link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock' + end + end +end diff --git a/groups/app/helpers/versions_helper.rb b/groups/app/helpers/versions_helper.rb new file mode 100644 index 000000000..0fcc6407c --- /dev/null +++ b/groups/app/helpers/versions_helper.rb @@ -0,0 +1,47 @@ +# 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 diff --git a/groups/app/helpers/watchers_helper.rb b/groups/app/helpers/watchers_helper.rb new file mode 100644 index 000000000..c83c785fc --- /dev/null +++ b/groups/app/helpers/watchers_helper.rb @@ -0,0 +1,36 @@ +# 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 ? 'remove' : 'add'), + :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 +end diff --git a/groups/app/helpers/welcome_helper.rb b/groups/app/helpers/welcome_helper.rb new file mode 100644 index 000000000..753e1f127 --- /dev/null +++ b/groups/app/helpers/welcome_helper.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/groups/app/helpers/wiki_helper.rb b/groups/app/helpers/wiki_helper.rb new file mode 100644 index 000000000..980035bd4 --- /dev/null +++ b/groups/app/helpers/wiki_helper.rb @@ -0,0 +1,56 @@ +# 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 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 << ' ' + change[2] + words_del += 1 + end + end + if add_at + words[add_at] = '' + words[add_at] + words[add_to] = words[add_to] + '' + end + if del_at + words.insert del_at - del_off + dels + words_add, '' + deleted + '' + dels += 1 + del_off += words_del + words_del = 0 + end + end + simple_format_without_paragraph(words.join(' ')) + end +end diff --git a/groups/app/models/attachment.rb b/groups/app/models/attachment.rb new file mode 100644 index 000000000..08f440816 --- /dev/null +++ b/groups/app/models/attachment.rb @@ -0,0 +1,103 @@ +# 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}} + + cattr_accessor :storage_path + @@storage_path = "#{RAILS_ROOT}/files" + + def validate + errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes + 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 = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename + self.content_type = @temp_file.content_type.to_s.chomp + self.filesize = @temp_file.size + end + end + end + + def file + nil + end + + # Copy temp file to its final location + def before_save + if @temp_file && (@temp_file.size > 0) + logger.debug("saving '#{self.diskfile}'") + File.open(diskfile, "wb") do |f| + f.write(@temp_file.read) + end + self.digest = Digest::MD5.hexdigest(File.read(diskfile)) + 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 + if self.filename? + File.delete(diskfile) if File.exist?(diskfile) + end + 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 image? + self.filename =~ /\.(jpe?g|gif|png)$/i + 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 +end diff --git a/groups/app/models/auth_source.rb b/groups/app/models/auth_source.rb new file mode 100644 index 000000000..47c121a13 --- /dev/null +++ b/groups/app/models/auth_source.rb @@ -0,0 +1,51 @@ +# 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, :host, :maximum => 60 + validates_length_of :account_password, :maximum => 60, :allow_nil => true + validates_length_of :account, :base_dn, :maximum => 255 + validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30 + + 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 + attrs = nil + end + return attrs if attrs + end + return nil + end +end diff --git a/groups/app/models/auth_source_ldap.rb b/groups/app/models/auth_source_ldap.rb new file mode 100644 index 000000000..a438bd3c7 --- /dev/null +++ b/groups/app/models/auth_source_ldap.rb @@ -0,0 +1,84 @@ +# 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_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? } + + 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 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) + entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name] + end +end diff --git a/groups/app/models/board.rb b/groups/app/models/board.rb new file mode 100644 index 000000000..26e2004d3 --- /dev/null +++ b/groups/app/models/board.rb @@ -0,0 +1,29 @@ +# 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 +end diff --git a/groups/app/models/change.rb b/groups/app/models/change.rb new file mode 100644 index 000000000..d14f435a4 --- /dev/null +++ b/groups/app/models/change.rb @@ -0,0 +1,22 @@ +# 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 +end diff --git a/groups/app/models/changeset.rb b/groups/app/models/changeset.rb new file mode 100644 index 000000000..3e95ce111 --- /dev/null +++ b/groups/app/models/changeset.rb @@ -0,0 +1,131 @@ +# 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 Changeset < ActiveRecord::Base + belongs_to :repository + 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.comments.blank? ? '' : (': ' + o.comments))}, + :description => :comments, + :datetime => :committed_on, + :author => :committer, + :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} + + acts_as_searchable :columns => 'comments', + :include => :repository, + :project_key => "#{Repository.table_name}.project_id", + :date_column => 'committed_on' + + 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, comment.strip) + end + + def committed_on=(date) + self.commit_date = date + super + end + + def project + repository.project + 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? + user = committer_user || User.anonymous + csettext = "r#{self.revision}" + if self.scmid && (! (csettext =~ /^r[0-9]+$/)) + csettext = "commit:\"#{self.scmid}\"" + end + journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext)) + issue.status = fix_status + issue.done_ratio = done_ratio if done_ratio + issue.save + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + end + end + referenced_issues += target_issues + end + + self.issues = referenced_issues.uniq + end + + # Returns the Redmine User corresponding to the committer + def committer_user + if committer && 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 + + # 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 +end diff --git a/groups/app/models/comment.rb b/groups/app/models/comment.rb new file mode 100644 index 000000000..88d5348da --- /dev/null +++ b/groups/app/models/comment.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/groups/app/models/custom_field.rb b/groups/app/models/custom_field.rb new file mode 100644 index 000000000..990adf9e2 --- /dev/null +++ b/groups/app/models/custom_field.rb @@ -0,0 +1,75 @@ +# 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 + 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 + # remove empty values + self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact + # 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, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty? + errors.add(:possible_values, :activerecord_error_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, :activerecord_error_invalid) unless v.valid? + end + + def <=>(field) + position <=> field.position + end + + # to move in project_custom_field + def self.for_all + find(:all, :conditions => ["is_for_all=?", true]) + end + + def type_name + nil + end +end diff --git a/groups/app/models/custom_value.rb b/groups/app/models/custom_value.rb new file mode 100644 index 000000000..98ce6b168 --- /dev/null +++ b/groups/app/models/custom_value.rb @@ -0,0 +1,50 @@ +# 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 custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?)) + self.value ||= custom_field.default_value + end + end + +protected + def validate + if value.blank? + errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank? + else + errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) + errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length + errors.add(:value, :activerecord_error_too_long) 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, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/ + when 'float' + begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end + when 'date' + errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ + when 'list' + errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) + end + end + end +end diff --git a/groups/app/models/document.rb b/groups/app/models/document.rb new file mode 100644 index 000000000..7a432b46b --- /dev/null +++ b/groups/app/models/document.rb @@ -0,0 +1,30 @@ +# 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 => "Enumeration", :foreign_key => "category_id" + has_many :attachments, :as => :container, :dependent => :destroy + + acts_as_searchable :columns => ['title', 'description'] + 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}} + + validates_presence_of :project, :title, :category + validates_length_of :title, :maximum => 60 +end diff --git a/groups/app/models/enabled_module.rb b/groups/app/models/enabled_module.rb new file mode 100644 index 000000000..3c05c76c1 --- /dev/null +++ b/groups/app/models/enabled_module.rb @@ -0,0 +1,23 @@ +# 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 EnabledModule < ActiveRecord::Base + belongs_to :project + + validates_presence_of :name + validates_uniqueness_of :name, :scope => :project_id +end diff --git a/groups/app/models/enumeration.rb b/groups/app/models/enumeration.rb new file mode 100644 index 000000000..400681a43 --- /dev/null +++ b/groups/app/models/enumeration.rb @@ -0,0 +1,67 @@ +# 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 + acts_as_list :scope => 'opt = \'#{opt}\'' + + before_destroy :check_integrity + + validates_presence_of :opt, :name + validates_uniqueness_of :name, :scope => [:opt] + validates_length_of :name, :maximum => 30 + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + OPTIONS = { + "IPRI" => :enumeration_issue_priorities, + "DCAT" => :enumeration_doc_categories, + "ACTI" => :enumeration_activities + }.freeze + + def self.get_values(option) + find(:all, :conditions => {:opt => option}, :order => 'position') + end + + def self.default(option) + find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position') + end + + def option_name + OPTIONS[self.opt] + end + + def before_save + Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default? + end + + def <=>(enumeration) + position <=> enumeration.position + end + + def to_s; name end + +private + def check_integrity + case self.opt + when "IPRI" + raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id]) + when "DCAT" + raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id]) + when "ACTI" + raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id]) + end + end +end diff --git a/groups/app/models/issue.rb b/groups/app/models/issue.rb new file mode 100644 index 000000000..8082e43b7 --- /dev/null +++ b/groups/app/models/issue.rb @@ -0,0 +1,250 @@ +# 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 => 'Enumeration', :foreign_key => 'priority_id' + belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' + + has_many :journals, :as => :journalized, :dependent => :destroy + has_many :attachments, :as => :container, :dependent => :destroy + has_many :time_entries, :dependent => :delete_all + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :custom_fields, :through => :custom_values + has_and_belongs_to_many :changesets, :order => "revision 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_watchable + acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue} + acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} + + validates_presence_of :subject, :description, :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 + validates_associated :custom_values, :on => :update + + def after_initialize + if new_record? + # set default values for new records only + self.status ||= IssueStatus.default + self.priority ||= Enumeration.default('IPRI') + end + end + + def copy_from(arg) + issue = arg.is_a?(Issue) ? arg : Issue.find(arg) + self.attributes = issue.attributes.dup + self.custom_values = issue.custom_values.collect {|v| v.clone} + self + end + + # Move an issue to a new project and tracker + def move_to(new_project, new_tracker = nil) + transaction do + if new_project && project_id != new_project.id + # delete issue relations + unless Setting.cross_project_issue_relations? + self.relations_from.clear + self.relations_to.clear + end + # issue is moved to another project + self.category = nil + self.fixed_version = nil + self.project = new_project + end + if new_tracker + self.tracker = new_tracker + end + if save + # Manually update project_id on related time entries + TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id}) + else + rollback_db_transaction + return false + end + end + return true + 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, :activerecord_error_not_a_date + end + + if self.due_date and self.start_date and self.due_date < self.start_date + errors.add :due_date, :activerecord_error_greater_than_start_date + end + + if start_date && soonest_start && start_date < soonest_start + errors.add :start_date, :activerecord_error_invalid + end + end + + def validate_on_create + errors.add :tracker_id, :activerecord_error_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 before_save + if @current_journal + # attributes changes + (Issue.column_names - %w(id description)).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 + # Save the issue even if the journal is not saved (because empty) + true + 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 custom_value_for(custom_field) + self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id } + return nil + 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 } + @current_journal + end + + # Return true if the issue is closed, otherwise false + def closed? + self.status.is_closed? + end + + # Users the issue can be assigned to + def assignable_users + project.assignable_users + 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.role_for_project(project), tracker) + statuses << status unless statuses.empty? + statuses.uniq.sort + 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 + + 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 the duplicate issues + def duplicates + relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)} + end + + 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 self.visible_by(usr) + with_scope(:find => { :conditions => Project.visible_by(usr) }) do + yield + end + end + + def to_s + "#{tracker} ##{id}: #{subject}" + end +end diff --git a/groups/app/models/issue_category.rb b/groups/app/models/issue_category.rb new file mode 100644 index 000000000..51baeb419 --- /dev/null +++ b/groups/app/models/issue_category.rb @@ -0,0 +1,43 @@ +# 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 diff --git a/groups/app/models/issue_custom_field.rb b/groups/app/models/issue_custom_field.rb new file mode 100644 index 000000000..d087768a4 --- /dev/null +++ b/groups/app/models/issue_custom_field.rb @@ -0,0 +1,27 @@ +# 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 + diff --git a/groups/app/models/issue_relation.rb b/groups/app/models/issue_relation.rb new file mode 100644 index 000000000..07e940b85 --- /dev/null +++ b/groups/app/models/issue_relation.rb @@ -0,0 +1,79 @@ +# 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_duplicates, :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 + + def validate + if issue_from && issue_to + errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id + errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? + errors.add_to_base :activerecord_error_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 diff --git a/groups/app/models/issue_status.rb b/groups/app/models/issue_status.rb new file mode 100644 index 000000000..ddff9c005 --- /dev/null +++ b/groups/app/models/issue_status.rb @@ -0,0 +1,69 @@ +# 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 before_save + IssueStatus.update_all "is_default=#{connection.quoted_false}" 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(role, tracker) + new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker + new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : [] + 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(role, tracker) + new_statuses = workflows.find(:all, + :include => :new_status, + :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact if role && tracker + new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : [] + end + + def new_status_allowed_to?(status, role, tracker) + status && role && tracker ? + !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? : + false + 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 diff --git a/groups/app/models/journal.rb b/groups/app/models/journal.rb new file mode 100644 index 000000000..1376d349e --- /dev/null +++ b/groups/app/models/journal.rb @@ -0,0 +1,66 @@ +# 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_searchable :columns => 'notes', + :include => :issue, + :project_key => "#{Issue.table_name}.project_id", + :date_column => "#{Issue.table_name}.created_on" + + acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') }, + :description => :notes, + :author => :user, + :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' }, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} + + def save + # 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 diff --git a/groups/app/models/journal_detail.rb b/groups/app/models/journal_detail.rb new file mode 100644 index 000000000..58239006b --- /dev/null +++ b/groups/app/models/journal_detail.rb @@ -0,0 +1,25 @@ +# 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 diff --git a/groups/app/models/mail_handler.rb b/groups/app/models/mail_handler.rb new file mode 100644 index 000000000..7a1d73244 --- /dev/null +++ b/groups/app/models/mail_handler.rb @@ -0,0 +1,40 @@ +# 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 + + # Processes incoming emails + # Currently, it only supports adding a note to an existing issue + # by replying to the initial notification message + def receive(email) + # find related issue by parsing the subject + m = email.subject.match %r{\[.*#(\d+)\]} + return unless m + issue = Issue.find_by_id(m[1]) + return unless issue + + # find user + user = User.find_active(:first, :conditions => {:mail => email.from.first}) + return unless user + # check permission + return unless user.allowed_to?(:add_issue_notes, issue.project) + + # add the note + issue.init_journal(user, email.body.chomp) + issue.save + end +end diff --git a/groups/app/models/mailer.rb b/groups/app/models/mailer.rb new file mode 100644 index 000000000..6fc879a15 --- /dev/null +++ b/groups/app/models/mailer.rb @@ -0,0 +1,194 @@ +# 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 + helper :application + helper :issues + helper :custom_fields + + include ActionController::UrlWriter + + 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 + recipients issue.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) + end + + def issue_edit(journal) + issue = journal.journalized + 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 + 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) + end + + 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) + end + + def attachments_added(attachments) + container = attachments.first.container + added_to = '' + added_to_url = '' + case container.class.name + 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 + end + + def news_added(news) + redmine_headers 'Project' => news.project.identifier + 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) + end + + def message_posted(message, recipients) + redmine_headers 'Project' => message.project.identifier, + 'Topic-Id' => (message.parent_id || message.id) + recipients(recipients) + subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}" + body :message => message, + :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root) + end + + 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') + end + + def account_activation_request(user) + # Send the email to all active administrators + recipients User.find_active(: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') + 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) + 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) + end + + def test(user) + set_language_if_valid(user.language) + recipients user.mail + subject 'Redmine test' + body :url => url_for(:controller => 'welcome') + 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?) + super + end + + private + def initialize_defaults(method_name) + super + set_language_if_valid Setting.default_language + from Setting.mail_from + default_url_options[:host] = Setting.host_name + default_url_options[:protocol] = Setting.protocol + # Common headers + headers 'X-Mailer' => 'Redmine', + 'X-Redmine-Host' => Setting.host_name, + 'X-Redmine-Site' => Setting.app_title + 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 + if User.current.pref[:no_self_notified] + recipients.delete(User.current.mail) if recipients + cc.delete(User.current.mail) if cc + end + # Blind carbon copy recipients + if Setting.bcc_recipients? + bcc([recipients, cc].flatten.compact.uniq) + recipients [] + cc [] + end + super + end + + # Renders a message with the corresponding layout + def render_message(method_name, body) + layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' + body[:content_for_layout] = render(:file => method_name, :body => body) + ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}") + end + + # Makes partial rendering work with Rails 1.2 (retro-compatibility) + def self.controller_path + '' + end unless respond_to?('controller_path') +end diff --git a/groups/app/models/member.rb b/groups/app/models/member.rb new file mode 100644 index 000000000..b4617c229 --- /dev/null +++ b/groups/app/models/member.rb @@ -0,0 +1,42 @@ +# 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 Member < ActiveRecord::Base + belongs_to :user + belongs_to :role + belongs_to :project + + validates_presence_of :role, :user, :project + validates_uniqueness_of :user_id, :scope => :project_id + + def validate + errors.add :role_id, :activerecord_error_invalid if role && !role.member? + end + + def name + self.user.name + end + + def <=>(member) + role == member.role ? (user <=> member.user) : (role <=> member.role) + end + + def before_destroy + # 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 diff --git a/groups/app/models/message.rb b/groups/app/models/message.rb new file mode 100644 index 000000000..a18d126c9 --- /dev/null +++ b/groups/app/models/message.rb @@ -0,0 +1,68 @@ +# 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" + has_many :attachments, :as => :container, :dependent => :destroy + belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' + + acts_as_searchable :columns => ['subject', 'content'], + :include => :board, + :project_key => 'project_id', + :date_column => '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, :id => o.id}} + + attr_protected :locked, :sticky + validates_presence_of :subject, :content + validates_length_of :subject, :maximum => 255 + + 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 + board.update_attribute(:last_message_id, self.id) + board.increment! :messages_count + if parent + parent.reload.update_attribute(:last_reply_id, self.id) + else + board.increment! :topics_count + end + end + + def after_destroy + # The following line is required so that the previous counter + # updates (due to children removal) are not overwritten + board.reload + board.decrement! :messages_count + board.decrement! :topics_count unless parent + end + + def sticky? + sticky == 1 + end + + def project + board.project + end +end diff --git a/groups/app/models/message_observer.rb b/groups/app/models/message_observer.rb new file mode 100644 index 000000000..043988172 --- /dev/null +++ b/groups/app/models/message_observer.rb @@ -0,0 +1,29 @@ +# 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) + # send notification to the authors of the thread + recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?} + # 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 diff --git a/groups/app/models/news.rb b/groups/app/models/news.rb new file mode 100644 index 000000000..3d8c4d661 --- /dev/null +++ b/groups/app/models/news.rb @@ -0,0 +1,34 @@ +# 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 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', 'description'] + acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} + + # returns latest news for projects visible by user + def self.latest(user=nil, count=5) + find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") + end +end diff --git a/groups/app/models/project.rb b/groups/app/models/project.rb new file mode 100644 index 000000000..964469649 --- /dev/null +++ b/groups/app/models/project.rb @@ -0,0 +1,256 @@ +# 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 + + has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :users, :through => :members + has_many :custom_values, :dependent => :delete_all, :as => :customized + 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 :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_tree :order => "name", :counter_cache => true + + acts_as_searchable :columns => ['name', 'description'], :project_key => 'id' + acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} + + attr_protected :status, :enabled_module_names + + validates_presence_of :name, :identifier + validates_uniqueness_of :name, :identifier + validates_associated :custom_values, :on => :update + validates_associated :repository, :wiki + validates_length_of :name, :maximum => 30 + validates_length_of :homepage, :maximum => 60 + validates_length_of :identifier, :in => 3..20 + validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ + + before_destroy :delete_all_members + + def identifier=(identifier) + super unless identifier_frozen? + end + + def identifier_frozen? + errors[:identifier].nil? && !(new_record? || identifier.blank?) + end + + def issues_with_subprojects(include_subprojects=false) + conditions = nil + if include_subprojects && !active_children.empty? + ids = [id] + active_children.collect {|c| c.id} + conditions = ["#{Project.table_name}.id IN (#{ids.join(',')})"] + end + conditions ||= ["#{Project.table_name}.id = ?", id] + # Quick and dirty fix for Rails 2 compatibility + Issue.send(:with_scope, :find => { :conditions => conditions }) do + Version.send(:with_scope, :find => { :conditions => conditions }) do + yield + end + end + 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 + + def self.visible_by(user=nil) + 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 options[:project] + project_statement = "#{Project.table_name}.id = #{options[:project].id}" + project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects] + base_statement = "(#{project_statement}) AND (#{base_statement})" + end + if user.admin? + # no restriction + elsif user.logged? + statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission) + allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id} + statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? + elsif Role.anonymous.allowed_to?(permission) + # anonymous user allowed on public project + statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" + else + # anonymous user is not authorized + statements << "1=0" + end + statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" + end + + def project_condition(with_subprojects) + cond = "#{Project.table_name}.id = #{id}" + cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" 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 + identifier + end + + def active? + self.status == STATUS_ACTIVE + end + + def archive + # Archive subprojects if any + children.each do |subproject| + subproject.archive + end + update_attribute :status, STATUS_ARCHIVED + end + + def unarchive + return false if parent && !parent.active? + update_attribute :status, STATUS_ACTIVE + end + + def active_children + children.select {|child| child.active?} + end + + # Returns an array of the trackers used by the project and its sub projects + def rolled_up_trackers + @rolled_up_trackers ||= + Tracker.find(:all, :include => :projects, + :select => "DISTINCT #{Tracker.table_name}.*", + :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id], + :order => "#{Tracker.table_name}.position") + end + + # Deletes all project's members + def delete_all_members + Member.delete_all(['project_id = ?', id]) + end + + # Users issues can be assigned to + def assignable_users + members.select {|m| m.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 custom_fields_for_issues(tracker) + all_custom_fields.select {|c| tracker.custom_fields.include? c } + end + + def all_custom_fields + @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq + 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]*).*$/m, '\1').strip if description + end + + 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) + enabled_modules.clear + module_names = [] unless module_names && module_names.is_a?(Array) + module_names.each do |name| + enabled_modules << EnabledModule.new(:name => name.to_s) + end + end + +protected + def validate + errors.add(parent_id, " must be a root project") if parent and parent.parent + errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 + errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) + end + +private + 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 +end diff --git a/groups/app/models/project_custom_field.rb b/groups/app/models/project_custom_field.rb new file mode 100644 index 000000000..f0dab6913 --- /dev/null +++ b/groups/app/models/project_custom_field.rb @@ -0,0 +1,22 @@ +# 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 diff --git a/groups/app/models/query.rb b/groups/app/models/query.rb new file mode 100644 index 000000000..641c0d17b --- /dev/null +++ b/groups/app/models/query.rb @@ -0,0 +1,368 @@ +# 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 QueryColumn + attr_accessor :name, :sortable, :default_order + include GLoc + + def initialize(name, options={}) + self.name = name + self.sortable = options[:sortable] + self.default_order = options[:default_order] + end + + def caption + set_language_if_valid(User.current.language) + l("field_#{name}") + end +end + +class QueryCustomFieldColumn < QueryColumn + + def initialize(custom_field) + self.name = "cf_#{custom_field.id}".to_sym + self.sortable = false + @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 + + 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_in_less_than, + ">t+" => :label_in_more_than, + "t+" => :label_in, + "t" => :label_today, + "w" => :label_this_week, + ">t-" => :label_less_than_ago, + " :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", "w", ">t-", " [ ">t-", " [ "=", "~", "!", "!~" ], + :text => [ "~", "!~" ], + :integer => [ "=", ">=", "<=" ] } + + cattr_reader :operators_by_filter_type + + @@available_columns = [ + QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"), + QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), + QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'), + QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), + QueryColumn.new(:author), + QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), + QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), + QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), + QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'), + 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"), + 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 => [""]} } + set_language_if_valid(User.current.language) + 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), :activerecord_error_blank unless + # filter requires one or more values + (values_for(field) and !values_for(field).first.empty?) 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 => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').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 }, + "done_ratio" => { :type => :integer, :order => 13 }} + + 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 project + # project specific filters + @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } + @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } + unless @project.active_children.empty? + @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } + end + @project.all_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 + # remove category filter if no category defined + @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? + 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_custom_fields : + IssueCustomField.find(:all, :conditions => {:is_for_all => true}) + ).collect {|cf| QueryCustomFieldColumn.new(cf) } + end + + def columns + if has_default_columns? + available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) } + 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 statement + # project/subprojects clause + clause = '' + if project && !@project.active_children.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.active_children.collect{|p| p.id} + end + elsif Setting.display_subprojects_issues? + ids += project.active_children.collect{|p| p.id} + end + clause << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',') + elsif project + clause << "#{Issue.table_name}.project_id = %d" % project.id + else + clause << Project.visible_by(User.current) + end + + # 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? + + sql = '' + if field =~ /^cf_(\d+)$/ + # custom field + db_table = CustomValue.table_name + db_field = 'value' + 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 " + else + # regular field + db_table = Issue.table_name + db_field = field + sql << '(' + end + + # "me" value subsitution + if %w(assigned_to_id author_id).include?(field) + v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") + end + + case operator_for field + when "=" + sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + when "!" + sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" + when "!*" + sql = sql + "#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} = ''" + when "*" + sql = sql + "#{db_table}.#{db_field} IS NOT NULL AND #{db_table}.#{db_field} <> ''" + when ">=" + sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}" + when "<=" + sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}" + when "o" + sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" + when "c" + sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" + when ">t-" + sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)] + when "t+" + sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time) + when " :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" + has_many :changes, :through => :changesets + + # 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 entries(path=nil, identifier=nil) + scm.entries(path, identifier) + end + + def diff(path, rev, rev_to, type) + scm.diff(path, rev, rev_to, type) + end + + # Default behaviour: we search in cached changesets + def changesets_for_path(path) + path = "/#{path}" unless path.starts_with?('/') + Change.find(:all, :include => :changeset, + :conditions => ["repository_id = ? AND path = ?", id, path], + :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset) + end + + def latest_changeset + @latest_changeset ||= changesets.find(:first) + end + + def scan_changesets_for_issue_ids + self.changesets.each(&:scan_comment_for_issue_ids) + 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 +end diff --git a/groups/app/models/repository/bazaar.rb b/groups/app/models/repository/bazaar.rb new file mode 100644 index 000000000..1b75066c2 --- /dev/null +++ b/groups/app/models/repository/bazaar.rb @@ -0,0 +1,86 @@ +# 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? + 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 diff --git a/groups/app/models/repository/cvs.rb b/groups/app/models/repository/cvs.rb new file mode 100644 index 000000000..c2d8be977 --- /dev/null +++ b/groups/app/models/repository/cvs.rb @@ -0,0 +1,155 @@ +# 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, identifier) + e = entries(path, identifier) + e ? e.first : nil + 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 diff(path, rev, rev_to, type) + #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 + diff=diff+scm.diff(change_from.path, revision_from, revision_to, type) + 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=>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 diff --git a/groups/app/models/repository/darcs.rb b/groups/app/models/repository/darcs.rb new file mode 100644 index 000000000..c7c14a397 --- /dev/null +++ b/groups/app/models/repository/darcs.rb @@ -0,0 +1,90 @@ +# 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 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 diff(path, rev, rev_to, type) + 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, type) : 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 diff --git a/groups/app/models/repository/git.rb b/groups/app/models/repository/git.rb new file mode 100644 index 000000000..7213588ac --- /dev/null +++ b/groups/app/models/repository/git.rb @@ -0,0 +1,70 @@ +# 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 changesets_for_path(path) + Change.find(:all, :include => :changeset, + :conditions => ["repository_id = ? AND path = ?", id, path], + :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset) + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision : nil + # latest revision in the repository + scm_revision = scm_info.lastrev.scmid + + unless changesets.find_by_scmid(scm_revision) + + revisions = scm.revisions('', db_revision, nil) + transaction do + revisions.reverse_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 + end + end + end +end diff --git a/groups/app/models/repository/mercurial.rb b/groups/app/models/repository/mercurial.rb new file mode 100644 index 000000000..18cbc9495 --- /dev/null +++ b/groups/app/models/repository/mercurial.rb @@ -0,0 +1,94 @@ +# 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 diff --git a/groups/app/models/repository/subversion.rb b/groups/app/models/repository/subversion.rb new file mode 100644 index 000000000..0c2239c43 --- /dev/null +++ b/groups/app/models/repository/subversion.rb @@ -0,0 +1,74 @@ +# 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|svn\+ssh|file):\/\/.+/i + + def scm_adapter + Redmine::Scm::Adapters::SubversionAdapter + end + + def self.scm_name + 'Subversion' + end + + def changesets_for_path(path) + revisions = scm.revisions(path) + revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : [] + 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, + :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 diff --git a/groups/app/models/role.rb b/groups/app/models/role.rb new file mode 100644 index 000000000..6f1fb4768 --- /dev/null +++ b/groups/app/models/role.rb @@ -0,0 +1,119 @@ +# 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 + + 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 workflows (tracker_id, old_status_id, new_status_id, role_id)" + + " SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" + + " FROM workflows" + + " WHERE role_id = #{role.id}" + end + end + + has_many :members + acts_as_list + + serialize :permissions + 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 if perms + write_attribute(:permissions, perms) + end + + def <=>(role) + position <=> role.position + 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 diff --git a/groups/app/models/setting.rb b/groups/app/models/setting.rb new file mode 100644 index 000000000..185991d9b --- /dev/null +++ b/groups/app/models/setting.rb @@ -0,0 +1,125 @@ +# 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' + ] + + cattr_accessor :available_settings + @@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) + Redmine::Plugin.registered_plugins.each do |id, plugin| + next unless plugin.settings + @@available_settings["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 + + # 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 diff --git a/groups/app/models/time_entry.rb b/groups/app/models/time_entry.rb new file mode 100644 index 000000000..ddaff2b60 --- /dev/null +++ b/groups/app/models/time_entry.rb @@ -0,0 +1,65 @@ +# 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 => 'Enumeration', :foreign_key => :activity_id + + attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek + + validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on + validates_numericality_of :hours, :allow_nil => true + validates_length_of :comments, :maximum => 255 + + def before_validation + self.project = issue.project if issue && project.nil? + end + + def validate + errors.add :hours, :activerecord_error_invalid if hours && (hours < 0 || hours >= 1000) + errors.add :project_id, :activerecord_error_invalid if project.nil? + errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project) + end + + def hours=(h) + write_attribute :hours, (h.is_a?(String) ? h.to_hours : 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 diff --git a/groups/app/models/token.rb b/groups/app/models/token.rb new file mode 100644 index 000000000..0e8c2c3e2 --- /dev/null +++ b/groups/app/models/token.rb @@ -0,0 +1,44 @@ +# 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 Token < ActiveRecord::Base + belongs_to :user + + @@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 + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + token_value = '' + 40.times { |i| token_value << chars[rand(chars.size-1)] } + token_value + end +end diff --git a/groups/app/models/tracker.rb b/groups/app/models/tracker.rb new file mode 100644 index 000000000..ecee908eb --- /dev/null +++ b/groups/app/models/tracker.rb @@ -0,0 +1,56 @@ +# 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 workflows (tracker_id, old_status_id, new_status_id, role_id)" + + " SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" + + " FROM workflows" + + " 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 diff --git a/groups/app/models/user.rb b/groups/app/models/user.rb new file mode 100644 index 000000000..a67a08567 --- /dev/null +++ b/groups/app/models/user.rb @@ -0,0 +1,291 @@ +# 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/sha1" + +class User < ActiveRecord::Base + + class OnTheFlyCreationFailure < Exception; end + + # 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_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all + has_many :projects, :through => :memberships + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :issue_categories, :foreign_key => 'assigned_to_id', :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 + + attr_accessor :password, :password_confirmation + attr_accessor :last_before_login_on + # Prevents unauthorized assignments + attr_protected :login, :admin, :password, :password_confirmation, :hashed_password + + 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? } + # 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_length_of :password, :minimum => 4, :allow_nil => true + validates_confirmation_of :password, :allow_nil => true + validates_associated :custom_values, :on => :update + + 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 self.active + with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do + yield + end + end + + def self.find_active(*args) + active do + find(*args) + end + 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 + onthefly = new(*attrs) + onthefly.login = login + onthefly.language = Setting.default_language + if onthefly.save + user = find(:first, :conditions => ["login=?", login]) + logger.info("User '#{user.login}' created from the LDAP") if logger + else + logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger + raise OnTheFlyCreationFailure.new + end + end + end + user.update_attribute(:last_login_on, Time.now) if user + user + rescue => text + raise text + end + + # Return user's full name for display + def name(formatter = nil) + f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] + eval '"' + f + '"' + 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 + + def pref + self.preference ||= UserPreference.new(:user => self) + end + + def time_zone + self.pref.time_zone.nil? ? nil : 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 + + def self.find_by_autologin_key(key) + token = Token.find_by_action_and_value('autologin', key) + token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil + end + + def <=>(user) + if user.nil? + -1 + elsif lastname.to_s.downcase == user.lastname.to_s.downcase + firstname.to_s.downcase <=> user.firstname.to_s.downcase + else + lastname.to_s.downcase <=> user.lastname.to_s.downcase + end + end + + def to_s + name + end + + def logged? + true + end + + # Return user's role for project + def role_for_project(project) + # No role on archived projects + return nil unless project && project.active? + if logged? + # Find project membership + membership = memberships.detect {|m| m.project_id == project.id} + if membership + membership.role + else + @role_non_member ||= Role.non_member + end + else + @role_anonymous ||= Role.anonymous + end + end + + # Return true if the user is a member of project + def member_of?(project) + role_for_project(project).member? + 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? + + role = role_for_project(project) + return false unless role + role.allowed_to?(action) && (project.is_public? || role.member?) + + elsif options[:global] + # authorize if user has at least one role that has this permission + roles = memberships.collect {|m| m.role}.uniq + roles.detect {|r| r.allowed_to?(action)} + else + false + end + end + + def self.current=(user) + @current_user = user + end + + def self.current + @current_user ||= User.anonymous + end + + def self.anonymous + return @anonymous_user if @anonymous_user + 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 = anonymous_user + 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 + + # Overrides a few properties + def logged?; false end + def admin; false end + def name; 'Anonymous' end + def mail; nil end + def time_zone; nil end + def rss_key; nil end +end diff --git a/groups/app/models/user_custom_field.rb b/groups/app/models/user_custom_field.rb new file mode 100644 index 000000000..99e76eea5 --- /dev/null +++ b/groups/app/models/user_custom_field.rb @@ -0,0 +1,23 @@ +# 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 + diff --git a/groups/app/models/user_preference.rb b/groups/app/models/user_preference.rb new file mode 100644 index 000000000..73e4a50c6 --- /dev/null +++ b/groups/app/models/user_preference.rb @@ -0,0 +1,52 @@ +# 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 + self.others ||= {} + self.others.store attr_name, value + end + end + + def comments_sorting; self[:comments_sorting] end + def comments_sorting=(order); self[:comments_sorting]=order end +end diff --git a/groups/app/models/version.rb b/groups/app/models/version.rb new file mode 100644 index 000000000..dc618af0f --- /dev/null +++ b/groups/app/models/version.rb @@ -0,0 +1,106 @@ +# 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' + has_many :attachments, :as => :container, :dependent => :destroy + + 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 => :activerecord_error_not_a_date, :allow_nil => true + + 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 + + # Returns true if the version is completed: due date reached and no open issues + def completed? + effective_date && (effective_date <= Date.today) && (open_issues_count == 0) + end + + def completed_pourcent + if fixed_issues.count == 0 + 0 + elsif open_issues_count == 0 + 100 + else + (closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count + end + end + + def closed_pourcent + if fixed_issues.count == 0 + 0 + else + closed_issues_count * 100.0 / fixed_issues.count + 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 + + def open_issues_count + @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status) + end + + 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 +end diff --git a/groups/app/models/watcher.rb b/groups/app/models/watcher.rb new file mode 100644 index 000000000..cb6ff52ea --- /dev/null +++ b/groups/app/models/watcher.rb @@ -0,0 +1,23 @@ +# 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_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] +end diff --git a/groups/app/models/wiki.rb b/groups/app/models/wiki.rb new file mode 100644 index 000000000..b6d6a9b50 --- /dev/null +++ b/groups/app/models/wiki.rb @@ -0,0 +1,54 @@ +# 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 Wiki < ActiveRecord::Base + belongs_to :project + has_many :pages, :class_name => 'WikiPage', :dependent => :destroy + has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all + + 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 + + # 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 diff --git a/groups/app/models/wiki_content.rb b/groups/app/models/wiki_content.rb new file mode 100644 index 000000000..724354ad6 --- /dev/null +++ b/groups/app/models/wiki_content.rb @@ -0,0 +1,78 @@ +# 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 + + acts_as_versioned + 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}} + + 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 diff --git a/groups/app/models/wiki_page.rb b/groups/app/models/wiki_page.rb new file mode 100644 index 000000000..8ce71cb80 --- /dev/null +++ b/groups/app/models/wiki_page.rb @@ -0,0 +1,160 @@ +# 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' +require 'enumerator' + +class WikiPage < ActiveRecord::Base + belongs_to :wiki + has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy + has_many :attachments, :as => :container, :dependent => :destroy + + acts_as_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, :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 +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 diff --git a/groups/app/models/wiki_redirect.rb b/groups/app/models/wiki_redirect.rb new file mode 100644 index 000000000..adc2b24c1 --- /dev/null +++ b/groups/app/models/wiki_redirect.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/groups/app/models/workflow.rb b/groups/app/models/workflow.rb new file mode 100644 index 000000000..89322aa58 --- /dev/null +++ b/groups/app/models/workflow.rb @@ -0,0 +1,24 @@ +# 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 +end diff --git a/groups/app/views/account/login.rhtml b/groups/app/views/account/login.rhtml new file mode 100644 index 000000000..ea1a1cd44 --- /dev/null +++ b/groups/app/views/account/login.rhtml @@ -0,0 +1,33 @@ +
+<% form_tag({:action=> "login"}) do %> + + + + + + + + + + + + + + + + + +

<%= text_field_tag 'username', nil, :size => 40 %>

<%= password_field_tag 'password', nil, :size => 40 %>
+ <% if Setting.autologin? %> + + <% end %> +
+ <% if Setting.lost_password? %> + <%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %> + <% end %> + + +
+<%= javascript_tag "Form.Element.focus('username');" %> +<% end %> +
diff --git a/groups/app/views/account/lost_password.rhtml b/groups/app/views/account/lost_password.rhtml new file mode 100644 index 000000000..420e8f9b1 --- /dev/null +++ b/groups/app/views/account/lost_password.rhtml @@ -0,0 +1,11 @@ +

<%=l(:label_password_lost)%>

+ +
+<% form_tag({:action=> "lost_password"}, :class => "tabular") do %> + +

+<%= text_field_tag 'mail', nil, :size => 40 %> +<%= submit_tag l(:button_submit) %>

+ +<% end %> +
diff --git a/groups/app/views/account/password_recovery.rhtml b/groups/app/views/account/password_recovery.rhtml new file mode 100644 index 000000000..7fdd2b2fd --- /dev/null +++ b/groups/app/views/account/password_recovery.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_password_lost)%>

+ +<%= error_messages_for 'user' %> + +<% form_tag({:token => @token.value}) do %> +
+

+<%= password_field_tag 'new_password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+ +

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

+
+

<%= submit_tag l(:button_save) %>

+<% end %> diff --git a/groups/app/views/account/register.rhtml b/groups/app/views/account/register.rhtml new file mode 100644 index 000000000..7cf4b6da3 --- /dev/null +++ b/groups/app/views/account/register.rhtml @@ -0,0 +1,37 @@ +

<%=l(:label_register)%>

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

+<%= text_field 'user', 'login', :size => 25 %>

+ +

+<%= password_field_tag 'password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+ +

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

+ +

+<%= text_field 'user', 'firstname' %>

+ +

+<%= text_field 'user', 'lastname' %>

+ +

+<%= text_field 'user', 'mail' %>

+ +

+<%= select("user", "language", lang_options_for_select) %>

+ +<% for @custom_value in @custom_values %> +

<%= custom_field_tag_with_label @custom_value %>

+<% end %> + +
+ +<%= submit_tag l(:button_submit) %> +<% end %> diff --git a/groups/app/views/account/show.rhtml b/groups/app/views/account/show.rhtml new file mode 100644 index 000000000..97212b377 --- /dev/null +++ b/groups/app/views/account/show.rhtml @@ -0,0 +1,28 @@ +

<%=h @user.name %>

+ +

+<%= mail_to @user.mail unless @user.pref.hide_mail %> +

    +
  • <%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %>
  • +<% for custom_value in @custom_values %> +<% if !custom_value.value.empty? %> +
  • <%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %>
  • +<% end %> +<% end %> +
+

+ +<% unless @memberships.empty? %> +

<%=l(:label_project_plural)%>

+
    +<% for membership in @memberships %> +
  • <%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %> + (<%= membership.role.name %>, <%= format_date(membership.created_on) %>)
  • +<% end %> +
+<% end %> + +

<%=l(:label_activity)%>

+

+<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %> +

\ No newline at end of file diff --git a/groups/app/views/admin/_menu.rhtml b/groups/app/views/admin/_menu.rhtml new file mode 100644 index 000000000..ef2abbc56 --- /dev/null +++ b/groups/app/views/admin/_menu.rhtml @@ -0,0 +1,25 @@ + + + + diff --git a/groups/app/views/admin/_no_data.rhtml b/groups/app/views/admin/_no_data.rhtml new file mode 100644 index 000000000..5d52dc059 --- /dev/null +++ b/groups/app/views/admin/_no_data.rhtml @@ -0,0 +1,8 @@ +
+<% form_tag({:action => 'default_configuration'}) do %> + <%= simple_format(l(:text_no_configuration_data)) %> +

<%= 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) %>

+<% end %> +
diff --git a/groups/app/views/admin/index.rhtml b/groups/app/views/admin/index.rhtml new file mode 100644 index 000000000..18bee34cb --- /dev/null +++ b/groups/app/views/admin/index.rhtml @@ -0,0 +1,41 @@ +

<%=l(:label_administration)%>

+ +<%= render :partial => 'no_data' if @no_configuration_data %> + +

+<%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> | +<%= link_to l(:label_new), :controller => 'projects', :action => 'add' %> +

+ +

+<%= link_to l(:label_user_plural), :controller => 'users' %> | +<%= link_to l(:label_new), :controller => 'users', :action => 'add' %> +

+ +

+<%= link_to l(:label_role_and_permissions), :controller => 'roles' %> +

+ +

+<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> | +<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> | +<%= link_to l(:label_workflow), :controller => 'roles', :action => 'workflow' %> +

+ +

+<%= link_to l(:label_custom_field_plural), :controller => 'custom_fields' %> +

+ +

+<%= link_to l(:label_enumerations), :controller => 'enumerations' %> +

+ +

+<%= link_to l(:label_settings), :controller => 'settings' %> +

+ +

+<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %> +

+ +<% html_title(l(:label_administration)) -%> diff --git a/groups/app/views/admin/info.rhtml b/groups/app/views/admin/info.rhtml new file mode 100644 index 000000000..05c27f5ac --- /dev/null +++ b/groups/app/views/admin/info.rhtml @@ -0,0 +1,27 @@ +

<%=l(:label_information_plural)%>

+ +

<%= Redmine::Info.versioned_name %> (<%= @db_adapter_name %>)

+ + + + + +
<%= l(:text_default_administrator_account_changed) %><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
<%= l(:text_file_repository_writable) %><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
<%= l(:text_rmagick_available) %><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
+ +<% if @plugins.any? %> +  +

<%= l(:label_plugins) %>

+ + <% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %> + + + + + + + + <% end %> +
<%=h @plugins[plugin].name %><%=h @plugins[plugin].description %><%=h @plugins[plugin].author %><%=h @plugins[plugin].version %><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %>
+<% end %> + +<% html_title(l(:label_information_plural)) -%> diff --git a/groups/app/views/admin/projects.rhtml b/groups/app/views/admin/projects.rhtml new file mode 100644 index 000000000..c42845622 --- /dev/null +++ b/groups/app/views/admin/projects.rhtml @@ -0,0 +1,50 @@ +
+<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_project_plural)%>

+ +<% form_tag() do %> +
<%= l(:label_filter_plural) %> + +<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> +<%= submit_tag l(:button_apply), :class => "small" %> +
+<% end %> +  + + + + <%= sort_header_tag('name', :caption => l(:label_project)) %> + + + <%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + + + + +<% for project in @projects %> + "> + + + +<% end %> + +
<%=l(:field_description)%><%=l(:label_subproject_plural)%>
<%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> + <%= textilizable project.short_description, :project => project %> + <%= project.children.size %> + <%= image_tag 'true.png' if project.is_public? %> + <%= format_date(project.created_on) %> + + + <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :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 }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %> + + + <%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %> +
+ +

<%= pagination_links_full @project_pages, @project_count %>

+ +<% html_title(l(:label_project_plural)) -%> diff --git a/groups/app/views/attachments/_form.rhtml b/groups/app/views/attachments/_form.rhtml new file mode 100644 index 000000000..c98528b85 --- /dev/null +++ b/groups/app/views/attachments/_form.rhtml @@ -0,0 +1,9 @@ + +<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%> +<%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %> +<%= l(:label_optional_description) %> + +
+<%= 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) %>) + diff --git a/groups/app/views/attachments/_links.rhtml b/groups/app/views/attachments/_links.rhtml new file mode 100644 index 000000000..4d485548b --- /dev/null +++ b/groups/app/views/attachments/_links.rhtml @@ -0,0 +1,18 @@ +
+<% for attachment in attachments %> +

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

+<% end %> +
diff --git a/groups/app/views/auth_sources/_form.rhtml b/groups/app/views/auth_sources/_form.rhtml new file mode 100644 index 000000000..3d148c11f --- /dev/null +++ b/groups/app/views/auth_sources/_form.rhtml @@ -0,0 +1,48 @@ +<%= error_messages_for 'auth_source' %> + +
+ +

+<%= text_field 'auth_source', 'name' %>

+ +

+<%= text_field 'auth_source', 'host' %>

+ +

+<%= text_field 'auth_source', 'port', :size => 6 %> <%= check_box 'auth_source', 'tls' %> LDAPS

+ +

+<%= text_field 'auth_source', 'account' %>

+ +

+<%= 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]';" %>

+ +

+<%= text_field 'auth_source', 'base_dn', :size => 60 %>

+
+ +
+

+<%= check_box 'auth_source', 'onthefly_register' %>

+ +

+

<%=l(:label_attribute_plural)%> +

+<%= text_field 'auth_source', 'attr_login', :size => 20 %>

+ +

+<%= text_field 'auth_source', 'attr_firstname', :size => 20 %>

+ +

+<%= text_field 'auth_source', 'attr_lastname', :size => 20 %>

+ +

+<%= text_field 'auth_source', 'attr_mail', :size => 20 %>

+
+

+
+ + diff --git a/groups/app/views/auth_sources/edit.rhtml b/groups/app/views/auth_sources/edit.rhtml new file mode 100644 index 000000000..165fd4f3e --- /dev/null +++ b/groups/app/views/auth_sources/edit.rhtml @@ -0,0 +1,7 @@ +

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

+ +<% form_tag({:action => 'update', :id => @auth_source}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + diff --git a/groups/app/views/auth_sources/list.rhtml b/groups/app/views/auth_sources/list.rhtml new file mode 100644 index 000000000..6836e6c67 --- /dev/null +++ b/groups/app/views/auth_sources/list.rhtml @@ -0,0 +1,28 @@ +
+<%= link_to l(:label_auth_source_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_auth_source_plural)%>

+ + + + + + + + + + +<% for source in @auth_sources %> + "> + + + + + + +<% end %> + +
<%=l(:field_name)%><%=l(:field_type)%><%=l(:field_host)%>
<%= link_to source.name, :action => 'edit', :id => source%><%= source.auth_method_name %><%= source.host %><%= link_to l(:button_test), :action => 'test_connection', :id => source %><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, :confirm => l(:text_are_you_sure), :class => "button-small" %>
+ +

<%= pagination_links_full @auth_source_pages %>

diff --git a/groups/app/views/auth_sources/new.rhtml b/groups/app/views/auth_sources/new.rhtml new file mode 100644 index 000000000..2d493dc3a --- /dev/null +++ b/groups/app/views/auth_sources/new.rhtml @@ -0,0 +1,6 @@ +

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

+ +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/groups/app/views/boards/_form.rhtml b/groups/app/views/boards/_form.rhtml new file mode 100644 index 000000000..7ede589ab --- /dev/null +++ b/groups/app/views/boards/_form.rhtml @@ -0,0 +1,8 @@ +<%= error_messages_for 'board' %> + + +
+

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

+

<%= f.text_field :description, :required => true, :size => 80 %>

+
+ diff --git a/groups/app/views/boards/edit.rhtml b/groups/app/views/boards/edit.rhtml new file mode 100644 index 000000000..ba4c8b5ac --- /dev/null +++ b/groups/app/views/boards/edit.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_board) %>

+ +<% 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 %> diff --git a/groups/app/views/boards/index.rhtml b/groups/app/views/boards/index.rhtml new file mode 100644 index 000000000..8d4560653 --- /dev/null +++ b/groups/app/views/boards/index.rhtml @@ -0,0 +1,40 @@ +

<%= l(:label_board_plural) %>

+ + + + + + + + + +<% for board in @boards %> + + + + + + +<% end %> + +
<%= l(:label_board) %><%= l(:label_topic_plural) %><%= l(:label_message_plural) %><%= l(:label_message_last) %>
+ <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "icon22 icon22-comment" %>
+ <%=h board.description %> +
<%= board.topics_count %><%= board.messages_count %> + + <% if board.last_message %> + <%= authoring board.last_message.created_on, board.last_message.author %>
+ <%= link_to_message board.last_message %> + <% end %> +
+
+ +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}, + :class => 'feed' %> +

+ +<% 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 %> diff --git a/groups/app/views/boards/new.rhtml b/groups/app/views/boards/new.rhtml new file mode 100644 index 000000000..b89121880 --- /dev/null +++ b/groups/app/views/boards/new.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_board_new) %>

+ +<% labelled_tabular_form_for :board, @board, :url => {:action => 'new'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/groups/app/views/boards/show.rhtml b/groups/app/views/boards/show.rhtml new file mode 100644 index 000000000..26d17ae56 --- /dev/null +++ b/groups/app/views/boards/show.rhtml @@ -0,0 +1,59 @@ +<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %> + +
+<%= link_to_if_authorized l(:label_message_new), + {:controller => 'messages', :action => 'new', :board_id => @board}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-message"); return false;' %> +<%= watcher_tag(@board, User.current) %> +
+ + + +

<%=h @board.name %>

+ +<% if @topics.any? %> + + + + + <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %> + + <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %> + + + <% @topics.each do |topic| %> + + + + + + + + <% end %> + +
<%= l(:field_subject) %><%= l(:field_author) %><%= l(:label_reply_plural) %>
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %><%= topic.author %><%= format_time(topic.created_on) %><%= topic.replies_count %> + <% if topic.last_reply %> + <%= authoring topic.last_reply.created_on, topic.last_reply.author %>
+ <%= link_to_message topic.last_reply %> + <% end %> +
+

<%= pagination_links_full @topic_pages, @topic_count %>

+<% else %> +

<%= l(:label_no_data) %>

+<% end %> diff --git a/groups/app/views/common/403.rhtml b/groups/app/views/common/403.rhtml new file mode 100644 index 000000000..d1173a186 --- /dev/null +++ b/groups/app/views/common/403.rhtml @@ -0,0 +1,6 @@ +

403

+ +

<%= l(:notice_not_authorized) %>

+

Back

+ +<% html_title '403' %> diff --git a/groups/app/views/common/404.rhtml b/groups/app/views/common/404.rhtml new file mode 100644 index 000000000..753e716c6 --- /dev/null +++ b/groups/app/views/common/404.rhtml @@ -0,0 +1,6 @@ +

404

+ +

<%= l(:notice_file_not_found) %>

+

Back

+ +<% html_title '404' %> diff --git a/groups/app/views/common/_calendar.rhtml b/groups/app/views/common/_calendar.rhtml new file mode 100644 index 000000000..1095cd501 --- /dev/null +++ b/groups/app/views/common/_calendar.rhtml @@ -0,0 +1,39 @@ + + +<% 7.times do |i| %><% end %> + + + +<% day = calendar.startdt +while day <= calendar.enddt %> +<%= "" if day.cwday == calendar.first_wday %> + +<%= '' if day.cwday==calendar.last_wday and day!=calendar.enddt %> +<% day = day + 1 +end %> + + +
<%= day_name( (calendar.first_wday+i)%7 ) %>
#{day.cweek} +

<%= day.day %>

+<% calendar.events_on(day).each do |i| %> + <% if i.is_a? Issue %> +
+ <%= 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 %>: <%= h(truncate(i.subject, 30)) %> + <%= render_issue_tooltip i %> +
+ <% else %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i%> + + <% end %> +<% end %> +
diff --git a/groups/app/views/common/_preview.rhtml b/groups/app/views/common/_preview.rhtml new file mode 100644 index 000000000..e3bfc3a25 --- /dev/null +++ b/groups/app/views/common/_preview.rhtml @@ -0,0 +1,3 @@ +
<%= l(:label_preview) %> +<%= textilizable @text, :attachments => @attachements %> +
diff --git a/groups/app/views/common/feed.atom.rxml b/groups/app/views/common/feed.atom.rxml new file mode 100644 index 000000000..b5cbeeed9 --- /dev/null +++ b/groups/app/views/common/feed.atom.rxml @@ -0,0 +1,27 @@ +xml.instruct! +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + xml.title @title + xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false})) + xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) + 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, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; } + @items.each do |item| + xml.entry do + url = url_for(item.event_url(:only_path => false)) + xml.title truncate(item.event_title, 100) + xml.link "rel" => "alternate", "href" => url + xml.id url + xml.updated item.event_datetime.xmlschema + author = item.event_author if item.respond_to?(:author) + xml.author do + xml.name(author) + xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank? + end if author + xml.content "type" => "html" do + xml.text! textilizable(item.event_description) + end + end + end +end diff --git a/groups/app/views/custom_fields/_form.rhtml b/groups/app/views/custom_fields/_form.rhtml new file mode 100644 index 000000000..5e4eadf21 --- /dev/null +++ b/groups/app/views/custom_fields/_form.rhtml @@ -0,0 +1,110 @@ +<%= error_messages_for 'custom_field' %> + + + +
+

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

+

<%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %>

+

+ <%= f.text_field :min_length, :size => 5, :no_label => true %> - + <%= f.text_field :max_length, :size => 5, :no_label => true %>
(<%=l(:text_min_max_length_info)%>)

+

<%= f.text_field :regexp, :size => 50 %>
(<%=l(:text_regexp_info)%>)

+

+<% (@custom_field.possible_values.to_a + [""]).each do |value| %> +<%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %>
+<% end %> +

+

<%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %>

+
+ +
+<% case @custom_field.type.to_s +when "IssueCustomField" %> + +
<%=l(:label_tracker_plural)%> + <% for tracker in @trackers %> + <%= check_box_tag "tracker_ids[]", tracker.id, (@custom_field.trackers.include? tracker) %> <%= tracker.name %> + <% end %> +
+   +

<%= f.check_box :is_required %>

+

<%= f.check_box :is_for_all %>

+

<%= f.check_box :is_filter %>

+

<%= f.check_box :searchable %>

+ +<% when "UserCustomField" %> +

<%= f.check_box :is_required %>

+ +<% when "ProjectCustomField" %> +

<%= f.check_box :is_required %>

+ +<% end %> +
+<%= javascript_tag "toggle_custom_field_format();" %> diff --git a/groups/app/views/custom_fields/edit.rhtml b/groups/app/views/custom_fields/edit.rhtml new file mode 100644 index 000000000..ef056fa41 --- /dev/null +++ b/groups/app/views/custom_fields/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_custom_field)%> (<%=l(@custom_field.type_name)%>)

+ +<% 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 %> diff --git a/groups/app/views/custom_fields/list.rhtml b/groups/app/views/custom_fields/list.rhtml new file mode 100644 index 000000000..43ddd99c8 --- /dev/null +++ b/groups/app/views/custom_fields/list.rhtml @@ -0,0 +1,58 @@ +

<%=l(:label_custom_field_plural)%>

+ +<% selected_tab = params[:tab] ? params[:tab].to_s : custom_fields_tabs.first[:name] %> + +
+
    +<% custom_fields_tabs.each do |tab| -%> +
  • <%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %>
  • +<% end -%> +
+
+ +<% custom_fields_tabs.each do |tab| %> +
+ + + + + + <% if tab[:name] == 'IssueCustomField' %> + + + <% end %> + + + + +<% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> + "> + + + + <% if tab[:name] == 'IssueCustomField' %> + + + <% end %> + + + +<% end; reset_cycle %> + +
<%=l(:field_name)%><%=l(:field_field_format)%><%=l(:field_is_required)%><%=l(:field_is_for_all)%><%=l(:label_used_by)%><%=l(:button_sort)%>
<%= link_to custom_field.name, :action => 'edit', :id => custom_field %><%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %><%= image_tag 'true.png' if custom_field.is_required? %><%= image_tag 'true.png' if custom_field.is_for_all? %><%= custom_field.projects.count.to_s + ' ' + lwr(:label_project, custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => custom_field, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => custom_field, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => custom_field, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => custom_field, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + + <%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +

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

+
+<% end %> + +<% html_title(l(:label_custom_field_plural)) -%> diff --git a/groups/app/views/custom_fields/new.rhtml b/groups/app/views/custom_fields/new.rhtml new file mode 100644 index 000000000..2e8aa2750 --- /dev/null +++ b/groups/app/views/custom_fields/new.rhtml @@ -0,0 +1,7 @@ +

<%=l(:label_custom_field_new)%> (<%=l(@custom_field.type_name)%>)

+ +<% 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 %> diff --git a/groups/app/views/documents/_document.rhtml b/groups/app/views/documents/_document.rhtml new file mode 100644 index 000000000..ddfdb9eec --- /dev/null +++ b/groups/app/views/documents/_document.rhtml @@ -0,0 +1,3 @@ +

<%= link_to h(document.title), :controller => 'documents', :action => 'show', :id => document %>
+<% unless document.description.blank? %><%=h(truncate(document.description, 250)) %>
<% end %> +<%= format_time(document.created_on) %>

\ No newline at end of file diff --git a/groups/app/views/documents/_form.rhtml b/groups/app/views/documents/_form.rhtml new file mode 100644 index 000000000..d45e295b0 --- /dev/null +++ b/groups/app/views/documents/_form.rhtml @@ -0,0 +1,15 @@ +<%= error_messages_for 'document' %> +
+ +

+<%= select('document', 'category_id', Enumeration.get_values('DCAT').collect {|c| [c.name, c.id]}) %>

+ +

+<%= text_field 'document', 'title', :size => 60 %>

+ +

+<%= text_area 'document', 'description', :cols => 60, :rows => 15, :class => 'wiki-edit' %>

+ +
+ +<%= wikitoolbar_for 'document_description' %> diff --git a/groups/app/views/documents/edit.rhtml b/groups/app/views/documents/edit.rhtml new file mode 100644 index 000000000..0b9f31f84 --- /dev/null +++ b/groups/app/views/documents/edit.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_document)%>

+ +<% form_tag({:action => 'edit', :id => @document}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> + + diff --git a/groups/app/views/documents/index.rhtml b/groups/app/views/documents/index.rhtml new file mode 100644 index 000000000..14d997360 --- /dev/null +++ b/groups/app/views/documents/index.rhtml @@ -0,0 +1,39 @@ +
+<%= link_to_if_authorized l(:label_document_new), + {:controller => 'documents', :action => 'new', :project_id => @project}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-document"); return false;' %> +
+ + + +

<%=l(:label_document_plural)%>

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

<%= l(:label_no_data) %>

<% end %> + +<% @grouped.keys.sort.each do |group| %> +

<%= group %>

+ <%= render :partial => 'documents/document', :collection => @grouped[group] %> +<% end %> + +<% content_for :sidebar do %> +

<%= l(:label_sort_by, '') %>

+ <% form_tag({}, :method => :get) do %> +
+
+
+ + <% end %> +<% end %> + +<% html_title(l(:label_document_plural)) -%> diff --git a/groups/app/views/documents/new.rhtml b/groups/app/views/documents/new.rhtml new file mode 100644 index 000000000..639b4f292 --- /dev/null +++ b/groups/app/views/documents/new.rhtml @@ -0,0 +1,13 @@ +

<%=l(:label_document_new)%>

+ +<% form_tag({:controller => 'documents', :action => 'new', :project_id => @project}, :class => "tabular", :multipart => true) do %> +<%= render :partial => 'documents/form' %> + +
+

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

+
+ +<%= submit_tag l(:button_create) %> +<% end %> + + diff --git a/groups/app/views/documents/show.rhtml b/groups/app/views/documents/show.rhtml new file mode 100644 index 000000000..aa90c5518 --- /dev/null +++ b/groups/app/views/documents/show.rhtml @@ -0,0 +1,28 @@ +
+<%= 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' %> +
+ +

<%=h @document.title %>

+ +

<%=h @document.category.name %>
+<%= format_date @document.created_on %>

+
+<%= textilizable @document.description, :attachments => @document.attachments %> +
+ +

<%= l(:label_attachment_plural) %>

+<%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %> + +<% if authorize_for('documents', 'add_attachment') %> +

<%= 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' %>

+ <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> +
+

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

+
+ <%= submit_tag l(:button_add) %> + <% end %> +<% end %> + +<% html_title @document.title -%> diff --git a/groups/app/views/enumerations/_form.rhtml b/groups/app/views/enumerations/_form.rhtml new file mode 100644 index 000000000..3f98f5213 --- /dev/null +++ b/groups/app/views/enumerations/_form.rhtml @@ -0,0 +1,12 @@ +<%= error_messages_for 'enumeration' %> +
+ +<%= hidden_field 'enumeration', 'opt' %> + +

+<%= text_field 'enumeration', 'name' %>

+ +

+<%= check_box 'enumeration', 'is_default' %>

+ +
\ No newline at end of file diff --git a/groups/app/views/enumerations/edit.rhtml b/groups/app/views/enumerations/edit.rhtml new file mode 100644 index 000000000..7baea028a --- /dev/null +++ b/groups/app/views/enumerations/edit.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_enumerations)%>

+ +<% 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 diff --git a/groups/app/views/enumerations/list.rhtml b/groups/app/views/enumerations/list.rhtml new file mode 100644 index 000000000..9de9bf37c --- /dev/null +++ b/groups/app/views/enumerations/list.rhtml @@ -0,0 +1,28 @@ +

<%=l(:label_enumerations)%>

+ +<% Enumeration::OPTIONS.each do |option, name| %> +

<%= l(name) %>

+ +<% enumerations = Enumeration.get_values(option) %> +<% if enumerations.any? %> + +<% enumerations.each do |enumeration| %> + + + + + +<% end %> +
<%= link_to enumeration.name, :action => 'edit', :id => enumeration %><%= image_tag('true.png') if enumeration.is_default? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => enumeration, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> +
+<% reset_cycle %> +<% end %> + +

<%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %>

+<% end %> + +<% html_title(l(:label_enumerations)) -%> diff --git a/groups/app/views/enumerations/new.rhtml b/groups/app/views/enumerations/new.rhtml new file mode 100644 index 000000000..5c2ccd133 --- /dev/null +++ b/groups/app/views/enumerations/new.rhtml @@ -0,0 +1,6 @@ +

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

+ +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/groups/app/views/issue_categories/_form.rhtml b/groups/app/views/issue_categories/_form.rhtml new file mode 100644 index 000000000..dc62c2000 --- /dev/null +++ b/groups/app/views/issue_categories/_form.rhtml @@ -0,0 +1,6 @@ +<%= error_messages_for 'category' %> + +
+

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

+

<%= f.select :assigned_to_id, @project.users.collect{|u| [u.name, u.id]}, :include_blank => true %>

+
diff --git a/groups/app/views/issue_categories/destroy.rhtml b/groups/app/views/issue_categories/destroy.rhtml new file mode 100644 index 000000000..2b61810e7 --- /dev/null +++ b/groups/app/views/issue_categories/destroy.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_issue_category)%>: <%=h @category.name %>

+ +<% form_tag({}) do %> +
+

<%= l(:text_issue_category_destroy_question, @issue_count) %>

+


+<% if @categories.size > 0 %> +: +<%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %>

+<% end %> +
+ +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' %> +<% end %> diff --git a/groups/app/views/issue_categories/edit.rhtml b/groups/app/views/issue_categories/edit.rhtml new file mode 100644 index 000000000..bc627797b --- /dev/null +++ b/groups/app/views/issue_categories/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_category)%>

+ +<% 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 %> diff --git a/groups/app/views/issue_relations/_form.rhtml b/groups/app/views/issue_relations/_form.rhtml new file mode 100644 index 000000000..0de386306 --- /dev/null +++ b/groups/app/views/issue_relations/_form.rhtml @@ -0,0 +1,12 @@ +<%= error_messages_for 'relation' %> + +

<%= f.select :relation_type, collection_for_relation_type_select, {}, :onchange => "setPredecessorFieldsVisibility();" %> +<%= l(:label_issue) %> #<%= f.text_field :issue_to_id, :size => 6 %> + +<%= submit_tag l(:button_add) %> +<%= toggle_link l(:button_cancel), 'new-relation-form'%> +

+ +<%= javascript_tag "setPredecessorFieldsVisibility();" %> diff --git a/groups/app/views/issue_statuses/_form.rhtml b/groups/app/views/issue_statuses/_form.rhtml new file mode 100644 index 000000000..6ae0a7c33 --- /dev/null +++ b/groups/app/views/issue_statuses/_form.rhtml @@ -0,0 +1,15 @@ +<%= error_messages_for 'issue_status' %> + +
+ +

+<%= text_field 'issue_status', 'name' %>

+ +

+<%= check_box 'issue_status', 'is_closed' %>

+ +

+<%= check_box 'issue_status', 'is_default' %>

+ + +
\ No newline at end of file diff --git a/groups/app/views/issue_statuses/edit.rhtml b/groups/app/views/issue_statuses/edit.rhtml new file mode 100644 index 000000000..b81426a02 --- /dev/null +++ b/groups/app/views/issue_statuses/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_status)%>

+ +<% form_tag({:action => 'update', :id => @issue_status}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/issue_statuses/list.rhtml b/groups/app/views/issue_statuses/list.rhtml new file mode 100644 index 000000000..e35911813 --- /dev/null +++ b/groups/app/views/issue_statuses/list.rhtml @@ -0,0 +1,37 @@ +
+<%= link_to l(:label_issue_status_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_issue_status_plural)%>

+ + + + + + + + + + +<% for status in @issue_statuses %> + "> + + + + + + +<% end %> + +
<%=l(:field_status)%><%=l(:field_is_default)%><%=l(:field_is_closed)%><%=l(:button_sort)%>
<%= link_to status.name, :action => 'edit', :id => status %><%= image_tag 'true.png' if status.is_default? %><%= image_tag 'true.png' if status.is_closed? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => status, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => status, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => status, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => status, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + + <%= button_to l(:button_delete), { :action => 'destroy', :id => status }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +

<%= pagination_links_full @issue_status_pages %>

+ +<% html_title(l(:label_issue_status_plural)) -%> diff --git a/groups/app/views/issue_statuses/new.rhtml b/groups/app/views/issue_statuses/new.rhtml new file mode 100644 index 000000000..ede1699b0 --- /dev/null +++ b/groups/app/views/issue_statuses/new.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_status_new)%>

+ +<% form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/groups/app/views/issues/_changesets.rhtml b/groups/app/views/issues/_changesets.rhtml new file mode 100644 index 000000000..caa983cbf --- /dev/null +++ b/groups/app/views/issues/_changesets.rhtml @@ -0,0 +1,8 @@ +<% changesets.each do |changeset| %> +
+

<%= link_to("#{l(:label_revision)} #{changeset.revision}", + :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %>
+ <%= authoring(changeset.committed_on, changeset.committer) %>

+ <%= textilizable(changeset, :comments) %> +
+<% end %> diff --git a/groups/app/views/issues/_edit.rhtml b/groups/app/views/issues/_edit.rhtml new file mode 100644 index 000000000..2e00ab520 --- /dev/null +++ b/groups/app/views/issues/_edit.rhtml @@ -0,0 +1,50 @@ +<% 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' %> +
+ <% if @edit_allowed || !@allowed_statuses.empty? %> +
<%= l(:label_change_properties) %> + <% if !@issue.new_record? && !@issue.errors.any? && @edit_allowed %> + (<%= link_to l(:label_more), {}, :onclick => 'Effect.toggle("issue_descr_fields", "appear", {duration:0.3}); return false;' %>) + <% end %> + + <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %> +
+ <% end %> + <% if authorize_for('timelog', 'edit') %> +
<%= l(:button_log_time) %> + <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> +
+

<%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %>

+
+
+

<%= time_entry.text_field :comments, :size => 40 %>

+

<%= time_entry.select :activity_id, (@activities.collect {|p| [p.name, p.id]}) %>

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

<%=l(:label_attachment_plural)%>
<%= render :partial => 'attachments/form' %>

+
+
+ + <%= 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 %> + +
diff --git a/groups/app/views/issues/_form.rhtml b/groups/app/views/issues/_form.rhtml new file mode 100644 index 000000000..9bb74fd34 --- /dev/null +++ b/groups/app/views/issues/_form.rhtml @@ -0,0 +1,51 @@ +<% if @issue.new_record? %> +

<%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %>

+<%= observe_field :issue_tracker_id, :url => { :action => :new }, + :update => :content, + :with => "Form.serialize('issue-form')" %> +
+<% end %> + +
> +

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

+

<%= f.text_area :description, :required => true, + :cols => 60, + :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), + :accesskey => accesskey(:edit), + :class => 'wiki-edit' %>

+
+ +
+<% if @issue.new_record? || @allowed_statuses.any? %> +

<%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %>

+<% else %> +

<%= @issue.status.name %>

+<% end %> + +

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

+

<%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %>

+

<%= 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') %>

+<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %> +
+ +
+

<%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %>

+

<%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %>

+

<%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %>

+

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

+
+ +
+<%= render :partial => 'form_custom_fields', :locals => {:values => @custom_values} %> + +<% if @issue.new_record? %> +

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

+<% end %> + +<%= wikitoolbar_for 'issue_description' %> diff --git a/groups/app/views/issues/_form_custom_fields.rhtml b/groups/app/views/issues/_form_custom_fields.rhtml new file mode 100644 index 000000000..1268bb1f9 --- /dev/null +++ b/groups/app/views/issues/_form_custom_fields.rhtml @@ -0,0 +1,11 @@ +
+<% i = 1 %> +<% for @custom_value in values %> +

<%= custom_field_tag_with_label @custom_value %>

+ <% if i == values.size / 2 %> +
+ <% end %> + <% i += 1 %> +<% end %> +
+
diff --git a/groups/app/views/issues/_form_update.rhtml b/groups/app/views/issues/_form_update.rhtml new file mode 100644 index 000000000..25e81a7fd --- /dev/null +++ b/groups/app/views/issues/_form_update.rhtml @@ -0,0 +1,10 @@ +
+

<%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %>

+

<%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %>

+
+
+

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

+<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %> +
diff --git a/groups/app/views/issues/_history.rhtml b/groups/app/views/issues/_history.rhtml new file mode 100644 index 000000000..f29a44daf --- /dev/null +++ b/groups/app/views/issues/_history.rhtml @@ -0,0 +1,13 @@ +<% for journal in journals %> +
+

<%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %>
+ <%= content_tag('a', '', :name => "note-#{journal.indice}")%> + <%= format_time(journal.created_on) %> - <%= journal.user.name %>

+
    + <% for detail in journal.details %> +
  • <%= show_detail(detail) %>
  • + <% end %> +
+ <%= render_notes(journal) unless journal.notes.blank? %> +
+<% end %> diff --git a/groups/app/views/issues/_list.rhtml b/groups/app/views/issues/_list.rhtml new file mode 100644 index 000000000..000f79853 --- /dev/null +++ b/groups/app/views/issues/_list.rhtml @@ -0,0 +1,22 @@ +<% form_tag({}) do -%> + + + + <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> + <% query.columns.each do |column| %> + <%= column_header(column) %> + <% end %> + + + <% issues.each do |issue| -%> + "> + + + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> + + <% end -%> + +
<%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> +
<%= check_box_tag("ids[]", issue.id, false, :id => nil) %><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
+<% end -%> diff --git a/groups/app/views/issues/_list_simple.rhtml b/groups/app/views/issues/_list_simple.rhtml new file mode 100644 index 000000000..8900b7359 --- /dev/null +++ b/groups/app/views/issues/_list_simple.rhtml @@ -0,0 +1,28 @@ +<% if issues && issues.any? %> +<% form_tag({}) do %> + + + + + + + + <% for issue in issues %> + "> + + + + + <% end %> + +
#<%=l(:field_tracker)%><%=l(:field_subject)%>
+ <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> + <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> + <%=h issue.project.name %> - <%= issue.tracker.name %>
+ <%= issue.status.name %> - <%= format_time(issue.updated_on) %>
+ <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> +
+<% end %> +<% else %> +

<%= l(:label_no_data) %>

+<% end %> diff --git a/groups/app/views/issues/_pdf.rfpdf b/groups/app/views/issues/_pdf.rfpdf new file mode 100644 index 000000000..6830506f6 --- /dev/null +++ b/groups/app/views/issues/_pdf.rfpdf @@ -0,0 +1,118 @@ +<% pdf.SetFontStyle('B',11) + pdf.Cell(190,10, "#{issue.project.name} - #{issue.tracker.name} # #{issue.id}: #{issue.subject}") + pdf.Ln + + y0 = pdf.GetY + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_status) + ":","LT") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, issue.status.name,"RT") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_priority) + ":","LT") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, issue.priority.name,"RT") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_author) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, issue.author.name,"R") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_category) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, (issue.category ? issue.category.name : "-"),"R") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_created_on) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(issue.created_on),"R") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_assigned_to) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, (issue.assigned_to ? issue.assigned_to.name : "-"),"R") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_updated_on) + ":","LB") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(issue.updated_on),"RB") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_due_date) + ":","LB") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(issue.due_date),"RB") + pdf.Ln + + for custom_value in issue.custom_values + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, custom_value.custom_field.name + ":","L") + pdf.SetFontStyle('',9) + pdf.MultiCell(155,5, (show_value custom_value),"R") + end + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_subject) + ":","LTB") + pdf.SetFontStyle('',9) + pdf.Cell(155,5, issue.subject,"RTB") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_description) + ":") + pdf.SetFontStyle('',9) + pdf.MultiCell(155,5, issue.description,"BR") + + pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY) + pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY) + + pdf.Ln + + if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project) + pdf.SetFontStyle('B',9) + pdf.Cell(190,5, l(:label_associated_revisions), "B") + pdf.Ln + for changeset in @issue.changesets + pdf.SetFontStyle('B',8) + pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.committer) + 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 +%> diff --git a/groups/app/views/issues/_relations.rhtml b/groups/app/views/issues/_relations.rhtml new file mode 100644 index 000000000..d4b3e5aa6 --- /dev/null +++ b/groups/app/views/issues/_relations.rhtml @@ -0,0 +1,31 @@ +
+<% if authorize_for('issue_relations', 'new') %> + <%= toggle_link l(:button_add), 'new-relation-form'%> +<% end %> +
+ +

<%=l(:label_related_issues)%>

+ +<% if @issue.relations.any? %> + +<% @issue.relations.each do |relation| %> + + + + + + + + +<% end %> +
<%= l(relation.label_for(@issue)) %> <%= "(#{lwr(:actionview_datehelper_time_in_words_day, relation.delay)})" if relation.delay && relation.delay != 0 %> <%= link_to_issue relation.other_issue(@issue) %><%=h relation.other_issue(@issue).subject %><%= relation.other_issue(@issue).status.name %><%= format_date(relation.other_issue(@issue).start_date) %><%= format_date(relation.other_issue(@issue).due_date) %><%= 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') %>
+<% 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 %> diff --git a/groups/app/views/issues/_sidebar.rhtml b/groups/app/views/issues/_sidebar.rhtml new file mode 100644 index 000000000..e94d4180b --- /dev/null +++ b/groups/app/views/issues/_sidebar.rhtml @@ -0,0 +1,14 @@ +

<%= l(:label_issue_plural) %>

+<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %>
+<% if @project %> +<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %>
+<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %> +<% end %> + +<% unless sidebar_queries.empty? -%> +

<%= l(:label_query_plural) %>

+ +<% sidebar_queries.each do |query| -%> +<%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %>
+<% end -%> +<% end -%> diff --git a/groups/app/views/issues/bulk_edit.rhtml b/groups/app/views/issues/bulk_edit.rhtml new file mode 100644 index 000000000..86bc76765 --- /dev/null +++ b/groups/app/views/issues/bulk_edit.rhtml @@ -0,0 +1,49 @@ +

<%= l(:label_bulk_edit_selected_issues) %>

+ +
    <%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %>
+ +<% form_tag() do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> +
+
+<%= l(:label_change_properties) %> +

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

+

+ + +

+ +

+ + + +

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

<%= submit_tag l(:button_submit) %> +<% end %> diff --git a/groups/app/views/issues/changes.rxml b/groups/app/views/issues/changes.rxml new file mode 100644 index 000000000..239d2d6a3 --- /dev/null +++ b/groups/app/views/issues/changes.rxml @@ -0,0 +1,30 @@ +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) + end + xml.content "type" => "html" do + xml.text! '

    ' + change.details.each do |detail| + xml.text! '
  • ' + show_detail(detail, false) + '
  • ' + end + xml.text! '
' + xml.text! textilizable(change.notes) unless change.notes.blank? + end + end + end +end \ No newline at end of file diff --git a/groups/app/views/issues/context_menu.rhtml b/groups/app/views/issues/context_menu.rhtml new file mode 100644 index 000000000..f42f254e8 --- /dev/null +++ b/groups/app/views/issues/context_menu.rhtml @@ -0,0 +1,54 @@ +
    +<% if !@issue.nil? -%> +
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
  • +
  • + <%= l(:field_status) %> +
      + <% @statuses.each do |s| -%> +
    • <%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, + :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %>
    • + <% end -%> +
    +
  • +
  • + <%= l(:field_priority) %> +
      + <% @priorities.each do |p| -%> +
    • <%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post, + :selected => (p == @issue.priority), :disabled => !@can[:edit] %>
    • + <% end -%> +
    +
  • +
  • + <%= l(:field_assigned_to) %> +
      + <% @assignables.each do |u| -%> +
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post, + :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %>
    • + <% end -%> +
    • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post, + :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %>
    • +
    +
  • +
  • + <%= l(:field_done_ratio) %> +
      + <% (0..10).map{|x|x*10}.each do |p| -%> +
    • <%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[done_ratio]' => p, :back_to => @back}, :method => :post, + :selected => (p == @issue.done_ratio), :disabled => !@can[:edit] %>
    • + <% end -%> +
    +
  • +
  • <%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, + :class => 'icon-copy', :disabled => !@can[:copy] %>
  • +<% else -%> +
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
  • +<% end -%> + +
  • <%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, + :class => 'icon-move', :disabled => !@can[:move] %>
  • +
  • <%= 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] %>
  • +
diff --git a/groups/app/views/issues/destroy.rhtml b/groups/app/views/issues/destroy.rhtml new file mode 100644 index 000000000..2f3c036b6 --- /dev/null +++ b/groups/app/views/issues/destroy.rhtml @@ -0,0 +1,15 @@ +

<%= l(:confirmation) %>

+ +<% form_tag do %> +<%= @issues.collect {|i| hidden_field_tag 'ids[]', i.id } %> +
+

<%= l(:text_destroy_time_entries_question, @hours) %>

+

+
+
+ +<%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %> +

+
+<%= submit_tag l(:button_apply) %> +<% end %> diff --git a/groups/app/views/issues/edit.rhtml b/groups/app/views/issues/edit.rhtml new file mode 100644 index 000000000..97f26a205 --- /dev/null +++ b/groups/app/views/issues/edit.rhtml @@ -0,0 +1,3 @@ +

<%=h "#{@issue.tracker.name} ##{@issue.id}" %>

+ +<%= render :partial => 'edit' %> diff --git a/groups/app/views/issues/index.rfpdf b/groups/app/views/issues/index.rfpdf new file mode 100644 index 000000000..d5a8d3c31 --- /dev/null +++ b/groups/app/views/issues/index.rfpdf @@ -0,0 +1,50 @@ +<% pdf=IfpdfHelper::IFPDF.new(current_language) + title = @project ? "#{@project.name} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}" + pdf.SetTitle(title) + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + pdf.AddPage("L") + row_height = 7 + + # + # title + # + pdf.SetFontStyle('B',11) + pdf.Cell(190,10, title) + pdf.Ln + + # + # headers + # + pdf.SetFontStyle('B',10) + pdf.SetFillColor(230, 230, 230) + pdf.Cell(15, row_height, "#", 0, 0, 'L', 1) + pdf.Cell(30, row_height, l(:field_tracker), 0, 0, 'L', 1) + pdf.Cell(30, row_height, l(:field_status), 0, 0, 'L', 1) + pdf.Cell(30, row_height, l(:field_priority), 0, 0, 'L', 1) + pdf.Cell(40, row_height, l(:field_assigned_to), 0, 0, 'L', 1) + pdf.Cell(25, row_height, l(:field_updated_on), 0, 0, 'L', 1) + pdf.Cell(0, row_height, l(:field_subject), 0, 0, 'L', 1) + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.Ln + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.SetY(pdf.GetY() + 1) + + # + # rows + # + pdf.SetFontStyle('',9) + pdf.SetFillColor(255, 255, 255) + @issues.each do |issue| + pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1) + pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1) + pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1) + pdf.Cell(30, row_height, issue.priority.name, 0, 0, 'L', 1) + pdf.Cell(40, row_height, issue.assigned_to ? issue.assigned_to.name : '', 0, 0, 'L', 1) + pdf.Cell(25, row_height, format_date(issue.updated_on), 0, 0, 'L', 1) + pdf.MultiCell(0, row_height, (@project == issue.project ? issue.subject : "#{issue.project.name} - #{issue.subject}")) + pdf.Line(10, pdf.GetY, 287, pdf.GetY) + pdf.SetY(pdf.GetY() + 1) + end +%> +<%= pdf.Output %> \ No newline at end of file diff --git a/groups/app/views/issues/index.rhtml b/groups/app/views/issues/index.rhtml new file mode 100644 index 000000000..027f3f006 --- /dev/null +++ b/groups/app/views/issues/index.rhtml @@ -0,0 +1,67 @@ +<% if @query.new_record? %> +

<%=l(:label_issue_plural)%>

+ <% html_title(l(:label_issue_plural)) %> + + <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> + <%= hidden_field_tag('project_id', @project.id) if @project %> +
<%= l(:label_filter_plural) %> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +

+ <%= 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 }, + :update => "content", + }, :class => 'icon icon-reload' %> + + <% if 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 %> +

+
+ <% end %> +<% else %> +
+ <% 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' %> + <% end %> +
+

<%=h @query.name %>

+
+ <% html_title @query.name %> +<% end %> +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<% if @issues.empty? %> +

<%= l(:label_no_data) %>

+<% else %> +<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %> +

<%= pagination_links_full @issue_pages, @issue_count %>

+ +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +<%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %> +<%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %> +

+<% 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 %> + + +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> diff --git a/groups/app/views/issues/move.rhtml b/groups/app/views/issues/move.rhtml new file mode 100644 index 000000000..35761e160 --- /dev/null +++ b/groups/app/views/issues/move.rhtml @@ -0,0 +1,22 @@ +

<%= l(:button_move) %>

+ +
    <%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %>
+ +<% form_tag({}, :id => 'move_form') do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> + +
+

+<%= select_tag "new_project_id", + options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id), + :onchange => remote_function(:url => { :action => 'move' }, + :method => :get, + :update => 'content', + :with => "Form.serialize('move_form')") %>

+ +

+<%= select_tag "new_tracker_id", "" + options_from_collection_for_select(@trackers, "id", "name") %>

+
+ +<%= submit_tag l(:button_move) %> +<% end %> diff --git a/groups/app/views/issues/new.rhtml b/groups/app/views/issues/new.rhtml new file mode 100644 index 000000000..280e2009b --- /dev/null +++ b/groups/app/views/issues/new.rhtml @@ -0,0 +1,19 @@ +

<%=l(:label_issue_new)%>

+ +<% labelled_tabular_form_for :issue, @issue, + :html => {:multipart => true, :id => 'issue-form'} do |f| %> + <%= error_messages_for 'issue' %> +
+ <%= render :partial => 'issues/form', :locals => {:f => f} %> +
+ <%= submit_tag l(:button_create) %> + <%= 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) %> +<% end %> + +
diff --git a/groups/app/views/issues/show.rfpdf b/groups/app/views/issues/show.rfpdf new file mode 100644 index 000000000..08f2cb92d --- /dev/null +++ b/groups/app/views/issues/show.rfpdf @@ -0,0 +1,10 @@ +<% pdf=IfpdfHelper::IFPDF.new(current_language) + pdf.SetTitle("#{@project.name} - ##{@issue.tracker.name} #{@issue.id}") + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + + render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => @issue } +%> + +<%= pdf.Output %> diff --git a/groups/app/views/issues/show.rhtml b/groups/app/views/issues/show.rhtml new file mode 100644 index 000000000..f788d0ec8 --- /dev/null +++ b/groups/app/views/issues/show.rhtml @@ -0,0 +1,113 @@ +
+<%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> +<%= 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' %> +
+ +

<%= @issue.tracker.name %> #<%= @issue.id %>

+ +
"> +

<%=h @issue.subject %>

+

+ <%= authoring @issue.created_on, @issue.author %>. + <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %> +

+ + + + + + + + + + + + + + + + + <% if User.current.allowed_to?(:view_time_entries, @project) %> + + + <% end %> + + + + <% if @issue.estimated_hours %> + + <% end %> + + +<% n = 0 +for custom_value in @custom_values %> + +<% n = n + 1 + if (n > 1) + n = 0 %> + + <%end +end %> + +
<%=l(:field_status)%> :<%= @issue.status.name %><%=l(:field_start_date)%> :<%= format_date(@issue.start_date) %>
<%=l(:field_priority)%> :<%= @issue.priority.name %><%=l(:field_due_date)%> :<%= format_date(@issue.due_date) %>
<%=l(:field_assigned_to)%> :<%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %><%=l(:field_done_ratio)%> :<%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %>
<%=l(:field_category)%> :<%=h @issue.category ? @issue.category.name : "-" %><%=l(:label_spent_time)%> :<%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %>
<%=l(:field_fixed_version)%> :<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %><%=l(:field_estimated_hours)%> :<%= lwr(:label_f_hour, @issue.estimated_hours) %>
<%= custom_value.custom_field.name %> :<%= simple_format(h(show_value(custom_value))) %>
+
+ +

<%=l(:field_description)%>

+
+<%= textilizable @issue, :description, :attachments => @issue.attachments %> +
+ +<% if @issue.attachments.any? %> +<%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %> +<% end %> + +<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %> +
+
+<%= render :partial => 'relations' %> +
+<% end %> + +
+ +<% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %> +
+

<%=l(:label_associated_revisions)%>

+<%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %> +
+<% end %> + +<% if @journals.any? %> +
+

<%=l(:label_history)%>

+<%= render :partial => 'history', :locals => { :journals => @journals } %> +
+<% end %> +
+ +<% if authorize_for('issues', 'edit') %> + +<% end %> + +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +<%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %> +

+ +<% 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}") %> +<% end %> diff --git a/groups/app/views/journals/_notes_form.rhtml b/groups/app/views/journals/_notes_form.rhtml new file mode 100644 index 000000000..9baec03fa --- /dev/null +++ b/groups/app/views/journals/_notes_form.rhtml @@ -0,0 +1,7 @@ +<% 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) %> +

<%= 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;" %>

+<% end %> diff --git a/groups/app/views/journals/edit.rjs b/groups/app/views/journals/edit.rjs new file mode 100644 index 000000000..798cb0f04 --- /dev/null +++ b/groups/app/views/journals/edit.rjs @@ -0,0 +1,3 @@ +page.hide "journal-#{@journal.id}-notes" +page.insert_html :after, "journal-#{@journal.id}-notes", + :partial => 'notes_form' diff --git a/groups/app/views/journals/update.rjs b/groups/app/views/journals/update.rjs new file mode 100644 index 000000000..2b5a54c0a --- /dev/null +++ b/groups/app/views/journals/update.rjs @@ -0,0 +1,8 @@ +if @journal.frozen? + # journal was destroyed + page.remove "change-#{@journal.id}" +else + page.replace "journal-#{@journal.id}-notes", render_notes(@journal) + page.show "journal-#{@journal.id}-notes" + page.remove "journal-#{@journal.id}-form" +end diff --git a/groups/app/views/layouts/_project_selector.rhtml b/groups/app/views/layouts/_project_selector.rhtml new file mode 100644 index 000000000..7a2803534 --- /dev/null +++ b/groups/app/views/layouts/_project_selector.rhtml @@ -0,0 +1,12 @@ +<% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %> + diff --git a/groups/app/views/layouts/base.rhtml b/groups/app/views/layouts/base.rhtml new file mode 100644 index 000000000..0b9d31512 --- /dev/null +++ b/groups/app/views/layouts/base.rhtml @@ -0,0 +1,65 @@ + + + +<%=h html_title %> + + + +<%= stylesheet_link_tag 'application', :media => 'all' %> +<%= javascript_include_tag :defaults %> +<%= stylesheet_link_tag 'jstoolbar' %> + + +<%= yield :header_tags %> + + +
+
+
+ <%= render_menu :account_menu -%> +
+ <%= content_tag('div', "#{l(:label_logged_as)} #{User.current.login}", :id => 'loggedas') if User.current.logged? %> + <%= render_menu :top_menu -%> +
+ + + +<%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %> + + +
+ <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %> + <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %> + <%= yield %> +
+
+ + + + + + + diff --git a/groups/app/views/mailer/_issue_text_html.rhtml b/groups/app/views/mailer/_issue_text_html.rhtml new file mode 100644 index 000000000..d0f247812 --- /dev/null +++ b/groups/app/views/mailer/_issue_text_html.rhtml @@ -0,0 +1,15 @@ +

<%= link_to "#{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 %> +
+ +<%= textilizable(issue, :description, :only_path => false) %> diff --git a/groups/app/views/mailer/_issue_text_plain.rhtml b/groups/app/views/mailer/_issue_text_plain.rhtml new file mode 100644 index 000000000..6b87c1808 --- /dev/null +++ b/groups/app/views/mailer/_issue_text_plain.rhtml @@ -0,0 +1,13 @@ +<%= "#{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 %> diff --git a/groups/app/views/mailer/account_activation_request.text.html.rhtml b/groups/app/views/mailer/account_activation_request.text.html.rhtml new file mode 100644 index 000000000..145ecfc8e --- /dev/null +++ b/groups/app/views/mailer/account_activation_request.text.html.rhtml @@ -0,0 +1,2 @@ +

<%= l(:mail_body_account_activation_request, @user.login) %>

+

<%= link_to @url, @url %>

diff --git a/groups/app/views/mailer/account_activation_request.text.plain.rhtml b/groups/app/views/mailer/account_activation_request.text.plain.rhtml new file mode 100644 index 000000000..f431e22d3 --- /dev/null +++ b/groups/app/views/mailer/account_activation_request.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_account_activation_request, @user.login) %> +<%= @url %> diff --git a/groups/app/views/mailer/account_information.text.html.rhtml b/groups/app/views/mailer/account_information.text.html.rhtml new file mode 100644 index 000000000..3b6ab6a9d --- /dev/null +++ b/groups/app/views/mailer/account_information.text.html.rhtml @@ -0,0 +1,11 @@ +<% 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) %>: <%= auto_link(@login_url) %>

diff --git a/groups/app/views/mailer/account_information.text.plain.rhtml b/groups/app/views/mailer/account_information.text.plain.rhtml new file mode 100644 index 000000000..0a02566d9 --- /dev/null +++ b/groups/app/views/mailer/account_information.text.plain.rhtml @@ -0,0 +1,6 @@ +<% 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 %> diff --git a/groups/app/views/mailer/attachments_added.text.html.rhtml b/groups/app/views/mailer/attachments_added.text.html.rhtml new file mode 100644 index 000000000..d2355b1c4 --- /dev/null +++ b/groups/app/views/mailer/attachments_added.text.html.rhtml @@ -0,0 +1,5 @@ +<%= link_to @added_to, @added_to_url %>
+ +
    <% @attachments.each do |attachment | %> +
  • <%= attachment.filename %>
  • +<% end %>
diff --git a/groups/app/views/mailer/attachments_added.text.plain.rhtml b/groups/app/views/mailer/attachments_added.text.plain.rhtml new file mode 100644 index 000000000..28cb8285e --- /dev/null +++ b/groups/app/views/mailer/attachments_added.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= @added_to %><% @attachments.each do |attachment | %> +- <%= attachment.filename %><% end %> + +<%= @added_to_url %> diff --git a/groups/app/views/mailer/document_added.text.html.rhtml b/groups/app/views/mailer/document_added.text.html.rhtml new file mode 100644 index 000000000..dc1f659a0 --- /dev/null +++ b/groups/app/views/mailer/document_added.text.html.rhtml @@ -0,0 +1,3 @@ +<%= link_to @document.title, @document_url %> (<%= @document.category.name %>)
+
+<%= textilizable(@document, :description, :only_path => false) %> diff --git a/groups/app/views/mailer/document_added.text.plain.rhtml b/groups/app/views/mailer/document_added.text.plain.rhtml new file mode 100644 index 000000000..a6a72829e --- /dev/null +++ b/groups/app/views/mailer/document_added.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= @document.title %> (<%= @document.category.name %>) +<%= @document_url %> + +<%= @document.description %> diff --git a/groups/app/views/mailer/issue_add.text.html.rhtml b/groups/app/views/mailer/issue_add.text.html.rhtml new file mode 100644 index 000000000..b1c4605e6 --- /dev/null +++ b/groups/app/views/mailer/issue_add.text.html.rhtml @@ -0,0 +1,3 @@ +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %> +
+<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/groups/app/views/mailer/issue_add.text.plain.rhtml b/groups/app/views/mailer/issue_add.text.plain.rhtml new file mode 100644 index 000000000..c6cb0837f --- /dev/null +++ b/groups/app/views/mailer/issue_add.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %> + +---------------------------------------- +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/groups/app/views/mailer/issue_edit.text.html.rhtml b/groups/app/views/mailer/issue_edit.text.html.rhtml new file mode 100644 index 000000000..48affaf77 --- /dev/null +++ b/groups/app/views/mailer/issue_edit.text.html.rhtml @@ -0,0 +1,11 @@ +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> + +
    +<% for detail in @journal.details %> +
  • <%= show_detail(detail, true) %>
  • +<% end %> +
+ +<%= textilizable(@journal, :notes, :only_path => false) %> +
+<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/groups/app/views/mailer/issue_edit.text.plain.rhtml b/groups/app/views/mailer/issue_edit.text.plain.rhtml new file mode 100644 index 000000000..b5a5ec978 --- /dev/null +++ b/groups/app/views/mailer/issue_edit.text.plain.rhtml @@ -0,0 +1,9 @@ +<%= l(:text_issue_updated, "##{@issue.id}", @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 } %> diff --git a/groups/app/views/mailer/layout.text.html.rhtml b/groups/app/views/mailer/layout.text.html.rhtml new file mode 100644 index 000000000..b78e92bdd --- /dev/null +++ b/groups/app/views/mailer/layout.text.html.rhtml @@ -0,0 +1,17 @@ + + + + + +<%= yield %> +
+<%= Redmine::WikiFormatting.to_html(Setting.emails_footer) %> + + diff --git a/groups/app/views/mailer/layout.text.plain.rhtml b/groups/app/views/mailer/layout.text.plain.rhtml new file mode 100644 index 000000000..ec3e1bfa0 --- /dev/null +++ b/groups/app/views/mailer/layout.text.plain.rhtml @@ -0,0 +1,3 @@ +<%= yield %> +---------------------------------------- +<%= Setting.emails_footer %> diff --git a/groups/app/views/mailer/lost_password.text.html.rhtml b/groups/app/views/mailer/lost_password.text.html.rhtml new file mode 100644 index 000000000..26eacfa92 --- /dev/null +++ b/groups/app/views/mailer/lost_password.text.html.rhtml @@ -0,0 +1,2 @@ +

<%= l(:mail_body_lost_password) %>
+<%= auto_link(@url) %>

diff --git a/groups/app/views/mailer/lost_password.text.plain.rhtml b/groups/app/views/mailer/lost_password.text.plain.rhtml new file mode 100644 index 000000000..aec1b5b86 --- /dev/null +++ b/groups/app/views/mailer/lost_password.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_lost_password) %> +<%= @url %> diff --git a/groups/app/views/mailer/message_posted.text.html.rhtml b/groups/app/views/mailer/message_posted.text.html.rhtml new file mode 100644 index 000000000..d91ce5a04 --- /dev/null +++ b/groups/app/views/mailer/message_posted.text.html.rhtml @@ -0,0 +1,4 @@ +

<%=h @message.board.project.name %> - <%=h @message.board.name %>: <%= link_to @message.subject, @message_url %>

+<%= @message.author %> + +<%= textilizable(@message, :content, :only_path => false) %> diff --git a/groups/app/views/mailer/message_posted.text.plain.rhtml b/groups/app/views/mailer/message_posted.text.plain.rhtml new file mode 100644 index 000000000..ef6a3b3ae --- /dev/null +++ b/groups/app/views/mailer/message_posted.text.plain.rhtml @@ -0,0 +1,4 @@ +<%= @message_url %> +<%= @message.author %> + +<%= @message.content %> diff --git a/groups/app/views/mailer/news_added.text.html.rhtml b/groups/app/views/mailer/news_added.text.html.rhtml new file mode 100644 index 000000000..15bc89fac --- /dev/null +++ b/groups/app/views/mailer/news_added.text.html.rhtml @@ -0,0 +1,4 @@ +

<%= link_to @news.title, @news_url %>

+<%= @news.author.name %> + +<%= textilizable(@news, :description, :only_path => false) %> diff --git a/groups/app/views/mailer/news_added.text.plain.rhtml b/groups/app/views/mailer/news_added.text.plain.rhtml new file mode 100644 index 000000000..c8ae3035f --- /dev/null +++ b/groups/app/views/mailer/news_added.text.plain.rhtml @@ -0,0 +1,5 @@ +<%= @news.title %> +<%= @news_url %> +<%= @news.author.name %> + +<%= @news.description %> diff --git a/groups/app/views/mailer/register.text.html.rhtml b/groups/app/views/mailer/register.text.html.rhtml new file mode 100644 index 000000000..145c3d7c9 --- /dev/null +++ b/groups/app/views/mailer/register.text.html.rhtml @@ -0,0 +1,2 @@ +

<%= l(:mail_body_register) %>
+<%= auto_link(@url) %>

diff --git a/groups/app/views/mailer/register.text.plain.rhtml b/groups/app/views/mailer/register.text.plain.rhtml new file mode 100644 index 000000000..102a15ee3 --- /dev/null +++ b/groups/app/views/mailer/register.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_register) %> +<%= @url %> diff --git a/groups/app/views/mailer/test.text.html.rhtml b/groups/app/views/mailer/test.text.html.rhtml new file mode 100644 index 000000000..25ad20c51 --- /dev/null +++ b/groups/app/views/mailer/test.text.html.rhtml @@ -0,0 +1,2 @@ +

This is a test email sent by Redmine.
+Redmine URL: <%= auto_link(@url) %>

diff --git a/groups/app/views/mailer/test.text.plain.rhtml b/groups/app/views/mailer/test.text.plain.rhtml new file mode 100644 index 000000000..790d6ab22 --- /dev/null +++ b/groups/app/views/mailer/test.text.plain.rhtml @@ -0,0 +1,2 @@ +This is a test email sent by Redmine. +Redmine URL: <%= @url %> diff --git a/groups/app/views/messages/_form.rhtml b/groups/app/views/messages/_form.rhtml new file mode 100644 index 000000000..540811ec3 --- /dev/null +++ b/groups/app/views/messages/_form.rhtml @@ -0,0 +1,21 @@ +<%= error_messages_for 'message' %> +<% replying ||= false %> + +
+ +


+<%= f.text_field :subject, :size => 120 %> + +<% if !replying && User.current.allowed_to?(:edit_messages, @project) %> + + +<% end %> +

+ +

<%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %>

+<%= wikitoolbar_for 'message_content' %> + + +

<%= l(:label_attachment_plural) %>
+<%= render :partial => 'attachments/form' %>

+
diff --git a/groups/app/views/messages/edit.rhtml b/groups/app/views/messages/edit.rhtml new file mode 100644 index 000000000..56e708620 --- /dev/null +++ b/groups/app/views/messages/edit.rhtml @@ -0,0 +1,14 @@ +

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%=h @message.subject %>

+ +<% 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 %> +
diff --git a/groups/app/views/messages/new.rhtml b/groups/app/views/messages/new.rhtml new file mode 100644 index 000000000..050c13284 --- /dev/null +++ b/groups/app/views/messages/new.rhtml @@ -0,0 +1,15 @@ +

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%= l(:label_message_new) %>

+ +<% 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 %> + +
diff --git a/groups/app/views/messages/show.rhtml b/groups/app/views/messages/show.rhtml new file mode 100644 index 000000000..251b7c7a5 --- /dev/null +++ b/groups/app/views/messages/show.rhtml @@ -0,0 +1,50 @@ +<%= 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}) %> + +
+ <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %> + <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> +
+ +

<%=h @topic.subject %>

+ +
+

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

+
+<%= textilizable(@topic.content, :attachments => @topic.attachments) %> +
+<%= link_to_attachments @topic.attachments, :no_author => true %> +
+
+ +

<%= l(:label_reply_plural) %>

+<% @replies.each do |message| %> + "> +
+ <%= link_to_if_authorized image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit) %> + <%= link_to_if_authorized image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete) %> +
+
+

<%=h message.subject %> - <%= authoring message.created_on, message.author %>

+
<%= textilizable message, :content, :attachments => message.attachments %>
+ <%= link_to_attachments message.attachments, :no_author => true %> +
+<% end %> + +<% if !@topic.locked? && authorize_for('messages', 'reply') %> +

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

+ +<% end %> diff --git a/groups/app/views/my/_block.rhtml b/groups/app/views/my/_block.rhtml new file mode 100644 index 000000000..bb5dd1f9f --- /dev/null +++ b/groups/app/views/my/_block.rhtml @@ -0,0 +1,14 @@ +
+ +
+ <%= link_to_remote "", { + :url => { :action => "remove_block", :block => block_name }, + :complete => "removeBlock('block_#{block_name}')" }, + :class => "close-icon" + %> +
+ +
+ <%= render :partial => "my/blocks/#{block_name}", :locals => { :user => user } %> +
+
\ No newline at end of file diff --git a/groups/app/views/my/_sidebar.rhtml b/groups/app/views/my/_sidebar.rhtml new file mode 100644 index 000000000..d30eacf90 --- /dev/null +++ b/groups/app/views/my/_sidebar.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_my_account)%>

+ +

<%=l(:field_login)%>: <%= @user.login %>
+<%=l(:field_created_on)%>: <%= format_time(@user.created_on) %>

+<% if @user.rss_token %> +

<%= 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 %>)

+<% end %> diff --git a/groups/app/views/my/account.rhtml b/groups/app/views/my/account.rhtml new file mode 100644 index 000000000..20210c99a --- /dev/null +++ b/groups/app/views/my/account.rhtml @@ -0,0 +1,52 @@ +
+<%= link_to(l(:button_change_password), :action => 'password') unless @user.auth_source_id %> +
+

<%=l(:label_my_account)%>

+<%= error_messages_for 'user' %> + +<% form_for :user, @user, :url => { :action => "account" }, + :builder => TabularFormBuilder, + :lang => current_language, + :html => { :id => 'my_account_form' } do |f| %> +
+

<%=l(:label_information_plural)%>

+
+

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

+

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

+

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

+

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

+
+ +<%= submit_tag l(:button_save) %> +
+ +
+

<%=l(:field_mail_notification)%>

+
+<%= 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 %> +

<% User.current.projects.each do |project| %> +
+<% end %>

+

<%= l(:text_user_mail_option) %>

+<% end %> +

+
+ +

<%=l(:label_preferences)%>

+
+<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %> +

<%= pref_fields.check_box :hide_mail %>

+

<%= pref_fields.select :time_zone, TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %>

+

<%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %>

+<% end %> +
+
+<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> + +<% html_title(l(:label_my_account)) -%> diff --git a/groups/app/views/my/blocks/_calendar.rhtml b/groups/app/views/my/blocks/_calendar.rhtml new file mode 100644 index 000000000..bad729363 --- /dev/null +++ b/groups/app/views/my/blocks/_calendar.rhtml @@ -0,0 +1,8 @@ +

<%= l(:label_calendar) %>

+ +<% 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 } %> diff --git a/groups/app/views/my/blocks/_documents.rhtml b/groups/app/views/my/blocks/_documents.rhtml new file mode 100644 index 000000000..a34be936f --- /dev/null +++ b/groups/app/views/my/blocks/_documents.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_document_plural)%>

+ +<%= render(:partial => 'documents/document', + :collection => Document.find(:all, + :limit => 10, + :order => "#{Document.table_name}.created_on DESC", + :conditions => "#{Document.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", + :include => [:project])) unless @user.projects.empty? %> \ No newline at end of file diff --git a/groups/app/views/my/blocks/_issuesassignedtome.rhtml b/groups/app/views/my/blocks/_issuesassignedtome.rhtml new file mode 100644 index 000000000..99812f6d0 --- /dev/null +++ b/groups/app/views/my/blocks/_issuesassignedtome.rhtml @@ -0,0 +1,17 @@ +

<%=l(:label_assigned_to_me_issues)%>

+<% assigned_issues = Issue.find(:all, + :conditions => ["assigned_to_id=? AND #{IssueStatus.table_name}.is_closed=? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id, false], + :limit => 10, + :include => [ :status, :project, :tracker, :priority ], + :order => "#{Enumeration.table_name}.position DESC, #{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => assigned_issues } %> +<% if assigned_issues.length > 0 %> +

<%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => 'me' %>

+<% 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 %> diff --git a/groups/app/views/my/blocks/_issuesreportedbyme.rhtml b/groups/app/views/my/blocks/_issuesreportedbyme.rhtml new file mode 100644 index 000000000..317aebbfc --- /dev/null +++ b/groups/app/views/my/blocks/_issuesreportedbyme.rhtml @@ -0,0 +1,17 @@ +

<%=l(:label_reported_issues)%>

+<% reported_issues = Issue.find(:all, + :conditions => ["author_id=? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.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 %> +

<%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :set_filter => 1, :author_id => 'me' %>

+<% 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 %> diff --git a/groups/app/views/my/blocks/_issueswatched.rhtml b/groups/app/views/my/blocks/_issueswatched.rhtml new file mode 100644 index 000000000..e5c2f23ab --- /dev/null +++ b/groups/app/views/my/blocks/_issueswatched.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_watched_issues)%>

+<% watched_issues = Issue.find(:all, + :include => [:status, :project, :tracker, :watchers], + :limit => 10, + :conditions => ["#{Watcher.table_name}.user_id = ? AND #{Project.table_name}.status=#{Project::STATUS_ACTIVE}", user.id], + :order => "#{Issue.table_name}.updated_on DESC") %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %> +<% if watched_issues.length > 0 %> +

<%=lwr(:label_last_updates, watched_issues.length)%>

+<% end %> diff --git a/groups/app/views/my/blocks/_news.rhtml b/groups/app/views/my/blocks/_news.rhtml new file mode 100644 index 000000000..d86cced92 --- /dev/null +++ b/groups/app/views/my/blocks/_news.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_news_latest)%>

+ +<%= render(:partial => 'news/news', + :collection => News.find(:all, + :limit => 10, + :order => "#{News.table_name}.created_on DESC", + :conditions => "#{News.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", + :include => [:project, :author])) unless @user.projects.empty? %> \ No newline at end of file diff --git a/groups/app/views/my/blocks/_timelog.rhtml b/groups/app/views/my/blocks/_timelog.rhtml new file mode 100644 index 000000000..a3f74e54d --- /dev/null +++ b/groups/app/views/my/blocks/_timelog.rhtml @@ -0,0 +1,52 @@ +

<%=l(:label_spent_time)%> (<%= l(:label_last_n_days, 7) %>)

+<% +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) +%> + +
+

<%= l(:label_total) %>: <%= html_hours("%.2f" % entries.sum(&:hours).to_f) %>

+
+ +<% if entries.any? %> + + + + + + + + + +<% entries_by_day.keys.sort.reverse.each do |day| %> + + + + + + + <% entries_by_day[day].each do |entry| -%> + + + + + + + + <% end -%> +<% end -%> + +
<%= l(:label_activity) %><%= l(:label_project) %><%= l(:field_comments) %><%= l(:field_hours) %>
<%= day == Date.today ? l(:label_today).titleize : format_date(day) %><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %>
<%=h entry.activity %><%=h entry.project %> <%= ' - ' + link_to_issue(entry.issue, :title => h("#{entry.issue.subject} (#{entry.issue.status})")) if entry.issue %><%=h entry.comments %><%= html_hours("%.2f" % entry.hours) %> + <% 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 -%> +
+<% end %> diff --git a/groups/app/views/my/page.rhtml b/groups/app/views/my/page.rhtml new file mode 100644 index 000000000..4d4c921b6 --- /dev/null +++ b/groups/app/views/my/page.rhtml @@ -0,0 +1,42 @@ +
+ <%= link_to l(:label_personalize_page), :action => 'page_layout' %> +
+ +

<%=l(:label_my_page)%>

+ +
+ <% @blocks['top'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['top'] %> +
+ +
+ <% @blocks['left'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['left'] %> +
+ +
+ <% @blocks['right'].each do |b| + next unless MyController::BLOCKS.keys.include? b %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['right'] %> +
+ +<% content_for :header_tags do %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + + +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> + +<% html_title(l(:label_my_page)) -%> diff --git a/groups/app/views/my/page_layout.rhtml b/groups/app/views/my/page_layout.rhtml new file mode 100644 index 000000000..1e348bf5b --- /dev/null +++ b/groups/app/views/my/page_layout.rhtml @@ -0,0 +1,107 @@ + + +
+<% form_tag({:action => "add_block"}, :id => "block-form") do %> +<%= select_tag 'block', "" + 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' %> +
+ +

<%=l(:label_my_page)%>

+ +
+ <% @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'] %> +
+ +
+ <% @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'] %> +
+ +
+ <% @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'] %> +
+ +<%= 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()" %> diff --git a/groups/app/views/my/password.rhtml b/groups/app/views/my/password.rhtml new file mode 100644 index 000000000..2e9fd0fa4 --- /dev/null +++ b/groups/app/views/my/password.rhtml @@ -0,0 +1,22 @@ +

<%=l(:button_change_password)%>

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

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

+ +

+<%= password_field_tag 'new_password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+ +

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

+
+<%= submit_tag l(:button_apply) %> +<% end %> + +<% content_for :sidebar do %> +<%= render :partial => 'sidebar' %> +<% end %> diff --git a/groups/app/views/news/_form.rhtml b/groups/app/views/news/_form.rhtml new file mode 100644 index 000000000..0cfe7a6d3 --- /dev/null +++ b/groups/app/views/news/_form.rhtml @@ -0,0 +1,8 @@ +<%= error_messages_for 'news' %> +
+

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

+

<%= f.text_area :summary, :cols => 60, :rows => 2 %>

+

<%= f.text_area :description, :required => true, :cols => 60, :rows => 15, :class => 'wiki-edit' %>

+
+ +<%= wikitoolbar_for 'news_description' %> diff --git a/groups/app/views/news/_news.rhtml b/groups/app/views/news/_news.rhtml new file mode 100644 index 000000000..e26d2c4a7 --- /dev/null +++ b/groups/app/views/news/_news.rhtml @@ -0,0 +1,6 @@ +

<%= 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 %> +<%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %> +
+<% unless news.summary.blank? %><%=h news.summary %>
<% end %> +<%= authoring news.created_on, news.author %>

diff --git a/groups/app/views/news/edit.rhtml b/groups/app/views/news/edit.rhtml new file mode 100644 index 000000000..a7e5e6e36 --- /dev/null +++ b/groups/app/views/news/edit.rhtml @@ -0,0 +1,14 @@ +

<%=l(:label_news)%>

+ +<% 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' }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
diff --git a/groups/app/views/news/index.rhtml b/groups/app/views/news/index.rhtml new file mode 100644 index 000000000..87db8a5f7 --- /dev/null +++ b/groups/app/views/news/index.rhtml @@ -0,0 +1,51 @@ +
+<%= link_to_if_authorized(l(:label_news_new), + {:controller => 'news', :action => 'new', :project_id => @project}, + :class => 'icon icon-add', + :onclick => 'Element.show("add-news"); return false;') if @project %> +
+ + + +

<%=l(:label_news_plural)%>

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

<%= l(:label_no_data) %>

+<% else %> +<% @newss.each do |news| %> +

<%= 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 %> + <%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %>

+

<%= authoring news.created_on, news.author %>

+
+ <%= textilizable(news.description) %> +
+<% end %> +<% end %> +

<%= pagination_links_full @news_pages %>

+ +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +

+ +<% 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)) -%> diff --git a/groups/app/views/news/new.rhtml b/groups/app/views/news/new.rhtml new file mode 100644 index 000000000..9208d8840 --- /dev/null +++ b/groups/app/views/news/new.rhtml @@ -0,0 +1,14 @@ +

<%=l(:label_news_new)%>

+ +<% 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' }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
diff --git a/groups/app/views/news/show.rhtml b/groups/app/views/news/show.rhtml new file mode 100644 index 000000000..a55b56f0b --- /dev/null +++ b/groups/app/views/news/show.rhtml @@ -0,0 +1,57 @@ +
+<%= 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' %> +
+ +

<%=h @news.title %>

+ + + +

<% unless @news.summary.blank? %><%=h @news.summary %>
<% end %> +<%= authoring @news.created_on, @news.author %>

+
+<%= textilizable(@news.description) %> +
+
+ +
+

<%= l(:label_comment_plural) %>

+<% @comments.each do |comment| %> + <% next if comment.new_record? %> +
+ <%= 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) %> +
+

<%= authoring comment.created_on, comment.author %>

+ <%= textilizable(comment.comments) %> +<% end if @comments.any? %> +
+ +<% if authorize_for 'news', 'add_comment' %> +

<%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %>

+<% form_tag({:action => 'add_comment', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %> +<%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'comment_comments' %> +

<%= submit_tag l(:button_add) %>

+<% end %> +<% end %> + +<% html_title @news.title -%> diff --git a/groups/app/views/projects/_edit.rhtml b/groups/app/views/projects/_edit.rhtml new file mode 100644 index 000000000..b7c2987d2 --- /dev/null +++ b/groups/app/views/projects/_edit.rhtml @@ -0,0 +1,4 @@ +<% 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 %> diff --git a/groups/app/views/projects/_form.rhtml b/groups/app/views/projects/_form.rhtml new file mode 100644 index 000000000..32e4dcd44 --- /dev/null +++ b/groups/app/views/projects/_form.rhtml @@ -0,0 +1,48 @@ +<%= error_messages_for 'project' %> + +
+ +

<%= f.text_field :name, :required => true %>
<%= l(:text_caracters_maximum, 30) %>

+ +<% if User.current.admin? and !@root_projects.empty? %> +

<%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %>

+<% end %> + +

<%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %>

+

<%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %> +<% unless @project.identifier_frozen? %> +
<%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %> +<% end %>

+

<%= f.text_field :homepage, :size => 40 %>

+

<%= f.check_box :is_public %>

+<%= wikitoolbar_for 'project_description' %> + +<% for @custom_value in @custom_values %> +

<%= custom_field_tag_with_label @custom_value %>

+<% end %> +
+ +<% unless @trackers.empty? %> +
<%=l(:label_tracker_plural)%> +<% @trackers.each do |tracker| %> + +<% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +
+<% end %> + +<% unless @custom_fields.empty? %> +
<%=l(:label_custom_field_plural)%> +<% for custom_field in @custom_fields %> + +<% end %> +<%= hidden_field_tag 'project[custom_field_ids][]', '' %> +
+<% end %> + diff --git a/groups/app/views/projects/activity.rhtml b/groups/app/views/projects/activity.rhtml new file mode 100644 index 000000000..c2f2f9ebd --- /dev/null +++ b/groups/app/views/projects/activity.rhtml @@ -0,0 +1,58 @@ +

<%= l(:label_activity) %>

+

<%= "#{l(:label_date_from)} #{format_date(@date_to - @days)} #{l(:label_date_to).downcase} #{format_date(@date_to-1)}" %>

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

<%= format_activity_day(day) %>

+
+<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> +
<%= format_time(e.event_datetime, false) %> + <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %>
+
<% unless e.event_description.blank? -%> + <%= format_activity_description(e.event_description) %>
+ <% end %> + <%= e.event_author if e.respond_to?(:event_author) %>
+<% end -%> +
+<% end -%> +
+ +<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %> + +
+<%= link_to_remote(('« ' + l(:label_previous)), + {:update => "content", :url => params.merge(:from => @date_to - @days), :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(params.merge(:from => @date_to - @days)), + :title => "#{l(:label_date_from)} #{format_date(@date_to - 2*@days)} #{l(:label_date_to).downcase} #{format_date(@date_to - @days - 1)}"}) %> +
+
+<%= link_to_remote((l(:label_next) + ' »'), + {:update => "content", :url => params.merge(:from => @date_to + @days), :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(params.merge(:from => @date_to + @days)), + :title => "#{l(:label_date_from)} #{format_date(@date_to)} #{l(:label_date_to).downcase} #{format_date(@date_to + @days - 1)}"}) unless @date_to >= Date.today %> +
+  +

+ <%= l(:label_export_to) %> + <%= link_to 'Atom', params.merge(:format => :atom, :key => User.current.rss_key).delete_if{|k,v|k=="commit"}, :class => 'feed' %> +

+ +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :year => nil, :month => nil, :key => User.current.rss_key)) %> +<% end %> + +<% content_for :sidebar do %> +<% form_tag({}, :method => :get) do %> +

<%= l(:label_activity) %>

+

<% @event_types.each do |t| %> +
+<% end %>

+<% if @project && @project.active_children.any? %> +

+ <%= hidden_field_tag 'with_subprojects', 0 %> +<% end %> +

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

+<% end %> +<% end %> + +<% html_title(l(:label_activity)) -%> diff --git a/groups/app/views/projects/add.rhtml b/groups/app/views/projects/add.rhtml new file mode 100644 index 000000000..bc3d7b09a --- /dev/null +++ b/groups/app/views/projects/add.rhtml @@ -0,0 +1,16 @@ +

<%=l(:label_project_new)%>

+ +<% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> + +
<%= l(:label_module_plural) %> +<% Redmine::AccessControl.available_project_modules.each do |m| %> + +<% end %> +
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/projects/add_file.rhtml b/groups/app/views/projects/add_file.rhtml new file mode 100644 index 000000000..0ee55083d --- /dev/null +++ b/groups/app/views/projects/add_file.rhtml @@ -0,0 +1,13 @@ +

<%=l(:label_attachment_new)%>

+ +<%= error_messages_for 'attachment' %> +
+<% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %> + +

+<%= select_tag "version_id", options_from_collection_for_select(@versions, "id", "name") %>

+ +

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

+
+<%= submit_tag l(:button_add) %> +<% end %> \ No newline at end of file diff --git a/groups/app/views/projects/add_issue_category.rhtml b/groups/app/views/projects/add_issue_category.rhtml new file mode 100644 index 000000000..08bc6d0e3 --- /dev/null +++ b/groups/app/views/projects/add_issue_category.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_category_new)%>

+ +<% 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 %> diff --git a/groups/app/views/projects/add_version.rhtml b/groups/app/views/projects/add_version.rhtml new file mode 100644 index 000000000..c038b7de9 --- /dev/null +++ b/groups/app/views/projects/add_version.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_version_new)%>

+ +<% 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 diff --git a/groups/app/views/projects/calendar.rhtml b/groups/app/views/projects/calendar.rhtml new file mode 100644 index 000000000..743721cb3 --- /dev/null +++ b/groups/app/views/projects/calendar.rhtml @@ -0,0 +1,41 @@ +

<%= "#{month_name(@month)} #{@year}" %>

+ + + +
+ <%= 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), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }}, + {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects])} + %> + + <%= 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), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }}, + {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1), :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects])} + %> +
+ +<%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %> + +<%= image_tag 'arrow_from.png' %>  <%= l(:text_tip_task_begin_day) %>
+<%= image_tag 'arrow_to.png' %>  <%= l(:text_tip_task_end_day) %>
+<%= image_tag 'arrow_bw.png' %>  <%= l(:text_tip_task_begin_end_day) %>
+ +<% content_for :sidebar do %> +

<%= l(:label_calendar) %>

+ + <% form_tag() do %> +

<%= select_month(@month, :prefix => "month", :discard_type => true) %> + <%= select_year(@year, :prefix => "year", :discard_type => true) %>

+ + <% @trackers.each do |tracker| %> +
+ <% end %> + <% if @project.active_children.any? %> +
+ <%= hidden_field_tag 'with_subprojects', 0 %> + <% end %> +

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

+ <% end %> +<% end %> + +<% html_title(l(:label_calendar)) -%> diff --git a/groups/app/views/projects/changelog.rhtml b/groups/app/views/projects/changelog.rhtml new file mode 100644 index 000000000..e4d32a393 --- /dev/null +++ b/groups/app/views/projects/changelog.rhtml @@ -0,0 +1,42 @@ +

<%=l(:label_change_log)%>

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

<%= l(:label_no_data) %>

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

<%= version.name %>

+ <% if version.effective_date %> +

<%= format_date(version.effective_date) %>

+ <% end %> +

<%=h version.description %>

+ <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :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? %> +
    + <% issues.each do |issue| %> +
  • <%= link_to_issue(issue) %>: <%=h issue.subject %>
  • + <% end %> +
+ <% end %> +<% end %> + +<% content_for :sidebar do %> +<% form_tag do %> +

<%= l(:label_change_log) %>

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

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

+<% end %> + +

<%= l(:label_version_plural) %>

+<% @versions.each do |version| %> +<%= link_to version.name, :anchor => version.name %>
+<% end %> +<% end %> diff --git a/groups/app/views/projects/destroy.rhtml b/groups/app/views/projects/destroy.rhtml new file mode 100644 index 000000000..a1913c115 --- /dev/null +++ b/groups/app/views/projects/destroy.rhtml @@ -0,0 +1,16 @@ +

<%=l(:label_confirmation)%>

+
+

<%=h @project_to_destroy %>
+<%=l(:text_project_destroy_confirmation)%> + +<% if @project_to_destroy.children.any? %> +
<%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %> +<% end %> +

+

+ <% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %> + + <%= submit_tag l(:button_delete) %> + <% end %> +

+
diff --git a/groups/app/views/projects/gantt.rfpdf b/groups/app/views/projects/gantt.rfpdf new file mode 100644 index 000000000..a293906ba --- /dev/null +++ b/groups/app/views/projects/gantt.rfpdf @@ -0,0 +1,188 @@ +<% +pdf=IfpdfHelper::IFPDF.new(current_language) +pdf.SetTitle("#{@project.name} - #{l(:label_gantt)}") +pdf.AliasNbPages +pdf.footer_date = format_date(Date.today) +pdf.AddPage("L") +pdf.SetFontStyle('B',12) +pdf.SetX(15) +pdf.Cell(70, 20, @project.name) +pdf.Ln +pdf.SetFontStyle('B',9) + +subject_width = 70 +header_heigth = 5 + +headers_heigth = header_heigth +show_weeks = false +show_days = false + +if @months < 7 + show_weeks = true + headers_heigth = 2*header_heigth + if @months < 3 + show_days = true + headers_heigth = 3*header_heigth + end +end + +g_width = 210 +zoom = (g_width) / (@date_to - @date_from + 1) +g_height = 120 +t_height = g_height + headers_heigth + +y_start = pdf.GetY + + +# +# Months headers +# +month_f = @date_from +left = subject_width +height = header_heigth +@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 @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-1 + pdf.SetY(y_start + header_heigth) + pdf.SetX(left) + pdf.Cell(width + 1, height, "", "LTR") + left = left + width+1 + end + while week_f <= @date_to + width = (week_f + 6 <= @date_to) ? 7 * zoom : (@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 = @date_from.cwday + pdf.SetFontStyle('B',7) + (@date_to - @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) +@events.each do |i| + pdf.SetY(top) + pdf.SetX(15) + + if i.is_a? Issue + pdf.Cell(subject_width-15, 5, "#{i.tracker.name} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR") + else + pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR") + end + + pdf.SetY(top) + pdf.SetX(subject_width) + pdf.Cell(g_width, 5, "", "LR") + + pdf.SetY(top+1.5) + + if i.is_a? Issue + i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) + i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to ) + + i_done_date = i.start_date + ((i.due_date - 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 = ((i_start_date - @date_from)*zoom) + i_width = ((i_end_date - i_start_date + 1)*zoom) + d_width = ((i_done_date - i_start_date)*zoom) + l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date + l_width ||= 0 + + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(200,200,200) + pdf.Cell(i_width, 2, "", 0, 0, "", 1) + + if l_width > 0 + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(255,100,100) + pdf.Cell(l_width, 2, "", 0, 0, "", 1) + end + if d_width > 0 + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left) + pdf.SetFillColor(100,100,255) + pdf.Cell(d_width, 2, "", 0, 0, "", 1) + end + + pdf.SetY(top+1.5) + pdf.SetX(subject_width + i_left + i_width) + pdf.Cell(30, 2, "#{i.status.name} #{i.done_ratio}%") + else + i_left = ((i.start_date - @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 %> \ No newline at end of file diff --git a/groups/app/views/projects/gantt.rhtml b/groups/app/views/projects/gantt.rhtml new file mode 100644 index 000000000..d941d2777 --- /dev/null +++ b/groups/app/views/projects/gantt.rhtml @@ -0,0 +1,250 @@ +<% zoom = 1 +@zoom.times { zoom = zoom * 2 } + +subject_width = 330 +header_heigth = 18 + +headers_height = header_heigth +show_weeks = false +show_days = false + +if @zoom >1 + show_weeks = true + headers_height = 2*header_heigth + if @zoom > 2 + show_days = true + headers_height = 3*header_heigth + end +end + +g_width = (@date_to - @date_from + 1)*zoom +g_height = [(20 * @events.length + 6)+150, 206].max +t_height = g_height + headers_height +%> + +
+
+ +

<%= l(:label_gantt) %>

+ +<% form_tag(params.merge(:month => nil, :year => nil, :months => nil)) do %> + + + + + + +
+ + <%= l(:label_months_from) %> + <%= select_month(@month_from, :prefix => "month", :discard_type => true) %> + <%= select_year(@year_from, :prefix => "year", :discard_type => true) %> + <%= hidden_field_tag 'zoom', @zoom %> + <%= submit_tag l(:button_submit), :class => "button-small" %> + +<%= if @zoom < 4 + link_to image_tag('zoom_in.png'), {:zoom => (@zoom+1), :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects]} + else + image_tag 'zoom_in_g.png' + end %> +<%= if @zoom > 1 + link_to image_tag('zoom_out.png'),{:zoom => (@zoom-1), :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects]} + else + image_tag 'zoom_out_g.png' + end %> +
+<% end %> + + + + + + +
+ +
+
+
+<% +# +# Tasks subjects +# +top = headers_height + 8 +@events.each do |i| %> +
+ <% if i.is_a? Issue %> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%=h i.subject %> + <% else %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i %> + + <% end %> +
+ <% top = top + 20 +end %> +
+
+ +
+
 
+<% +# +# Months headers +# +month_f = @date_from +left = 0 +height = (show_weeks ? header_heigth : header_heigth + g_height) +@months.times do + width = ((month_f >> 1) - month_f) * zoom - 1 + %> +
+ <%= link_to "#{month_f.year}-#{month_f.month}", { :year => month_f.year, :month => month_f.month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] }, :title => "#{month_name(month_f.month)} #{month_f.year}"%> +
+ <% + 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 @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-1 + %> +
 
+ <% + left = left + width+1 + end %> + <% + while week_f <= @date_to + width = (week_f + 6 <= @date_to) ? 7 * zoom -1 : (@date_to - week_f + 1) * zoom-1 + %> +
+ <%= week_f.cweek if width >= 16 %> +
+ <% + 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 = @date_from.cwday + (@date_to - @date_from + 1).to_i.times do + width = zoom - 1 + %> +
5 %>" class="gantt_hdr"> + <%= day_name(wday).first %> +
+ <% + left = left + width+1 + wday = wday + 1 + wday = 1 if wday > 7 + end +end %> + +<% +# +# Tasks +# +top = headers_height + 10 +@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_date <= @date_to ? i.due_date : @date_to ) + + i_done_date = i.start_date + ((i.due_date - 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 = ((i_start_date - @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 + %> +
 
+ <% if l_width > 0 %> +
 
+ <% end %> + <% if d_width > 0 %> +
 
+ <% end %> +
+ <%= i.status.name %> + <%= (i.done_ratio).to_i %>% +
+ <% # === tooltip === %> +
+ + <%= render_issue_tooltip i %> +
+<% else + i_left = ((i.start_date - @date_from)*zoom).floor + %> +
 
+
+ <%= h("#{i.project} -") unless @project && @project == i.project %> + <%=h i %> +
+<% end %> + <% top = top + 20 +end %> + +<% +# +# Today red line (excluded from cache) +# +if Date.today >= @date_from and Date.today <= @date_to %> +
 
+<% end %> + +
+
+ + + + + + +
<%= link_to ('« ' + l(:label_previous)), :year => (@date_from << @months).year, :month => (@date_from << @months).month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] %><%= link_to (l(:label_next) + ' »'), :year => (@date_from >> @months).year, :month => (@date_from >> @months).month, :zoom => @zoom, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects] %>
+ +

+<%= l(:label_export_to) %> +<%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'pdf'}, :class => 'pdf' %> +<%= content_tag('span', link_to('PNG', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'png'}, :class => 'image')) if respond_to?('gantt_image') %> +

+ +<% content_for :sidebar do %> +

<%= l(:label_gantt) %>

+ <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil)) do %> + <% @trackers.each do |tracker| %> +
+ <% end %> + <% if @project.active_children.any? %> +
+ <%= hidden_field_tag 'with_subprojects', 0 %> + <% end %> +

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

+ <% end %> +<% end %> + +<% html_title(l(:label_gantt)) -%> diff --git a/groups/app/views/projects/list.rhtml b/groups/app/views/projects/list.rhtml new file mode 100644 index 000000000..b8bb62ebb --- /dev/null +++ b/groups/app/views/projects/list.rhtml @@ -0,0 +1,25 @@ +
+ <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> | + <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%> +
+ +

<%=l(:label_project_plural)%>

+ +<% @project_tree.keys.sort.each do |project| %> +

<%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %>

+<%= textilizable(project.short_description, :project => project) %> + +<% if @project_tree[project].any? %> +

<%= l(:label_subproject_plural) %>: + <%= @project_tree[project].sort.collect {|subproject| + link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %>

+<% end %> +<% end %> + +<% if User.current.logged? %> +
+<%= l(:label_my_projects) %> +
+<% end %> + +<% html_title(l(:label_project_plural)) -%> diff --git a/groups/app/views/projects/list_files.rhtml b/groups/app/views/projects/list_files.rhtml new file mode 100644 index 000000000..f385229ae --- /dev/null +++ b/groups/app/views/projects/list_files.rhtml @@ -0,0 +1,45 @@ +
+<%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_attachment_plural)%>

+ +<% delete_allowed = authorize_for('versions', 'destroy_file') %> + + + + + <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %> + <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %> + <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %> + <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %> + + <% if delete_allowed %><% end %> + + +<% for version in @versions %> + <% unless version.attachments.empty? %> + + <% for file in version.attachments %> + "> + + + + + + + <% if delete_allowed %> + + <% end %> + + <% end + reset_cycle %> + <% end %> +<% end %> + +
<%=l(:field_version)%>MD5
<%= version.name %>
<%= link_to(file.filename, {:controller => 'versions', :action => 'download', :id => version, :attachment_id => file}, + :title => file.description) %><%= format_time(file.created_on) %><%= number_to_human_size(file.filesize) %><%= file.downloads %><%= file.digest %> + <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %> +
+ +<% html_title(l(:label_attachment_plural)) -%> diff --git a/groups/app/views/projects/list_members.rhtml b/groups/app/views/projects/list_members.rhtml new file mode 100644 index 000000000..fcfb4f7c0 --- /dev/null +++ b/groups/app/views/projects/list_members.rhtml @@ -0,0 +1,13 @@ +

<%=l(:label_member_plural)%>

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

<%= l(:label_no_data) %>

<% end %> + +<% members = @members.group_by {|m| m.role } %> +<% members.keys.sort{|x,y| x.position <=> y.position}.each do |role| %> +

<%= role.name %>

+
    +<% members[role].each do |m| %> +
  • <%= link_to m.name, :controller => 'account', :action => 'show', :id => m.user %> (<%= format_date m.created_on %>)
  • +<% end %> +
+<% end %> diff --git a/groups/app/views/projects/roadmap.rhtml b/groups/app/views/projects/roadmap.rhtml new file mode 100644 index 000000000..d9329d109 --- /dev/null +++ b/groups/app/views/projects/roadmap.rhtml @@ -0,0 +1,50 @@ +

<%=l(:label_roadmap)%>

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

<%= l(:label_no_data) %>

+<% else %> +
+<% @versions.each do |version| %> + <%= tag 'a', :name => version.name %> +

<%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %>

+ <%= 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], + :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 %> + + <% end %> +<% end %> +
+<% end %> + +<% content_for :sidebar do %> +<% form_tag do %> +

<%= l(:label_roadmap) %>

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

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

+<% end %> + +

<%= l(:label_version_plural) %>

+<% @versions.each do |version| %> +<%= link_to version.name, :anchor => version.name %>
+<% end %> +<% end %> + +<% html_title(l(:label_roadmap)) %> diff --git a/groups/app/views/projects/settings.rhtml b/groups/app/views/projects/settings.rhtml new file mode 100644 index 000000000..c7b0f5006 --- /dev/null +++ b/groups/app/views/projects/settings.rhtml @@ -0,0 +1,24 @@ +

<%=l(:label_settings)%>

+ +<% tabs = project_settings_tabs %> +<% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %> + +
+
    +<% tabs.each do |tab| -%> +
  • <%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %>
  • +<% end -%> +
+
+ +<% tabs.each do |tab| -%> +<%= content_tag('div', render(:partial => tab[:partial]), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> + +<% html_title(l(:label_settings)) -%> diff --git a/groups/app/views/projects/settings/_boards.rhtml b/groups/app/views/projects/settings/_boards.rhtml new file mode 100644 index 000000000..bf6da3ab0 --- /dev/null +++ b/groups/app/views/projects/settings/_boards.rhtml @@ -0,0 +1,28 @@ +<% if @project.boards.any? %> + + + +<% @project.boards.each do |board| + next if board.new_record? %> + + + + + + + +<% end %> + +
<%= l(:label_board) %><%= l(:field_description) %>
<%=h board.name %><%=h board.description %> + <% if authorize_for("boards", "edit") %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + <% end %> + <%= link_to_if_authorized l(:button_edit), {:controller => 'boards', :action => 'edit', :project_id => @project, :id => board}, :class => 'icon icon-edit' %><%= 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' %>
+<% else %> +

<%= l(:label_no_data) %>

+<% end %> + +

<%= link_to_if_authorized l(:label_board_new), {:controller => 'boards', :action => 'new', :project_id => @project} %>

diff --git a/groups/app/views/projects/settings/_issue_categories.rhtml b/groups/app/views/projects/settings/_issue_categories.rhtml new file mode 100644 index 000000000..6d9dc0d55 --- /dev/null +++ b/groups/app/views/projects/settings/_issue_categories.rhtml @@ -0,0 +1,26 @@ +<% if @project.issue_categories.any? %> + + + + + + + + +<% for category in @project.issue_categories %> + <% unless category.new_record? %> + + + + + + + <% end %> +<% end %> + +
<%= l(:label_issue_category) %><%= l(:field_assigned_to) %>
<%=h(category.name) %><%=h(category.assigned_to.name) if category.assigned_to %><%= link_to_if_authorized l(:button_edit), { :controller => 'issue_categories', :action => 'edit', :id => category }, :class => 'icon icon-edit' %><%= link_to_if_authorized l(:button_delete), {:controller => 'issue_categories', :action => 'destroy', :id => category}, :method => :post, :class => 'icon icon-del' %>
+<% else %> +

<%= l(:label_no_data) %>

+<% end %> + +

<%= link_to_if_authorized l(:label_issue_category_new), :controller => 'projects', :action => 'add_issue_category', :id => @project %>

diff --git a/groups/app/views/projects/settings/_members.rhtml b/groups/app/views/projects/settings/_members.rhtml new file mode 100644 index 000000000..ab22ac602 --- /dev/null +++ b/groups/app/views/projects/settings/_members.rhtml @@ -0,0 +1,48 @@ +<%= error_messages_for 'member' %> +<% roles = Role.find_all_givable %> +<% users = User.find_active(:all).sort - @project.users %> +<% # members sorted by role position + members = @project.members.find(:all, :include => [:role, :user]).sort %> + +<% if members.any? %> + + + + + + + + <% members.each do |member| %> + <% next if member.new_record? %> + + + + + + +<% end; reset_cycle %> +
<%= l(:label_user) %><%= l(:label_role) %>
<%= member.name %> + <% if authorize_for('members', 'edit') %> + <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member}, :method => :post) do |f| %> + <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, {}, :class => "small" %> + <%= submit_tag l(:button_change), :class => "small" %> + <% end %> + <% end %> + + <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member}, + :method => :post + }, :title => l(:button_delete), + :class => 'icon icon-del' %> +
+<% else %> +

<%= l(:label_no_data) %>

+<% end %> + +<% if authorize_for('members', 'new') && !users.empty? %> + <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %> +


+ <%= f.select :user_id, users.collect{|user| [user.name, user.id]} %> + <%= l(:label_role) %>: <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, :selected => nil %> + <%= submit_tag l(:button_add) %>

+ <% end %> +<% end %> diff --git a/groups/app/views/projects/settings/_modules.rhtml b/groups/app/views/projects/settings/_modules.rhtml new file mode 100644 index 000000000..b4decf78a --- /dev/null +++ b/groups/app/views/projects/settings/_modules.rhtml @@ -0,0 +1,17 @@ +<% form_for :project, @project, + :url => { :action => 'modules', :id => @project }, + :html => {:id => 'modules-form'} do |f| %> + +
+<%= l(:text_select_project_modules) %> + +<% Redmine::AccessControl.available_project_modules.each do |m| %> +

+<% end %> +
+ +

<%= check_all_links 'modules-form' %>

+

<%= submit_tag l(:button_save) %>

+ +<% end %> diff --git a/groups/app/views/projects/settings/_repository.rhtml b/groups/app/views/projects/settings/_repository.rhtml new file mode 100644 index 000000000..95830ab98 --- /dev/null +++ b/groups/app/views/projects/settings/_repository.rhtml @@ -0,0 +1,21 @@ +<% remote_form_for :repository, @repository, + :url => { :controller => 'repositories', :action => 'edit', :id => @project }, + :builder => TabularFormBuilder, + :lang => current_language do |f| %> + +<%= error_messages_for 'repository' %> + +
+

<%= scm_select_tag(@repository) %>

+<%= repository_field_tags(f, @repository) if @repository %> +
+ +
+<%= link_to(l(:button_delete), {:controller => 'repositories', :action => 'destroy', :id => @project}, + :confirm => l(:text_are_you_sure), + :method => :post, + :class => 'icon icon-del') if @repository && !@repository.new_record? %> +
+ +<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save)) %> +<% end %> diff --git a/groups/app/views/projects/settings/_versions.rhtml b/groups/app/views/projects/settings/_versions.rhtml new file mode 100644 index 000000000..7329c7f3b --- /dev/null +++ b/groups/app/views/projects/settings/_versions.rhtml @@ -0,0 +1,29 @@ +<% if @project.versions.any? %> + + + + + + + + + + +<% for version in @project.versions.sort %> + + + + + + + + + +<% end; reset_cycle %> + +
<%= l(:label_version) %><%= l(:field_effective_date) %><%= l(:field_description) %><%= l(:label_wiki_page) unless @project.wiki.nil? %>
<%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %><%= format_date(version.effective_date) %><%=h version.description %><%= link_to(version.wiki_page_title, :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %><%= 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' %>
+<% else %> +

<%= l(:label_no_data) %>

+<% end %> + +

<%= link_to_if_authorized l(:label_version_new), :controller => 'projects', :action => 'add_version', :id => @project %>

diff --git a/groups/app/views/projects/settings/_wiki.rhtml b/groups/app/views/projects/settings/_wiki.rhtml new file mode 100644 index 000000000..00da93039 --- /dev/null +++ b/groups/app/views/projects/settings/_wiki.rhtml @@ -0,0 +1,19 @@ +<% remote_form_for :wiki, @wiki, + :url => { :controller => 'wikis', :action => 'edit', :id => @project }, + :builder => TabularFormBuilder, + :lang => current_language do |f| %> + +<%= error_messages_for 'wiki' %> + +
+

<%= f.text_field :start_page, :size => 60, :required => true %>
+<%= l(:text_unallowed_characters) %>: , . / ? ; : |

+
+ +
+<%= link_to(l(:button_delete), {:controller => 'wikis', :action => 'destroy', :id => @project}, + :class => 'icon icon-del') if @wiki && !@wiki.new_record? %> +
+ +<%= submit_tag((@wiki.nil? || @wiki.new_record?) ? l(:button_create) : l(:button_save)) %> +<% end %> diff --git a/groups/app/views/projects/show.rhtml b/groups/app/views/projects/show.rhtml new file mode 100644 index 000000000..62b911937 --- /dev/null +++ b/groups/app/views/projects/show.rhtml @@ -0,0 +1,80 @@ +

<%=l(:label_overview)%>

+ +
+ <%= textilizable @project.description %> +
    + <% unless @project.homepage.blank? %>
  • <%=l(:field_homepage)%>: <%= auto_link @project.homepage %>
  • <% end %> + <% if @subprojects.any? %> +
  • <%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %>
  • + <% end %> + <% if @project.parent %> +
  • <%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %>
  • + <% end %> + <% for custom_value in @custom_values %> + <% if !custom_value.value.empty? %> +
  • <%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %>
  • + <% end %> + <% end %> +
+ + <% if User.current.allowed_to?(:view_issues, @project) %> +
+

<%=l(:label_issue_tracking)%>

+
    + <% for tracker in @trackers %> +
  • <%= link_to tracker.name, :controller => 'issues', :action => 'index', :project_id => @project, + :set_filter => 1, + "tracker_id" => tracker.id %>: + <%= @open_issues_by_tracker[tracker] || 0 %> <%= lwr(:label_open_issues, @open_issues_by_tracker[tracker] || 0) %> + <%= l(:label_on) %> <%= @total_issues_by_tracker[tracker] || 0 %>
  • + <% end %> +
+

<%= link_to l(:label_issue_view_all), :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 %>

+
+ <% end %> +
+ +
+ <% if @members_by_role.any? %> +
+

<%=l(:label_member_plural)%>

+

<% @members_by_role.keys.sort.each do |role| %> + <%= role.name %>: + <%= @members_by_role[role].collect(&:user).sort.collect{|u| link_to_user u}.join(", ") %> +
+ <% end %>

+
+ <% end %> + + <% if @news.any? && authorize_for('news', 'index') %> +
+

<%=l(:label_news_latest)%>

+ <%= render :partial => 'news/news', :collection => @news %> +

<%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %>

+
+ <% end %> +
+ +<% content_for :sidebar do %> + <% planning_links = [] + planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project) + planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project) + planning_links.compact! + unless planning_links.empty? %> +

<%= l(:label_planning) %>

+

<%= planning_links.join(' | ') %>

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

<%= l(:label_spent_time) %>

+

<%= lwr(:label_f_hour, @total_hours) %>

+

<%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> | + <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %>

+ <% end %> +<% 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)) -%> diff --git a/groups/app/views/queries/_columns.rhtml b/groups/app/views/queries/_columns.rhtml new file mode 100644 index 000000000..1a481adae --- /dev/null +++ b/groups/app/views/queries/_columns.rhtml @@ -0,0 +1,27 @@ +<% content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %> +<%= l(:field_column_names) %> + +<%= hidden_field_tag 'query[column_names][]', '', :id => nil %> + + + + + + +
<%= 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" %> + +
+ +
<%= 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" %> +
+<% end %> + +<% content_for :header_tags do %> +<%= javascript_include_tag 'select_list_move' %> +<% end %> diff --git a/groups/app/views/queries/_filters.rhtml b/groups/app/views/queries/_filters.rhtml new file mode 100644 index 000000000..ec9d4fef6 --- /dev/null +++ b/groups/app/views/queries/_filters.rhtml @@ -0,0 +1,101 @@ + + + + + + + +
+ +<% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %> + <% field = filter[0] + options = filter[1] %> + id="tr_<%= field %>" class="filter"> + + + + +<% end %> +
+ <%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %> + + + <%= 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;" %> + + + +
+
+<%= 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" %> +
diff --git a/groups/app/views/queries/_form.rhtml b/groups/app/views/queries/_form.rhtml new file mode 100644 index 000000000..8da264032 --- /dev/null +++ b/groups/app/views/queries/_form.rhtml @@ -0,0 +1,29 @@ +<%= error_messages_for 'query' %> +<%= hidden_field_tag 'confirm', 1 %> + +
+
+

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

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

+<%= 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;}') %>

+<% end %> + +

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

+ +

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

+
+ +
<%= l(:label_filter_plural) %> +<%= render :partial => 'queries/filters', :locals => {:query => query}%> +
+ +<%= render :partial => 'queries/columns', :locals => {:query => query}%> +
diff --git a/groups/app/views/queries/edit.rhtml b/groups/app/views/queries/edit.rhtml new file mode 100644 index 000000000..1c99ac077 --- /dev/null +++ b/groups/app/views/queries/edit.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_query) %>

+ +<% form_tag({:action => 'edit', :id => @query}, :onsubmit => 'selectAllOptions("selected_columns");') do %> + <%= render :partial => 'form', :locals => {:query => @query} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/queries/index.rhtml b/groups/app/views/queries/index.rhtml new file mode 100644 index 000000000..1c608b8ac --- /dev/null +++ b/groups/app/views/queries/index.rhtml @@ -0,0 +1,27 @@ +
+<%= link_to_if_authorized l(:label_query_new), {:controller => 'queries', :action => 'new', :project_id => @project}, :class => 'icon icon-add' %> +
+ +

<%= l(:label_query_plural) %>

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

<%=l(:label_no_data)%>

+<% else %> + + <% @queries.each do |query| %> + + + + + <% end %> +
+ <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %> + + + <% 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' %> + + <% end %> +
+<% end %> diff --git a/groups/app/views/queries/new.rhtml b/groups/app/views/queries/new.rhtml new file mode 100644 index 000000000..631fd6c02 --- /dev/null +++ b/groups/app/views/queries/new.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_query_new) %>

+ +<% 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 %> diff --git a/groups/app/views/reports/_details.rhtml b/groups/app/views/reports/_details.rhtml new file mode 100644 index 000000000..c3ad2bed7 --- /dev/null +++ b/groups/app/views/reports/_details.rhtml @@ -0,0 +1,48 @@ +<% if @statuses.empty? or rows.empty? %> +

<%=l(:label_no_data)%>

+<% else %> +<% col_width = 70 / (@statuses.length+3) %> + + + +<% for status in @statuses %> + +<% end %> + + + + + +<% for row in rows %> +"> + + <% for status in @statuses %> + + <% end %> + + + + +<% end %> + +
<%= status.name %><%=l(:label_open_issues_plural)%><%=l(:label_closed_issues_plural)%><%=l(:label_total)%>
<%= link_to row.name, :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id %><%= 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 %><%= 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" %><%= 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" %><%= 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" => "*" %>
+<% end + reset_cycle %> \ No newline at end of file diff --git a/groups/app/views/reports/_simple.rhtml b/groups/app/views/reports/_simple.rhtml new file mode 100644 index 000000000..7f799f325 --- /dev/null +++ b/groups/app/views/reports/_simple.rhtml @@ -0,0 +1,37 @@ +<% if @statuses.empty? or rows.empty? %> +

<%=l(:label_no_data)%>

+<% else %> + + + + + + + + +<% for row in rows %> +"> + + + + + +<% end %> + +
<%=l(:label_open_issues_plural)%><%=l(:label_closed_issues_plural)%><%=l(:label_total)%>
<%= link_to row.name, :controller => 'issues', :action => 'index', :project_id => ((row.is_a?(Project) ? row : @project)), + :set_filter => 1, + "#{field_name}" => row.id %><%= 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" %><%= 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" %><%= 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" => "*" %>
+<% end + reset_cycle %> \ No newline at end of file diff --git a/groups/app/views/reports/issue_report.rhtml b/groups/app/views/reports/issue_report.rhtml new file mode 100644 index 000000000..1ed16ea3b --- /dev/null +++ b/groups/app/views/reports/issue_report.rhtml @@ -0,0 +1,31 @@ +

<%=l(:label_report_plural)%>

+ +
+

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

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

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

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

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

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

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

+<%= render :partial => 'simple', :locals => { :data => @issues_by_author, :field_name => "author_id", :rows => @authors } %> +
+
+ +
+

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

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

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

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

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

+<%= render :partial => 'simple', :locals => { :data => @issues_by_category, :field_name => "category_id", :rows => @categories } %> +
+
+ diff --git a/groups/app/views/reports/issue_report_details.rhtml b/groups/app/views/reports/issue_report_details.rhtml new file mode 100644 index 000000000..6b4b42232 --- /dev/null +++ b/groups/app/views/reports/issue_report_details.rhtml @@ -0,0 +1,7 @@ +

<%=l(:label_report_plural)%>

+ +

<%=@report_title%>

+<%= render :partial => 'details', :locals => { :data => @data, :field_name => @field, :rows => @rows } %> +
+<%= link_to l(:button_back), :action => 'issue_report' %> + diff --git a/groups/app/views/repositories/_dir_list.rhtml b/groups/app/views/repositories/_dir_list.rhtml new file mode 100644 index 000000000..5590652f5 --- /dev/null +++ b/groups/app/views/repositories/_dir_list.rhtml @@ -0,0 +1,15 @@ + + + + + + + + + + + + +<%= render :partial => 'dir_list_content' %> + +
<%= l(:field_name) %><%= l(:field_filesize) %><%= l(:label_revision) %><%= l(:label_age) %><%= l(:field_author) %><%= l(:field_comments) %>
diff --git a/groups/app/views/repositories/_dir_list_content.rhtml b/groups/app/views/repositories/_dir_list_content.rhtml new file mode 100644 index 000000000..3564e52ab --- /dev/null +++ b/groups/app/views/repositories/_dir_list_content.rhtml @@ -0,0 +1,32 @@ +<% @entries.each do |entry| %> +<% tr_id = Digest::MD5.hexdigest(entry.path) + depth = params[:depth].to_i %> + + +<%= if entry.is_dir? + link_to_remote h(entry.name), + {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id}, + :update => { :success => tr_id }, + :position => :after, + :success => "scmEntryLoaded('#{tr_id}')", + :condition => "scmEntryClick('#{tr_id}')" + }, + {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}), + :class => ('icon icon-folder'), + :style => "margin-left: #{18 * depth}px;" + } +else + link_to h(entry.name), + {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev}, + :class => 'icon icon-file', + :style => "margin-left: #{18 * depth}px;" +end %> + +<%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %> +<%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> +<%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %> +<%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %> +<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %> +<%=h truncate(changeset.comments, 50) unless changeset.nil? %> + +<% end %> diff --git a/groups/app/views/repositories/_navigation.rhtml b/groups/app/views/repositories/_navigation.rhtml new file mode 100644 index 000000000..b7ac989bc --- /dev/null +++ b/groups/app/views/repositories/_navigation.rhtml @@ -0,0 +1,21 @@ +<%= link_to 'root', :action => 'browse', :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 => 'browse', :id => @project, :path => link_path, :rev => @rev %> +<% end %> +<% if filename %> + / <%= link_to h(filename), :action => 'changes', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %> +<% end %> + +<%= "@ #{revision}" if revision %> + +<% html_title(with_leading_slash(path)) -%> diff --git a/groups/app/views/repositories/_revisions.rhtml b/groups/app/views/repositories/_revisions.rhtml new file mode 100644 index 000000000..1bcf0208c --- /dev/null +++ b/groups/app/views/repositories/_revisions.rhtml @@ -0,0 +1,28 @@ +<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => path}, :method => :get) do %> + + + + + + + + + + +<% show_diff = entry && entry.is_file? && revisions.size > 1 %> +<% line_num = 1 %> +<% revisions.each do |changeset| %> + + + + + + + + +<% line_num += 1 %> +<% end %> + +
#<%= l(:label_date) %><%= l(:field_author) %><%= l(:field_comments) %>
<%= link_to format_revision(changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %><%= 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) %><%= 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) %><%= format_time(changeset.committed_on) %><%=h changeset.committer.to_s.split('<').first %><%= textilizable(changeset.comments) %>
+<%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %> +<% end %> diff --git a/groups/app/views/repositories/annotate.rhtml b/groups/app/views/repositories/annotate.rhtml new file mode 100644 index 000000000..b5669ef76 --- /dev/null +++ b/groups/app/views/repositories/annotate.rhtml @@ -0,0 +1,28 @@ +

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

+ +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + +
+ + + <% line_num = 1 %> + <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %> + <% revision = @annotate.revisions[line_num-1] %> + + + + + + + <% line_num += 1 %> + <% end %> + +
<%= line_num %> + <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %><%= h(revision.author.to_s.split('<').first) if revision %>
<%= line %>
+
+ +<% html_title(l(:button_annotate)) -%> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/groups/app/views/repositories/browse.rhtml b/groups/app/views/repositories/browse.rhtml new file mode 100644 index 000000000..868388f11 --- /dev/null +++ b/groups/app/views/repositories/browse.rhtml @@ -0,0 +1,13 @@ +
+<% form_tag do %> +<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<% end %> +
+ +

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

+ +<%= render :partial => 'dir_list' %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> diff --git a/groups/app/views/repositories/changes.rhtml b/groups/app/views/repositories/changes.rhtml new file mode 100644 index 000000000..2d7462b29 --- /dev/null +++ b/groups/app/views/repositories/changes.rhtml @@ -0,0 +1,18 @@ +

<%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %>

+ +

<%=h @entry.name %>

+ +

+<% if @repository.supports_cat? %> + <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> | +<% end %> +<% if @repository.supports_annotate? %> + <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> | +<% end %> +<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %> +<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %> +

+ +<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%> + +<% html_title(l(:label_change_plural)) -%> diff --git a/groups/app/views/repositories/diff.rhtml b/groups/app/views/repositories/diff.rhtml new file mode 100644 index 000000000..eaef1abf5 --- /dev/null +++ b/groups/app/views/repositories/diff.rhtml @@ -0,0 +1,95 @@ +

<%= l(:label_revision) %> <%= format_revision(@rev) %> <%= @path.gsub(/^.*\//, '') %>

+ + +<% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %> + <% params.each do |k, p| %> + <% if k != "type" %> + <%= hidden_field_tag(k,p) %> + <% end %> + <% end %> +

+ <%= 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()}" %>

+<% end %> + +<% cache(@cache_key) do %> +<% @diff.each do |table_file| %> +
+<% if @diff_type == 'sbs' %> + + + + + + + + + + + + <% table_file.keys.sort.each do |key| %> + + + + + + + <% end %> + +
+ <%= table_file.file_name %> +
@<%= format_revision @rev %>@<%= format_revision @rev_to %>
+ <%= table_file[key].nb_line_left %> + +
<%=to_utf8 table_file[key].line_left %>
+
+ <%= table_file[key].nb_line_right %> + +
<%=to_utf8 table_file[key].line_right %>
+
+ +<% else %> + + + + + + + + + + + + + <% table_file.keys.sort.each do |key, line| %> + + + + <% if table_file[key].line_left.empty? %> + + <% else %> + + <% end %> + + <% end %> + +
+ <%= table_file.file_name %> +
@<%= format_revision @rev %>@<%= format_revision @rev_to %>
+ <%= table_file[key].nb_line_left %> + + <%= table_file[key].nb_line_right %> + +
<%=to_utf8 table_file[key].line_right %>
+
+
<%=to_utf8 table_file[key].line_left %>
+
+<% end %> +
+<% end %> +<% end %> + +<% html_title(with_leading_slash(@path), 'Diff') -%> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> diff --git a/groups/app/views/repositories/entry.rhtml b/groups/app/views/repositories/entry.rhtml new file mode 100644 index 000000000..309da76fc --- /dev/null +++ b/groups/app/views/repositories/entry.rhtml @@ -0,0 +1,17 @@ +

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

+ +
+ + +<% line_num = 1 %> +<% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %> + +<% line_num += 1 %> +<% end %> + +
<%= line_num %>
<%= line %>
+
+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> diff --git a/groups/app/views/repositories/revision.rhtml b/groups/app/views/repositories/revision.rhtml new file mode 100644 index 000000000..f1e176669 --- /dev/null +++ b/groups/app/views/repositories/revision.rhtml @@ -0,0 +1,65 @@ +
+ « + <% 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 do %> + <%= text_field_tag 'rev', @rev, :size => 5 %> + <%= submit_tag 'OK' %> + <% end %> +
+ +

<%= l(:label_revision) %> <%= format_revision(@changeset.revision) %>

+ +

<% if @changeset.scmid %>ID: <%= @changeset.scmid %>
<% end %> +<%= @changeset.committer.to_s.split('<').first %>, <%= format_time(@changeset.committed_on) %>

+ +<%= textilizable @changeset.comments %> + +<% if @changeset.issues.any? %> +

<%= l(:label_related_issues) %>

+
    +<% @changeset.issues.each do |issue| %> +
  • <%= link_to_issue issue %>: <%=h issue.subject %>
  • +<% end %> +
+<% end %> + +

<%= l(:label_attachment_plural) %>

+
+
<%= l(:label_added) %> 
+
<%= l(:label_modified) %> 
+
<%= l(:label_deleted) %> 
+
+

<%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %>

+ + +<% @changes.each do |change| %> + + + + +<% end %> + +
<%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %>
+<% if change.action == "M" %> +<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.path), :rev => @changeset.revision %> +<% end %> +
+

<%= pagination_links_full @changes_pages %>

+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> + +<% html_title("#{l(:label_revision)} #{@changeset.revision}") -%> diff --git a/groups/app/views/repositories/revisions.rhtml b/groups/app/views/repositories/revisions.rhtml new file mode 100644 index 000000000..ac5919dd6 --- /dev/null +++ b/groups/app/views/repositories/revisions.rhtml @@ -0,0 +1,19 @@ +
+<% form_tag({:action => 'revision', :id => @project}) do %> +<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<%= submit_tag 'OK' %> +<% end %> +
+ +

<%= l(:label_revision_plural) %>

+ +<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%> + +

<%= pagination_links_full @changeset_pages,@changeset_count %>

+ +<% 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 %> + +<% html_title(l(:label_revision_plural)) -%> diff --git a/groups/app/views/repositories/show.rhtml b/groups/app/views/repositories/show.rhtml new file mode 100644 index 000000000..469ac063e --- /dev/null +++ b/groups/app/views/repositories/show.rhtml @@ -0,0 +1,25 @@ +
+<%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %> +
+ +

<%= l(:label_repository) %> (<%= @repository.scm_name %>)

+ +<% if !@entries.nil? && authorize_for('repositories', 'browse') %> +

<%= l(:label_browse) %>

+<%= render :partial => 'dir_list' %> +<% end %> + +<% if !@changesets.empty? && authorize_for('repositories', 'revisions') %> +

<%= l(:label_latest_revision_plural) %>

+<%= render :partial => 'revisions', :locals => {:project => @project, :path => '', :revisions => @changesets, :entry => nil }%> +

<%= link_to l(:label_view_revisions), :action => 'revisions', :id => @project %>

+<% 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 %> +<% end %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> + +<% html_title(l(:label_repository)) -%> diff --git a/groups/app/views/repositories/stats.rhtml b/groups/app/views/repositories/stats.rhtml new file mode 100644 index 000000000..76ce892d5 --- /dev/null +++ b/groups/app/views/repositories/stats.rhtml @@ -0,0 +1,13 @@ +

<%= l(:label_statistics) %>

+ + + +
+<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %> + +<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %> +
+
+

<%= link_to l(:button_back), :action => 'show', :id => @project %>

+ +<% html_title(l(:label_repository), l(:label_statistics)) -%> diff --git a/groups/app/views/roles/_form.rhtml b/groups/app/views/roles/_form.rhtml new file mode 100644 index 000000000..58dc2af41 --- /dev/null +++ b/groups/app/views/roles/_form.rhtml @@ -0,0 +1,29 @@ +<%= error_messages_for 'role' %> + +<% unless @role.builtin? %> +
+

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

+

<%= f.check_box :assignable %>

+<% if @role.new_record? && @roles.any? %> +

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

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

<%= l(:label_permissions) %>

+
+<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> +
<%= mod.blank? ? l(:label_project) : mod.humanize %> + <% perms_by_module[mod].each do |permission| %> + + <% end %> +
+<% end %> +
<%= check_all_links 'role_form' %> +<%= hidden_field_tag 'role[permissions][]', '' %> +
diff --git a/groups/app/views/roles/edit.rhtml b/groups/app/views/roles/edit.rhtml new file mode 100644 index 000000000..e53a0f545 --- /dev/null +++ b/groups/app/views/roles/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_role)%>: <%= @role.name %>

+ +<% 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 %> diff --git a/groups/app/views/roles/list.rhtml b/groups/app/views/roles/list.rhtml new file mode 100644 index 000000000..93b821387 --- /dev/null +++ b/groups/app/views/roles/list.rhtml @@ -0,0 +1,37 @@ +
+<%= link_to l(:label_role_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_role_plural)%>

+ + + + + + + + +<% for role in @roles %> + "> + + + + +<% end %> + +
<%=l(:label_role)%><%=l(:button_sort)%>
<%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, :action => 'edit', :id => role)) %> + <% unless role.builtin? %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => role, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => role, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => role, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => role, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + <% end %> + + <%= button_to(l(:button_delete), { :action => 'destroy', :id => role }, :confirm => l(:text_are_you_sure), :class => "button-small", :disabled => role.builtin? ) %> +
+ +

<%= pagination_links_full @role_pages %>

+ +

<%= link_to l(:label_permissions_report), :action => 'report' %>

+ +<% html_title(l(:label_role_plural)) -%> diff --git a/groups/app/views/roles/new.rhtml b/groups/app/views/roles/new.rhtml new file mode 100644 index 000000000..8f03aefac --- /dev/null +++ b/groups/app/views/roles/new.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_role_new)%>

+ +<% 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 diff --git a/groups/app/views/roles/report.rhtml b/groups/app/views/roles/report.rhtml new file mode 100644 index 000000000..98c3b651e --- /dev/null +++ b/groups/app/views/roles/report.rhtml @@ -0,0 +1,37 @@ +

<%=l(:label_permissions_report)%>

+ +<% form_tag({:action => 'report'}, :id => 'permissions_form') do %> +<%= hidden_field_tag 'permissions[0]', '' %> + + + + + <% @roles.each do |role| %> + + <% end %> + + + +<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> +<% perms_by_module.keys.sort.each do |mod| %> + <% unless mod.blank? %> + <%= content_tag('th', mod.humanize, :colspan => (@roles.size + 1), :align => 'left') %> + <% end %> + <% perms_by_module[mod].each do |permission| %> + + + <% @roles.each do |role| %> + + <% end %> + + <% end %> +<% end %> + +
<%=l(:label_permissions)%><%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
<%= permission.name.to_s.humanize %> + <% if role.setable_permissions.include? permission %> + <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name) %> + <% end %> +
+

<%= check_all_links 'permissions_form' %>

+

<%= submit_tag l(:button_save) %>

+<% end %> diff --git a/groups/app/views/roles/workflow.rhtml b/groups/app/views/roles/workflow.rhtml new file mode 100644 index 000000000..0f08b0d22 --- /dev/null +++ b/groups/app/views/roles/workflow.rhtml @@ -0,0 +1,58 @@ +

<%=l(:label_workflow)%>

+ +

<%=l(:text_workflow_edit)%>:

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

+ + + + +<%= submit_tag l(:button_edit) %> +

+<% end %> + + + +<% unless @tracker.nil? or @role.nil? %> +<% form_tag({:action => 'workflow', :role_id => @role, :tracker_id => @tracker }, :id => 'workflow_form' ) do %> +
+ + + + + + + + <% for new_status in @statuses %> + + <% end %> + + + <% for old_status in @statuses %> + + + <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to(@role, @tracker).collect(&:id) -%> + <% for new_status in @statuses -%> + + <% end -%> + + <% end %> +
<%=l(:label_current_status)%><%=l(:label_new_statuses_allowed)%>
<%= new_status.name %>
<%= old_status.name %> + > +
+

<%= check_all_links 'workflow_form' %>

+
+<%= submit_tag l(:button_save) %> +<% end %> + +<% end %> + +<% html_title(l(:label_workflow)) -%> diff --git a/groups/app/views/search/index.rhtml b/groups/app/views/search/index.rhtml new file mode 100644 index 000000000..29c604a21 --- /dev/null +++ b/groups/app/views/search/index.rhtml @@ -0,0 +1,45 @@ +

<%= l(:label_search) %>

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

<%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %> +<%= javascript_tag "Field.focus('search-input')" %> + +<% @object_types.each do |t| %> + +<% end %> +
+ + +

+<%= submit_tag l(:button_submit), :name => 'submit' %> +<% end %> +
+ +<% if @results %> +

<%= l(:label_result_plural) %>

+
    + <% @results.each do |e| %> +
  • <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %>
    + <%= highlight_tokens(e.event_description, @tokens) %>
    + <%= format_time(e.event_datetime) %>

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

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

+ +<% html_title(l(:label_search)) -%> diff --git a/groups/app/views/settings/_authentication.rhtml b/groups/app/views/settings/_authentication.rhtml new file mode 100644 index 000000000..6bf20cbce --- /dev/null +++ b/groups/app/views/settings/_authentication.rhtml @@ -0,0 +1,27 @@ +<% form_tag({:action => 'edit', :tab => 'authentication'}) do %> + +
+

+<%= check_box_tag 'settings[login_required]', 1, Setting.login_required? %><%= hidden_field_tag 'settings[login_required]', 0 %>

+ +

+<%= select_tag 'settings[autologin]', options_for_select( [[l(:label_disabled), "0"]] + [1, 7, 30, 365].collect{|days| [lwr(:actionview_datehelper_time_in_words_day, days), days.to_s]}, Setting.autologin) %>

+ +

+<%= 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 ) %>

+ +

+<%= check_box_tag 'settings[lost_password]', 1, Setting.lost_password? %><%= hidden_field_tag 'settings[lost_password]', 0 %>

+
+ +
+ <%= link_to l(:label_ldap_authentication), :controller => 'auth_sources', :action => 'list' %> +
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/settings/_general.rhtml b/groups/app/views/settings/_general.rhtml new file mode 100644 index 000000000..1d17a003e --- /dev/null +++ b/groups/app/views/settings/_general.rhtml @@ -0,0 +1,55 @@ +<% form_tag({:action => 'edit'}) do %> + +
+

+<%= text_field_tag 'settings[app_title]', Setting.app_title, :size => 30 %>

+ +

+<%= text_area_tag 'settings[welcome_text]', Setting.welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %>

+<%= wikitoolbar_for 'settings[welcome_text]' %> + +

+<%= select_tag 'settings[ui_theme]', options_for_select( ([[l(:label_default), '']] + Redmine::Themes.themes.collect {|t| [t.name, t.id]}), Setting.ui_theme) %>

+ +

+<%= select_tag 'settings[default_language]', options_for_select( lang_options_for_select(false), Setting.default_language) %>

+ +

+<%= 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) %>

+ +

+<%= 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) %>

+ +

+<%= select_tag 'settings[user_format]', options_for_select( @options[:user_format], Setting.user_format.to_s ) %>

+ +

+<%= text_field_tag 'settings[attachment_max_size]', Setting.attachment_max_size, :size => 6 %> KB

+ +

+<%= text_field_tag 'settings[per_page_options]', Setting.per_page_options_array.join(', '), :size => 20 %>
<%= l(:text_comma_separated) %>

+ +

+<%= text_field_tag 'settings[activity_days_default]', Setting.activity_days_default, :size => 6 %> <%= l(:label_day_plural) %>

+ +

+<%= text_field_tag 'settings[host_name]', Setting.host_name, :size => 60 %>

+ +

+<%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %>

+ +

+<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], ["textile", "textile"]], Setting.text_formatting) %>

+ +

+<%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %>

+ +

+<%= text_field_tag 'settings[feeds_limit]', Setting.feeds_limit, :size => 6 %>

+ +

+<%= check_box_tag 'settings[default_projects_public]', 1, Setting.default_projects_public? %><%= hidden_field_tag 'settings[default_projects_public]', 0 %>

+
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/settings/_issues.rhtml b/groups/app/views/settings/_issues.rhtml new file mode 100644 index 000000000..bbd7356fd --- /dev/null +++ b/groups/app/views/settings/_issues.rhtml @@ -0,0 +1,23 @@ +<% form_tag({:action => 'edit', :tab => 'issues'}) do %> + +
+

+<%= check_box_tag 'settings[cross_project_issue_relations]', 1, Setting.cross_project_issue_relations? %><%= hidden_field_tag 'settings[cross_project_issue_relations]', 0 %>

+ +

+<%= check_box_tag 'settings[display_subprojects_issues]', 1, Setting.display_subprojects_issues? %><%= hidden_field_tag 'settings[display_subprojects_issues]', 0 %>

+ +

+<%= text_field_tag 'settings[issues_export_limit]', Setting.issues_export_limit, :size => 6 %>

+
+ +
<%= l(:setting_issue_list_default_columns) %> +<%= hidden_field_tag 'settings[issue_list_default_columns][]', '' %> +

<% Query.new.available_columns.each do |column| %> + +<% end %>

+
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/settings/_notifications.rhtml b/groups/app/views/settings/_notifications.rhtml new file mode 100644 index 000000000..ac3213853 --- /dev/null +++ b/groups/app/views/settings/_notifications.rhtml @@ -0,0 +1,30 @@ +<% form_tag({:action => 'edit', :tab => 'notifications'}) do %> + +
+

+<%= text_field_tag 'settings[mail_from]', Setting.mail_from, :size => 60 %>

+ +

+<%= check_box_tag 'settings[bcc_recipients]', 1, Setting.bcc_recipients? %> +<%= hidden_field_tag 'settings[bcc_recipients]', 0 %>

+
+ +
<%=l(:text_select_mail_notifications)%> +<% @notifiables.each do |notifiable| %> +
+<% end %> +<%= hidden_field_tag 'settings[notified_events][]', '' %> +

<%= check_all_links('mail-options-form') %>

+
+ +
<%= l(:setting_emails_footer) %> +<%= text_area_tag 'settings[emails_footer]', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %> +
+ +
+<%= link_to l(:label_send_test_email), :controller => 'admin', :action => 'test_email' %> +
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/settings/_repositories.rhtml b/groups/app/views/settings/_repositories.rhtml new file mode 100644 index 000000000..59b3b51de --- /dev/null +++ b/groups/app/views/settings/_repositories.rhtml @@ -0,0 +1,26 @@ +<% form_tag({:action => 'edit', :tab => 'repositories'}) do %> + +
+

+<%= check_box_tag 'settings[autofetch_changesets]', 1, Setting.autofetch_changesets? %><%= hidden_field_tag 'settings[autofetch_changesets]', 0 %>

+ +

+<%= check_box_tag 'settings[sys_api_enabled]', 1, Setting.sys_api_enabled? %><%= hidden_field_tag 'settings[sys_api_enabled]', 0 %>

+ +

+<%= text_field_tag 'settings[repositories_encodings]', Setting.repositories_encodings, :size => 60 %>
<%= l(:text_comma_separated) %>

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

+<%= text_field_tag 'settings[commit_ref_keywords]', Setting.commit_ref_keywords, :size => 30 %>
<%= l(:text_comma_separated) %>

+ +

+<%= 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) %> +
<%= l(:text_comma_separated) %>

+
+ +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/settings/edit.rhtml b/groups/app/views/settings/edit.rhtml new file mode 100644 index 000000000..c99a13960 --- /dev/null +++ b/groups/app/views/settings/edit.rhtml @@ -0,0 +1,23 @@ +

<%= l(:label_settings) %>

+ +<% selected_tab = params[:tab] ? params[:tab].to_s : administration_settings_tabs.first[:name] %> + +
+
    +<% administration_settings_tabs.each do |tab| -%> +
  • <%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %>
  • +<% end -%> +
+
+ +<% administration_settings_tabs.each do |tab| -%> +<%= content_tag('div', render(:partial => tab[:partial]), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> + +<% html_title(l(:label_settings), l(:label_administration)) -%> diff --git a/groups/app/views/settings/plugin.rhtml b/groups/app/views/settings/plugin.rhtml new file mode 100644 index 000000000..61913484d --- /dev/null +++ b/groups/app/views/settings/plugin.rhtml @@ -0,0 +1,10 @@ +

<%= l(:label_settings) %>: <%=h @plugin.name %>

+ +
+<% form_tag({:action => 'plugin'}) do %> +
+<%= render :partial => @partial, :locals => {:settings => @settings}%> +
+<%= submit_tag l(:button_apply) %> +<% end %> +
diff --git a/groups/app/views/timelog/_date_range.rhtml b/groups/app/views/timelog/_date_range.rhtml new file mode 100644 index 000000000..ed84b16cf --- /dev/null +++ b/groups/app/views/timelog/_date_range.rhtml @@ -0,0 +1,28 @@ +
<%= l(:label_date_range) %> +

+<%= 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;' %> +

+

+<%= radio_button_tag 'period_type', '2', @free_period %> + +<%= l(:label_date_from) %> +<%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %> +<%= l(:label_date_to) %> +<%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %> + +<%= submit_tag l(:button_apply), :name => nil %> +

+
+ +
+<% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %> +
    +
  • <%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project }), + :class => (@controller.action_name == 'details' ? 'selected' : nil)) %>
  • +
  • <%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project}), + :class => (@controller.action_name == 'report' ? 'selected' : nil)) %>
  • +
+
diff --git a/groups/app/views/timelog/_list.rhtml b/groups/app/views/timelog/_list.rhtml new file mode 100644 index 000000000..189f4f5e8 --- /dev/null +++ b/groups/app/views/timelog/_list.rhtml @@ -0,0 +1,41 @@ + + + +<%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %> +<%= sort_header_tag('user_id', :caption => l(:label_member)) %> +<%= sort_header_tag('activity_id', :caption => l(:label_activity)) %> +<%= sort_header_tag("#{Project.table_name}.name", :caption => l(:label_project)) %> +<%= sort_header_tag('issue_id', :caption => l(:label_issue), :default_order => 'desc') %> + +<%= sort_header_tag('hours', :caption => l(:field_hours)) %> + + + + +<% entries.each do |entry| -%> +"> + + + + + + + + + +<% end -%> + +
<%= l(:field_comments) %>
<%= format_date(entry.spent_on) %><%=h entry.user %><%=h entry.activity %><%=h entry.project %> +<% if entry.issue -%> +<%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%> +<% end -%> +<%=h entry.comments %><%= html_hours("%.2f" % entry.hours) %> +<% if entry.editable_by?(User.current) -%> + <%= 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 -%> +
diff --git a/groups/app/views/timelog/_report_criteria.rhtml b/groups/app/views/timelog/_report_criteria.rhtml new file mode 100644 index 000000000..94f3d20f9 --- /dev/null +++ b/groups/app/views/timelog/_report_criteria.rhtml @@ -0,0 +1,19 @@ +<% @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? -%> + +<%= '' * level %> +<%= format_criteria_value(criterias[level], value) %> +<%= '' * (criterias.length - level - 1) -%> + <% total = 0 -%> + <% @periods.each do |period| -%> + <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%> + <%= html_hours("%.2f" % sum) if sum > 0 %> + <% end -%> + <%= html_hours("%.2f" % total) if total > 0 %> + +<% if criterias.length > level+1 -%> + <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %> +<% end -%> + +<% end %> diff --git a/groups/app/views/timelog/details.rhtml b/groups/app/views/timelog/details.rhtml new file mode 100644 index 000000000..f02da9959 --- /dev/null +++ b/groups/app/views/timelog/details.rhtml @@ -0,0 +1,31 @@ +
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %> +
+ +

<%= l(:label_spent_time) %>

+ +<% if @issue %> +

<%= link_to(@project.name, {:action => 'details', :project_id => @project}) %> / <%= link_to_issue(@issue) %>

+<% end %> + +<% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %> +<%= hidden_field_tag 'project_id', params[:project_id] %> +<%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %> +<%= render :partial => 'date_range' %> +<% end %> + +
+

<%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %>

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

<%= pagination_links_full @entry_pages, @entry_count %>

+ +

+<%= l(:label_export_to) %> +<%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %> +

+<% end %> + +<% html_title l(:label_spent_time), l(:label_details) %> diff --git a/groups/app/views/timelog/edit.rhtml b/groups/app/views/timelog/edit.rhtml new file mode 100644 index 000000000..f9dae8a99 --- /dev/null +++ b/groups/app/views/timelog/edit.rhtml @@ -0,0 +1,17 @@ +

<%= l(:label_spent_time) %>

+ +<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %> +<%= error_messages_for 'time_entry' %> +<%= back_url_hidden_field_tag %> + +
+

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

+

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

+

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

+

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

+

<%= f.select :activity_id, (@activities.collect {|p| [p.name, p.id]}), :required => true %>

+
+ +<%= submit_tag l(:button_save) %> + +<% end %> diff --git a/groups/app/views/timelog/report.rhtml b/groups/app/views/timelog/report.rhtml new file mode 100644 index 000000000..97251bc11 --- /dev/null +++ b/groups/app/views/timelog/report.rhtml @@ -0,0 +1,72 @@ +
+<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %> +
+ +

<%= l(:label_spent_time) %>

+ +<% form_remote_tag(:url => {}, :update => 'content') do %> + <% @criterias.each do |criteria| %> + <%= hidden_field_tag 'criterias[]', criteria, :id => nil %> + <% end %> + <%= hidden_field_tag 'project_id', params[:project_id] %> + <%= render :partial => 'date_range' %> + +

<%= 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(@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}, + :update => 'content' + }, :class => 'icon icon-reload' %>

+<% end %> + +<% unless @criterias.empty? %> +
+

<%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %>

+
+ +<% unless @hours.empty? %> + + + +<% @criterias.each do |criteria| %> + +<% end %> +<% columns_width = (40 / (@periods.length+1)).to_i %> +<% @periods.each do |period| %> + +<% end %> + + + + +<%= render :partial => 'report_criteria', :locals => {:criterias => @criterias, :hours => @hours, :level => 0} %> + + + <%= '' * (@criterias.size - 1) %> + <% total = 0 -%> + <% @periods.each do |period| -%> + <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%> + + <% end -%> + + + +
<%= l(@available_criterias[criteria][:label]) %><%= period %><%= l(:label_total) %>
<%= l(:label_total) %><%= html_hours("%.2f" % sum) if sum > 0 %><%= html_hours("%.2f" % total) if total > 0 %>
+ +

+<%= l(:label_export_to) %> +<%= link_to 'CSV', params.merge({:format => 'csv'}), :class => 'csv' %> +

+<% end %> +<% end %> + +<% html_title l(:label_spent_time), l(:label_report) %> + diff --git a/groups/app/views/trackers/_form.rhtml b/groups/app/views/trackers/_form.rhtml new file mode 100644 index 000000000..856b70bbc --- /dev/null +++ b/groups/app/views/trackers/_form.rhtml @@ -0,0 +1,12 @@ +<%= error_messages_for 'tracker' %> +
+ +

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

+

<%= f.check_box :is_in_chlog %>

+

<%= f.check_box :is_in_roadmap %>

+<% if @tracker.new_record? && @trackers.any? %> +

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

+<% end %> + +
diff --git a/groups/app/views/trackers/edit.rhtml b/groups/app/views/trackers/edit.rhtml new file mode 100644 index 000000000..d8411099c --- /dev/null +++ b/groups/app/views/trackers/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_tracker)%>

+ +<% labelled_tabular_form_for :tracker, @tracker, :url => { :action => 'edit' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> \ No newline at end of file diff --git a/groups/app/views/trackers/list.rhtml b/groups/app/views/trackers/list.rhtml new file mode 100644 index 000000000..9ccfda386 --- /dev/null +++ b/groups/app/views/trackers/list.rhtml @@ -0,0 +1,35 @@ +
+<%= link_to l(:label_tracker_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_tracker_plural)%>

+ + + + + + + + + +<% for tracker in @trackers %> + "> + + + + + +<% end %> + +
<%=l(:label_tracker)%><%=l(:button_sort)%>
<%= link_to tracker.name, :action => 'edit', :id => tracker %><% unless tracker.workflows.count > 0 %><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => "roles", :action => "workflow", :tracker_id => tracker} %>)<% end %> + <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => tracker, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> + <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => tracker, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> - + <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => tracker, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> + <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => tracker, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + + <%= button_to l(:button_delete), { :action => 'destroy', :id => tracker }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +

<%= pagination_links_full @tracker_pages %>

+ +<% html_title(l(:label_tracker_plural)) -%> diff --git a/groups/app/views/trackers/new.rhtml b/groups/app/views/trackers/new.rhtml new file mode 100644 index 000000000..b318a5dc4 --- /dev/null +++ b/groups/app/views/trackers/new.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_tracker_new)%>

+ +<% labelled_tabular_form_for :tracker, @tracker, :url => { :action => 'new' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/groups/app/views/users/_form.rhtml b/groups/app/views/users/_form.rhtml new file mode 100644 index 000000000..ff4278c1f --- /dev/null +++ b/groups/app/views/users/_form.rhtml @@ -0,0 +1,32 @@ +<%= error_messages_for 'user' %> + + +
+

<%=l(:label_information_plural)%>

+

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

+

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

+

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

+

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

+

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

+ +<% for @custom_value in @custom_values %> +

<%= custom_field_tag_with_label @custom_value %>

+<% end if @custom_values%> + +

<%= f.check_box :admin %>

+
+ +
+

<%=l(:label_authentication)%>

+<% unless @auth_sources.empty? %> +

<%= 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');}" %>

+<% end %> +
+

+<%= password_field_tag 'password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

+

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

+
+
+ diff --git a/groups/app/views/users/_memberships.rhtml b/groups/app/views/users/_memberships.rhtml new file mode 100644 index 000000000..2499ba387 --- /dev/null +++ b/groups/app/views/users/_memberships.rhtml @@ -0,0 +1,29 @@ +
+

<%= l(:label_project_plural) %>

+ +<% @user.memberships.each do |membership| %> +<% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }, :class => "tabular") do %> +

+ + + <%= submit_tag l(:button_change), :class => "button-small" %> + <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> +

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

+
+<% form_tag({ :action => 'edit_membership', :id => @user }) do %> + + +<%= submit_tag l(:button_add) %> +<% end %> +

+
\ No newline at end of file diff --git a/groups/app/views/users/add.rhtml b/groups/app/views/users/add.rhtml new file mode 100644 index 000000000..636bdcbcd --- /dev/null +++ b/groups/app/views/users/add.rhtml @@ -0,0 +1,7 @@ +

<%=l(:label_user_new)%>

+ +<% labelled_tabular_form_for :user, @user, :url => { :action => "add" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %> +<% end %> diff --git a/groups/app/views/users/edit.rhtml b/groups/app/views/users/edit.rhtml new file mode 100644 index 000000000..0da99d0d2 --- /dev/null +++ b/groups/app/views/users/edit.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_user)%>

+ +<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> + +<%= render :partial => 'memberships' %> \ No newline at end of file diff --git a/groups/app/views/users/list.rhtml b/groups/app/views/users/list.rhtml new file mode 100644 index 000000000..d89672d19 --- /dev/null +++ b/groups/app/views/users/list.rhtml @@ -0,0 +1,44 @@ +
+<%= link_to l(:label_user_new), {:action => 'add'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_user_plural)%>

+ +<% form_tag({}, :method => :get) do %> +
<%= l(:label_filter_plural) %> + +<%= select_tag 'status', status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> +
+<% end %> +  + + + + <%= sort_header_tag('login', :caption => l(:field_login)) %> + <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %> + <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %> + <%= sort_header_tag('mail', :caption => l(:field_mail)) %> + <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %> + + + +<% for user in @users -%> + <%= %w(anon active registered locked)[user.status] %>"> + + + + + + + + + +<% end -%> + +
<%= link_to user.login, :action => 'edit', :id => user %><%= user.firstname %><%= user.lastname %><%= image_tag('true.png') if user.admin? %><%= format_time(user.created_on) %><%= change_status_link(user) %>
+ +

<%= pagination_links_full @user_pages, @user_count %>

+ +<% html_title(l(:label_user_plural)) -%> diff --git a/groups/app/views/versions/_form.rhtml b/groups/app/views/versions/_form.rhtml new file mode 100644 index 000000000..adc83b573 --- /dev/null +++ b/groups/app/views/versions/_form.rhtml @@ -0,0 +1,8 @@ +<%= error_messages_for 'version' %> + +
+

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

+

<%= f.text_field :description, :size => 60 %>

+

<%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %>

+

<%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %>

+
diff --git a/groups/app/views/versions/_issue_counts.rhtml b/groups/app/views/versions/_issue_counts.rhtml new file mode 100644 index 000000000..4bab5c659 --- /dev/null +++ b/groups/app/views/versions/_issue_counts.rhtml @@ -0,0 +1,35 @@ +
+
+ +<%= 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')"))) %> + +<% if counts.empty? %> +

<%= l(:label_no_data) %>

+<% else %> + + <% counts.each do |count| %> + + + + + <% end %> +
+ <%= link_to count[:group], {:controller => 'issues', + :action => 'index', + :project_id => version.project, + :set_filter => 1, + :fixed_version_id => version, + "#{criteria}_id" => count[:group]} %> + + <%= progress_bar((count[:closed].to_f / count[:total])*100, + :legend => "#{count[:closed]}/#{count[:total]}", + :width => "#{(count[:total].to_f / max * 200).floor}px;") %> +
+<% end %> +
+
diff --git a/groups/app/views/versions/_overview.rhtml b/groups/app/views/versions/_overview.rhtml new file mode 100644 index 000000000..d3aa6b18f --- /dev/null +++ b/groups/app/views/versions/_overview.rhtml @@ -0,0 +1,24 @@ +<% if version.completed? %> +

<%= format_date(version.effective_date) %>

+<% elsif version.overdue? %> +

<%= l(:label_roadmap_overdue, distance_of_time_in_words(Time.now, version.effective_date)) %> (<%= format_date(version.effective_date) %>)

+<% elsif version.effective_date %> +

<%=l(:label_roadmap_due_in)%> <%= distance_of_time_in_words Time.now, version.effective_date %> (<%= format_date(version.effective_date) %>)

+<% end %> + +

<%=h version.description %>

+ +<% if version.fixed_issues.count > 0 %> + <%= progress_bar([version.closed_pourcent, version.completed_pourcent], :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %> +

+ <%= link_to(version.closed_issues_count, :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %> + <%= lwr(:label_closed_issues, version.closed_issues_count) %> + (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%) +   + <%= link_to(version.open_issues_count, :controller => 'issues', :action => 'index', :project_id => version.project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %> + <%= lwr(:label_open_issues, version.open_issues_count)%> + (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%) +

+<% else %> +

<%= l(:label_roadmap_no_issues) %>

+<% end %> diff --git a/groups/app/views/versions/edit.rhtml b/groups/app/views/versions/edit.rhtml new file mode 100644 index 000000000..1556ebba1 --- /dev/null +++ b/groups/app/views/versions/edit.rhtml @@ -0,0 +1,7 @@ +

<%=l(:label_version)%>

+ +<% labelled_tabular_form_for :version, @version, :url => { :action => 'edit' } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> + diff --git a/groups/app/views/versions/show.rhtml b/groups/app/views/versions/show.rhtml new file mode 100644 index 000000000..7f81cf503 --- /dev/null +++ b/groups/app/views/versions/show.rhtml @@ -0,0 +1,48 @@ +
+<%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %> +
+ +

<%= h(@version.name) %>

+ +
+<% if @version.estimated_hours > 0 || User.current.allowed_to?(:view_time_entries, @project) %> +
<%= l(:label_time_tracking) %> + + + + + +<% if User.current.allowed_to?(:view_time_entries, @project) %> + + + + +<% end %> +
<%= l(:field_estimated_hours) %><%= html_hours(lwr(:label_f_hour, @version.estimated_hours)) %>
<%= l(:label_spent_time) %><%= html_hours(lwr(:label_f_hour, @version.spent_hours)) %>
+
+<% end %> + +
+<%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %> +
+
+ +
+<%= 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], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %> +<% if issues.size > 0 %> + +<% end %> +
+ +<% html_title @version.name %> diff --git a/groups/app/views/welcome/index.rhtml b/groups/app/views/welcome/index.rhtml new file mode 100644 index 000000000..5da5a1ed3 --- /dev/null +++ b/groups/app/views/welcome/index.rhtml @@ -0,0 +1,33 @@ +

<%= l(:label_home) %>

+ +
+ <%= textilizable Setting.welcome_text %> + <% if @news.any? %> +
+

<%=l(:label_news_latest)%>

+ <%= render :partial => 'news/news', :collection => @news %> + <%= link_to l(:label_news_view_all), :controller => 'news' %> +
+ <% end %> +
+ +
+
+

<%=l(:label_project_latest)%>

+
    + <% for project in @projects %> +
  • + <%= link_to project.name, :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>) + <%= textilizable project.short_description, :project => project %> +
  • + <% end %> +
+
+
+ +<% 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 %> diff --git a/groups/app/views/wiki/_content.rhtml b/groups/app/views/wiki/_content.rhtml new file mode 100644 index 000000000..421d26cbb --- /dev/null +++ b/groups/app/views/wiki/_content.rhtml @@ -0,0 +1,3 @@ +
+ <%= textilizable content, :text, :attachments => content.page.attachments %> +
diff --git a/groups/app/views/wiki/_sidebar.rhtml b/groups/app/views/wiki/_sidebar.rhtml new file mode 100644 index 000000000..20c087123 --- /dev/null +++ b/groups/app/views/wiki/_sidebar.rhtml @@ -0,0 +1,5 @@ +

<%= l(:label_wiki) %>

+ +<%= link_to l(:field_start_page), {:action => 'index', :page => nil} %>
+<%= link_to l(:label_index_by_title), {:action => 'special', :page => 'Page_index'} %>
+<%= link_to l(:label_index_by_date), {:action => 'special', :page => 'Date_index'} %>
diff --git a/groups/app/views/wiki/annotate.rhtml b/groups/app/views/wiki/annotate.rhtml new file mode 100644 index 000000000..1c683404b --- /dev/null +++ b/groups/app/views/wiki/annotate.rhtml @@ -0,0 +1,32 @@ +
+<%= 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') %> +
+ +

<%= @page.pretty_title %>

+ +

+<%= l(:label_version) %> <%= link_to @annotate.content.version, :action => 'index', :page => @page.title, :version => @annotate.content.version %> +(<%= @annotate.content.author ? @annotate.content.author.name : "anonyme" %>, <%= format_time(@annotate.content.updated_on) %>) +

+ +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + + + +<% line_num = 1 %> +<% @annotate.lines.each do |line| -%> + + + + + + +<% line_num += 1 %> +<% end -%> + +
<%= line_num %><%= link_to line[0], :controller => 'wiki', :action => 'index', :id => @project, :page => @page.title, :version => line[0] %><%= h(line[1]) %>
<%= line[2] %>
+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/groups/app/views/wiki/diff.rhtml b/groups/app/views/wiki/diff.rhtml new file mode 100644 index 000000000..512d41082 --- /dev/null +++ b/groups/app/views/wiki/diff.rhtml @@ -0,0 +1,18 @@ +
+<%= 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') %> +
+ +

<%= @page.pretty_title %>

+ +

+<%= l(:label_version) %> <%= link_to @diff.content_from.version, :action => 'index', :page => @page.title, :version => @diff.content_from.version %> +(<%= @diff.content_from.author ? @diff.content_from.author.name : "anonyme" %>, <%= format_time(@diff.content_from.updated_on) %>) +→ +<%= l(:label_version) %> <%= link_to @diff.content_to.version, :action => 'index', :page => @page.title, :version => @diff.content_to.version %>/<%= @page.content.version %> +(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>) +

+ +
+ +<%= html_diff(@diff) %> diff --git a/groups/app/views/wiki/edit.rhtml b/groups/app/views/wiki/edit.rhtml new file mode 100644 index 000000000..19f3bd5ae --- /dev/null +++ b/groups/app/views/wiki/edit.rhtml @@ -0,0 +1,26 @@ +

<%= @page.pretty_title %>

+ +<% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %> +<%= f.hidden_field :version %> +<%= error_messages_for 'content' %> + +

<%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>

+


<%= f.text_field :comments, :size => 120 %>

+

<%= 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) %>

+<%= wikitoolbar_for 'content_text' %> +<% end %> + +
+ +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% html_title @page.pretty_title %> diff --git a/groups/app/views/wiki/export.rhtml b/groups/app/views/wiki/export.rhtml new file mode 100644 index 000000000..1ab5c13e4 --- /dev/null +++ b/groups/app/views/wiki/export.rhtml @@ -0,0 +1,14 @@ + + + +<%=h @page.pretty_title %> + + + + +<%= textilizable @content, :text, :wiki_links => :local %> + + diff --git a/groups/app/views/wiki/export_multiple.rhtml b/groups/app/views/wiki/export_multiple.rhtml new file mode 100644 index 000000000..6f6c603ad --- /dev/null +++ b/groups/app/views/wiki/export_multiple.rhtml @@ -0,0 +1,27 @@ + + + +<%=h @wiki.project.name %> + + + + + +<%= l(:label_index_by_title) %> + + +<% @pages.each do |page| %> +
+ +<%= textilizable page.content ,:text, :wiki_links => :anchor %> +<% end %> + + + diff --git a/groups/app/views/wiki/history.rhtml b/groups/app/views/wiki/history.rhtml new file mode 100644 index 000000000..6462e9fdd --- /dev/null +++ b/groups/app/views/wiki/history.rhtml @@ -0,0 +1,35 @@ +

<%= @page.pretty_title %>

+ +

<%= l(:label_history) %>

+ +<% form_tag({:action => "diff"}, :method => :get) do %> + + + + + + + + + + + +<% show_diff = @versions.size > 1 %> +<% line_num = 1 %> +<% @versions.each do |ver| %> +"> + + + + + + + +<% line_num += 1 %> +<% end %> + +
#<%= l(:field_updated_on) %><%= l(:field_author) %><%= l(:field_comments) %>
<%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %> + <%= 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) %><%= 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) %><%= format_time(ver.updated_on) %><%= ver.author ? ver.author.name : "anonyme" %><%=h ver.comments %><%= link_to l(:button_annotate), :action => 'annotate', :page => @page.title, :version => ver.version %>
+<%= submit_tag l(:label_view_diff), :class => 'small' %> +<%= pagination_links_full @version_pages, @version_count, :page_param => :p %> +<% end %> diff --git a/groups/app/views/wiki/rename.rhtml b/groups/app/views/wiki/rename.rhtml new file mode 100644 index 000000000..0c069f43d --- /dev/null +++ b/groups/app/views/wiki/rename.rhtml @@ -0,0 +1,11 @@ +

<%= l(:button_rename) %>: <%= @original_title %>

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

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

+

<%= f.check_box :redirect_existing_links %>

+
+<%= submit_tag l(:button_rename) %> +<% end %> diff --git a/groups/app/views/wiki/show.rhtml b/groups/app/views/wiki/show.rhtml new file mode 100644 index 000000000..e4413d090 --- /dev/null +++ b/groups/app/views/wiki/show.rhtml @@ -0,0 +1,53 @@ +
+<%= 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 %> +<%= 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 %> +<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> +
+ +<% if @content.version != @page.content.version %> +

+ <%= 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) %> +
+ <%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %>
+ <%=h @content.comments %> +

+
+<% end %> + +<%= render(:partial => "wiki/content", :locals => {:content => @content}) %> + +<%= link_to_attachments @page.attachments, :delete_url => (authorize_for('wiki', 'destroy_attachment') ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %> + +<% if authorize_for('wiki', 'add_attachment') %> +

<%= 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' %>

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

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

+
+<%= submit_tag l(:button_add) %> +<%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %> +<% end %> +<% end %> + +

+<%= l(:label_export_to) %> +<%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'html' %> +<%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'text' %> +

+ +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% html_title @page.pretty_title %> diff --git a/groups/app/views/wiki/special_date_index.rhtml b/groups/app/views/wiki/special_date_index.rhtml new file mode 100644 index 000000000..6717ebc85 --- /dev/null +++ b/groups/app/views/wiki/special_date_index.rhtml @@ -0,0 +1,30 @@ +

<%= l(:label_index_by_date) %>

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

<%= l(:label_no_data) %>

+<% end %> + +<% @pages_by_date.keys.sort.reverse.each do |date| %> +

<%= format_date(date) %>

+
    +<% @pages_by_date[date].each do |page| %> +
  • <%= link_to page.pretty_title, :action => 'index', :page => page.title %>
  • +<% end %> +
+<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% unless @pages.empty? %> +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +<%= link_to 'HTML', {:action => 'special', :page => 'export'}, :class => 'html' %> +

+<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %> +<% end %> diff --git a/groups/app/views/wiki/special_page_index.rhtml b/groups/app/views/wiki/special_page_index.rhtml new file mode 100644 index 000000000..f21cc3423 --- /dev/null +++ b/groups/app/views/wiki/special_page_index.rhtml @@ -0,0 +1,27 @@ +

<%= l(:label_index_by_title) %>

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

<%= l(:label_no_data) %>

+<% end %> + +
    <% @pages.each do |page| %> +
  • <%= link_to page.pretty_title, {:action => 'index', :page => page.title}, + :title => l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) %> +
  • +<% end %>
+ +<% content_for :sidebar do %> + <%= render :partial => 'sidebar' %> +<% end %> + +<% unless @pages.empty? %> +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +<%= link_to 'HTML', {:action => 'special', :page => 'export'} %> +

+<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %> +<% end %> diff --git a/groups/app/views/wikis/destroy.rhtml b/groups/app/views/wikis/destroy.rhtml new file mode 100644 index 000000000..b5b1de114 --- /dev/null +++ b/groups/app/views/wikis/destroy.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_confirmation)%>

+ +
+

<%= @project.name %>
<%=l(:text_wiki_destroy_confirmation)%>

+ +<% form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %> +<%= hidden_field_tag "confirm", 1 %> +<%= submit_tag l(:button_delete) %> +<% end %> +
diff --git a/groups/config/boot.rb b/groups/config/boot.rb new file mode 100644 index 000000000..9fcd50fe3 --- /dev/null +++ b/groups/config/boot.rb @@ -0,0 +1,19 @@ +# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb + +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + unless RUBY_PLATFORM =~ /mswin32/ + require 'pathname' + root_path = Pathname.new(root_path).cleanpath(true).to_s + end + RAILS_ROOT = root_path +end + +if File.directory?("#{RAILS_ROOT}/vendor/rails") + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" +else + require 'rubygems' + require 'initializer' +end + +Rails::Initializer.run(:set_load_path) diff --git a/groups/config/database.yml.example b/groups/config/database.yml.example new file mode 100644 index 000000000..f72844a07 --- /dev/null +++ b/groups/config/database.yml.example @@ -0,0 +1,44 @@ +# MySQL (default setup). Versions 4.1 and 5.0 are recommended. +# +# Get the fast C bindings: +# gem install mysql +# (on OS X: gem install mysql -- --include=/usr/local/lib) +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html + +production: + adapter: mysql + database: redmine + host: localhost + username: root + password: + +development: + adapter: mysql + database: redmine_development + host: localhost + username: root + password: + +test: + adapter: mysql + database: redmine_test + host: localhost + username: root + password: + +test_pgsql: + adapter: postgresql + database: redmine_test + host: localhost + username: postgres + password: "postgres" + +test_sqlite3: + adapter: sqlite3 + dbfile: db/test.db + +demo: + adapter: sqlite3 + dbfile: db/demo.db + diff --git a/groups/config/environment.rb b/groups/config/environment.rb new file mode 100644 index 000000000..7878eca47 --- /dev/null +++ b/groups/config/environment.rb @@ -0,0 +1,102 @@ +# 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.0.2' 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 + + # Use the database for sessions instead of the file system + # (create the session table with 'rake create_sessions_table') + # config.action_controller.session_store = :active_record_store + config.action_controller.session_store = :PStore + + # 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 + + # 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 + + # See Rails::Configuration for more options + + # SMTP server configuration + config.action_mailer.smtp_settings = { + :address => "127.0.0.1", + :port => 25, + :domain => "somenet.foo", + :authentication => :login, + :user_name => "redmine@somenet.foo", + :password => "redmine", + } + + config.action_mailer.perform_deliveries = true + + # Tell ActionMailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + #config.action_mailer.delivery_method = :test + config.action_mailer.delivery_method = :smtp + +end + +ActiveRecord::Errors.default_error_messages = { + :inclusion => "activerecord_error_inclusion", + :exclusion => "activerecord_error_exclusion", + :invalid => "activerecord_error_invalid", + :confirmation => "activerecord_error_confirmation", + :accepted => "activerecord_error_accepted", + :empty => "activerecord_error_empty", + :blank => "activerecord_error_blank", + :too_long => "activerecord_error_too_long", + :too_short => "activerecord_error_too_short", + :wrong_length => "activerecord_error_wrong_length", + :taken => "activerecord_error_taken", + :not_a_number => "activerecord_error_not_a_number" +} + +ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" } + +Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV) +Mime::Type.register 'application/pdf', :pdf + +GLoc.set_config :default_language => :en +GLoc.clear_strings +GLoc.set_kcode +GLoc.load_localized_strings +GLoc.set_config(:raise_string_not_found_errors => false) + +require 'redmine' + diff --git a/groups/config/environments/demo.rb b/groups/config/environments/demo.rb new file mode 100644 index 000000000..c7e997e8d --- /dev/null +++ b/groups/config/environments/demo.rb @@ -0,0 +1,21 @@ +# 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 + diff --git a/groups/config/environments/development.rb b/groups/config/environments/development.rb new file mode 100644 index 000000000..c816f03e3 --- /dev/null +++ b/groups/config/environments/development.rb @@ -0,0 +1,16 @@ +# 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 diff --git a/groups/config/environments/production.rb b/groups/config/environments/production.rb new file mode 100644 index 000000000..cfd2aa0f2 --- /dev/null +++ b/groups/config/environments/production.rb @@ -0,0 +1,22 @@ +# 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 diff --git a/groups/config/environments/test.rb b/groups/config/environments/test.rb new file mode 100644 index 000000000..9ba9ae0f8 --- /dev/null +++ b/groups/config/environments/test.rb @@ -0,0 +1,16 @@ +# 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.delivery_method = :test diff --git a/groups/config/environments/test_pgsql.rb b/groups/config/environments/test_pgsql.rb new file mode 100644 index 000000000..35bb19bee --- /dev/null +++ b/groups/config/environments/test_pgsql.rb @@ -0,0 +1,16 @@ +# 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.delivery_method = :test \ No newline at end of file diff --git a/groups/config/environments/test_sqlite3.rb b/groups/config/environments/test_sqlite3.rb new file mode 100644 index 000000000..35bb19bee --- /dev/null +++ b/groups/config/environments/test_sqlite3.rb @@ -0,0 +1,16 @@ +# 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.delivery_method = :test \ No newline at end of file diff --git a/groups/config/routes.rb b/groups/config/routes.rb new file mode 100644 index 000000000..0edb71a06 --- /dev/null +++ b/groups/config/routes.rb @@ -0,0 +1,41 @@ +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 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil + map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow' + map.connect 'help/:ctrl/:page', :controller => 'help' + #map.connect ':controller/:action/:id/:sort_key/:sort_order' + + map.connect 'issues/:issue_id/relations/:action/:id', :controller => 'issue_relations' + map.connect 'projects/:project_id/issues/:action', :controller => 'issues' + map.connect 'projects/:project_id/news/:action', :controller => 'news' + map.connect 'projects/:project_id/documents/:action', :controller => 'documents' + map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards' + map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog' + map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages' + + 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' + end + + # Allow downloading Web Service WSDL as a file with an extension + # instead of a file named 'wsdl' + map.connect ':controller/service.wsdl', :action => 'wsdl' + + + # Install the default route as the lowest priority. + map.connect ':controller/:action/:id' +end diff --git a/groups/config/settings.yml b/groups/config/settings.yml new file mode 100644 index 000000000..bb501823e --- /dev/null +++ b/groups/config/settings.yml @@ -0,0 +1,118 @@ +# 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 +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@somenet.foo +bcc_recipients: + default: 1 +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 +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 +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 +# encodings used to convert repository files content to UTF-8 +# multiple values accepted, comma separated +repositories_encodings: + default: '' +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 + diff --git a/groups/db/migrate/001_setup.rb b/groups/db/migrate/001_setup.rb new file mode 100644 index 000000000..1160dd5ef --- /dev/null +++ b/groups/db/migrate/001_setup.rb @@ -0,0 +1,321 @@ +# 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 + + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + create_table "attachments", :force => true do |t| + t.column "container_id", :integer, :default => 0, :null => false + t.column "container_type", :string, :limit => 30, :default => "", :null => false + t.column "filename", :string, :default => "", :null => false + t.column "disk_filename", :string, :default => "", :null => false + t.column "filesize", :integer, :default => 0, :null => false + t.column "content_type", :string, :limit => 60, :default => "" + t.column "digest", :string, :limit => 40, :default => "", :null => false + t.column "downloads", :integer, :default => 0, :null => false + t.column "author_id", :integer, :default => 0, :null => false + t.column "created_on", :timestamp + end + + 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 + + create_table "custom_fields", :force => true do |t| + t.column "type", :string, :limit => 30, :default => "", :null => false + t.column "name", :string, :limit => 30, :default => "", :null => false + t.column "field_format", :string, :limit => 30, :default => "", :null => false + t.column "possible_values", :text + t.column "regexp", :string, :default => "" + t.column "min_length", :integer, :default => 0, :null => false + t.column "max_length", :integer, :default => 0, :null => false + t.column "is_required", :boolean, :default => false, :null => false + t.column "is_for_all", :boolean, :default => false, :null => false + end + + create_table "custom_fields_projects", :id => false, :force => true do |t| + t.column "custom_field_id", :integer, :default => 0, :null => false + t.column "project_id", :integer, :default => 0, :null => false + end + + 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 + + create_table "custom_values", :force => true do |t| + 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 + t.column "value", :text + end + + create_table "documents", :force => true do |t| + t.column "project_id", :integer, :default => 0, :null => false + t.column "category_id", :integer, :default => 0, :null => false + t.column "title", :string, :limit => 60, :default => "", :null => false + t.column "description", :text + t.column "created_on", :timestamp + end + + add_index "documents", ["project_id"], :name => "documents_project_id" + + create_table "enumerations", :force => true do |t| + t.column "opt", :string, :limit => 4, :default => "", :null => false + t.column "name", :string, :limit => 30, :default => "", :null => false + end + + create_table "issue_categories", :force => true do |t| + t.column "project_id", :integer, :default => 0, :null => false + t.column "name", :string, :limit => 30, :default => "", :null => false + end + + add_index "issue_categories", ["project_id"], :name => "issue_categories_project_id" + + 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 + t.column "created_on", :timestamp + end + + add_index "issue_histories", ["issue_id"], :name => "issue_histories_issue_id" + + create_table "issue_statuses", :force => true do |t| + t.column "name", :string, :limit => 30, :default => "", :null => false + t.column "is_closed", :boolean, :default => false, :null => false + t.column "is_default", :boolean, :default => false, :null => false + t.column "html_color", :string, :limit => 6, :default => "FFFFFF", :null => false + end + + create_table "issues", :force => true do |t| + t.column "tracker_id", :integer, :default => 0, :null => false + t.column "project_id", :integer, :default => 0, :null => false + t.column "subject", :string, :default => "", :null => false + t.column "description", :text + t.column "due_date", :date + t.column "category_id", :integer + t.column "status_id", :integer, :default => 0, :null => false + t.column "assigned_to_id", :integer + t.column "priority_id", :integer, :default => 0, :null => false + t.column "fixed_version_id", :integer + t.column "author_id", :integer, :default => 0, :null => false + t.column "lock_version", :integer, :default => 0, :null => false + t.column "created_on", :timestamp + t.column "updated_on", :timestamp + end + + add_index "issues", ["project_id"], :name => "issues_project_id" + + create_table "members", :force => true do |t| + t.column "user_id", :integer, :default => 0, :null => false + t.column "project_id", :integer, :default => 0, :null => false + t.column "role_id", :integer, :default => 0, :null => false + t.column "created_on", :timestamp + end + + create_table "news", :force => true do |t| + t.column "project_id", :integer + t.column "title", :string, :limit => 60, :default => "", :null => false + t.column "summary", :string, :limit => 255, :default => "" + t.column "description", :text + t.column "author_id", :integer, :default => 0, :null => false + t.column "created_on", :timestamp + end + + add_index "news", ["project_id"], :name => "news_project_id" + + create_table "permissions", :force => true do |t| + t.column "controller", :string, :limit => 30, :default => "", :null => false + t.column "action", :string, :limit => 30, :default => "", :null => false + t.column "description", :string, :limit => 60, :default => "", :null => false + t.column "is_public", :boolean, :default => false, :null => false + t.column "sort", :integer, :default => 0, :null => false + t.column "mail_option", :boolean, :default => false, :null => false + t.column "mail_enabled", :boolean, :default => false, :null => false + end + + create_table "permissions_roles", :id => false, :force => true do |t| + t.column "permission_id", :integer, :default => 0, :null => false + t.column "role_id", :integer, :default => 0, :null => false + end + + add_index "permissions_roles", ["role_id"], :name => "permissions_roles_role_id" + + create_table "projects", :force => true do |t| + t.column "name", :string, :limit => 30, :default => "", :null => false + t.column "description", :string, :default => "", :null => false + t.column "homepage", :string, :limit => 60, :default => "" + t.column "is_public", :boolean, :default => true, :null => false + t.column "parent_id", :integer + t.column "projects_count", :integer, :default => 0 + t.column "created_on", :timestamp + t.column "updated_on", :timestamp + end + + create_table "roles", :force => true do |t| + t.column "name", :string, :limit => 30, :default => "", :null => false + end + + 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 + + create_table "trackers", :force => true do |t| + t.column "name", :string, :limit => 30, :default => "", :null => false + t.column "is_in_chlog", :boolean, :default => false, :null => false + end + + create_table "users", :force => true do |t| + t.column "login", :string, :limit => 30, :default => "", :null => false + t.column "hashed_password", :string, :limit => 40, :default => "", :null => false + t.column "firstname", :string, :limit => 30, :default => "", :null => false + t.column "lastname", :string, :limit => 30, :default => "", :null => false + t.column "mail", :string, :limit => 60, :default => "", :null => false + t.column "mail_notification", :boolean, :default => true, :null => false + t.column "admin", :boolean, :default => false, :null => false + t.column "status", :integer, :default => 1, :null => false + t.column "last_login_on", :datetime + t.column "language", :string, :limit => 2, :default => "" + t.column "auth_source_id", :integer + t.column "created_on", :timestamp + t.column "updated_on", :timestamp + end + + create_table "versions", :force => true do |t| + t.column "project_id", :integer, :default => 0, :null => false + t.column "name", :string, :limit => 30, :default => "", :null => false + t.column "description", :string, :default => "" + t.column "effective_date", :date + t.column "created_on", :timestamp + t.column "updated_on", :timestamp + end + + add_index "versions", ["project_id"], :name => "versions_project_id" + + create_table "workflows", :force => true do |t| + t.column "tracker_id", :integer, :default => 0, :null => false + t.column "old_status_id", :integer, :default => 0, :null => false + t.column "new_status_id", :integer, :default => 0, :null => false + t.column "role_id", :integer, :default => 0, :null => false + end + + # project + Permission.create :controller => "projects", :action => "show", :description => "label_overview", :sort => 100, :is_public => true + Permission.create :controller => "projects", :action => "changelog", :description => "label_change_log", :sort => 105, :is_public => true + Permission.create :controller => "reports", :action => "issue_report", :description => "label_report_plural", :sort => 110, :is_public => true + Permission.create :controller => "projects", :action => "settings", :description => "label_settings", :sort => 150 + Permission.create :controller => "projects", :action => "edit", :description => "button_edit", :sort => 151 + # members + Permission.create :controller => "projects", :action => "list_members", :description => "button_list", :sort => 200, :is_public => true + Permission.create :controller => "projects", :action => "add_member", :description => "button_add", :sort => 220 + Permission.create :controller => "members", :action => "edit", :description => "button_edit", :sort => 221 + Permission.create :controller => "members", :action => "destroy", :description => "button_delete", :sort => 222 + # versions + Permission.create :controller => "projects", :action => "add_version", :description => "button_add", :sort => 320 + Permission.create :controller => "versions", :action => "edit", :description => "button_edit", :sort => 321 + Permission.create :controller => "versions", :action => "destroy", :description => "button_delete", :sort => 322 + # issue categories + Permission.create :controller => "projects", :action => "add_issue_category", :description => "button_add", :sort => 420 + Permission.create :controller => "issue_categories", :action => "edit", :description => "button_edit", :sort => 421 + Permission.create :controller => "issue_categories", :action => "destroy", :description => "button_delete", :sort => 422 + # issues + Permission.create :controller => "projects", :action => "list_issues", :description => "button_list", :sort => 1000, :is_public => true + 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 + Permission.create :controller => "issues", :action => "download", :description => "button_download", :sort => 1010, :is_public => true + Permission.create :controller => "projects", :action => "add_issue", :description => "button_add", :sort => 1050, :mail_option => 1, :mail_enabled => 1 + Permission.create :controller => "issues", :action => "edit", :description => "button_edit", :sort => 1055 + Permission.create :controller => "issues", :action => "change_status", :description => "label_change_status", :sort => 1060, :mail_option => 1, :mail_enabled => 1 + Permission.create :controller => "issues", :action => "destroy", :description => "button_delete", :sort => 1065 + Permission.create :controller => "issues", :action => "add_attachment", :description => "label_attachment_new", :sort => 1070 + Permission.create :controller => "issues", :action => "destroy_attachment", :description => "label_attachment_delete", :sort => 1075 + # news + Permission.create :controller => "projects", :action => "list_news", :description => "button_list", :sort => 1100, :is_public => true + Permission.create :controller => "news", :action => "show", :description => "button_view", :sort => 1101, :is_public => true + Permission.create :controller => "projects", :action => "add_news", :description => "button_add", :sort => 1120 + Permission.create :controller => "news", :action => "edit", :description => "button_edit", :sort => 1121 + Permission.create :controller => "news", :action => "destroy", :description => "button_delete", :sort => 1122 + # documents + Permission.create :controller => "projects", :action => "list_documents", :description => "button_list", :sort => 1200, :is_public => true + Permission.create :controller => "documents", :action => "show", :description => "button_view", :sort => 1201, :is_public => true + Permission.create :controller => "documents", :action => "download", :description => "button_download", :sort => 1202, :is_public => true + Permission.create :controller => "projects", :action => "add_document", :description => "button_add", :sort => 1220 + Permission.create :controller => "documents", :action => "edit", :description => "button_edit", :sort => 1221 + Permission.create :controller => "documents", :action => "destroy", :description => "button_delete", :sort => 1222 + Permission.create :controller => "documents", :action => "add_attachment", :description => "label_attachment_new", :sort => 1223 + Permission.create :controller => "documents", :action => "destroy_attachment", :description => "label_attachment_delete", :sort => 1224 + # files + Permission.create :controller => "projects", :action => "list_files", :description => "button_list", :sort => 1300, :is_public => true + Permission.create :controller => "versions", :action => "download", :description => "button_download", :sort => 1301, :is_public => true + Permission.create :controller => "projects", :action => "add_file", :description => "button_add", :sort => 1320 + Permission.create :controller => "versions", :action => "destroy_file", :description => "button_delete", :sort => 1322 + + # create default administrator account + user = User.create :firstname => "Redmine", :lastname => "Admin", :mail => "admin@somenet.foo", :mail_notification => true, :language => "en" + user.login = "admin" + user.password = "admin" + user.admin = true + user.save + + + end + + def self.down + drop_table :attachments + drop_table :auth_sources + drop_table :custom_fields + drop_table :custom_fields_projects + drop_table :custom_fields_trackers + drop_table :custom_values + drop_table :documents + drop_table :enumerations + drop_table :issue_categories + drop_table :issue_histories + drop_table :issue_statuses + drop_table :issues + drop_table :members + drop_table :news + drop_table :permissions + drop_table :permissions_roles + drop_table :projects + drop_table :roles + drop_table :trackers + drop_table :tokens + drop_table :users + drop_table :versions + drop_table :workflows + end +end diff --git a/groups/db/migrate/002_issue_move.rb b/groups/db/migrate/002_issue_move.rb new file mode 100644 index 000000000..085593e08 --- /dev/null +++ b/groups/db/migrate/002_issue_move.rb @@ -0,0 +1,12 @@ +class IssueMove < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.create :controller => "projects", :action => "move_issues", :description => "button_move", :sort => 1061, :mail_option => 0, :mail_enabled => 0 + end + + def self.down + Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'move_issues']).destroy + end +end diff --git a/groups/db/migrate/003_issue_add_note.rb b/groups/db/migrate/003_issue_add_note.rb new file mode 100644 index 000000000..a2ab756ee --- /dev/null +++ b/groups/db/migrate/003_issue_add_note.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/004_export_pdf.rb b/groups/db/migrate/004_export_pdf.rb new file mode 100644 index 000000000..6ccd67eae --- /dev/null +++ b/groups/db/migrate/004_export_pdf.rb @@ -0,0 +1,14 @@ +class ExportPdf < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.create :controller => "projects", :action => "export_issues_pdf", :description => "label_export_pdf", :sort => 1002, :is_public => true, :mail_option => 0, :mail_enabled => 0 + Permission.create :controller => "issues", :action => "export_pdf", :description => "label_export_pdf", :sort => 1015, :is_public => true, :mail_option => 0, :mail_enabled => 0 + end + + def self.down + Permission.find(:first, :conditions => ["controller=? and action=?", 'projects', 'export_issues_pdf']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'export_pdf']).destroy + end +end diff --git a/groups/db/migrate/005_issue_start_date.rb b/groups/db/migrate/005_issue_start_date.rb new file mode 100644 index 000000000..3d1693fc6 --- /dev/null +++ b/groups/db/migrate/005_issue_start_date.rb @@ -0,0 +1,11 @@ +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 diff --git a/groups/db/migrate/006_calendar_and_activity.rb b/groups/db/migrate/006_calendar_and_activity.rb new file mode 100644 index 000000000..1cdc91d8e --- /dev/null +++ b/groups/db/migrate/006_calendar_and_activity.rb @@ -0,0 +1,16 @@ +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 diff --git a/groups/db/migrate/007_create_journals.rb b/groups/db/migrate/007_create_journals.rb new file mode 100644 index 000000000..b00347839 --- /dev/null +++ b/groups/db/migrate/007_create_journals.rb @@ -0,0 +1,56 @@ +class CreateJournals < ActiveRecord::Migration + + # model removed, but needed for data migration + class IssueHistory < ActiveRecord::Base; belongs_to :issue; end + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + create_table :journals, :force => true do |t| + t.column "journalized_id", :integer, :default => 0, :null => false + t.column "journalized_type", :string, :limit => 30, :default => "", :null => false + t.column "user_id", :integer, :default => 0, :null => false + t.column "notes", :text + t.column "created_on", :datetime, :null => false + end + create_table :journal_details, :force => true do |t| + t.column "journal_id", :integer, :default => 0, :null => false + t.column "property", :string, :limit => 30, :default => "", :null => false + t.column "prop_key", :string, :limit => 30, :default => "", :null => false + t.column "old_value", :string + t.column "value", :string + end + + # indexes + add_index "journals", ["journalized_id", "journalized_type"], :name => "journals_journalized_id" + add_index "journal_details", ["journal_id"], :name => "journal_details_journal_id" + + Permission.create :controller => "issues", :action => "history", :description => "label_history", :sort => 1006, :is_public => true, :mail_option => 0, :mail_enabled => 0 + + # data migration + IssueHistory.find(:all, :include => :issue).each {|h| + j = Journal.new(:journalized => h.issue, :user_id => h.author_id, :notes => h.notes, :created_on => h.created_on) + j.details << JournalDetail.new(:property => 'attr', :prop_key => 'status_id', :value => h.status_id) + j.save + } + + drop_table :issue_histories + end + + def self.down + drop_table :journal_details + drop_table :journals + + create_table "issue_histories", :force => true do |t| + t.column "issue_id", :integer, :default => 0, :null => false + t.column "status_id", :integer, :default => 0, :null => false + t.column "author_id", :integer, :default => 0, :null => false + t.column "notes", :text, :default => "" + t.column "created_on", :timestamp + end + + add_index "issue_histories", ["issue_id"], :name => "issue_histories_issue_id" + + Permission.find(:first, :conditions => ["controller=? and action=?", 'issues', 'history']).destroy + end +end diff --git a/groups/db/migrate/008_create_user_preferences.rb b/groups/db/migrate/008_create_user_preferences.rb new file mode 100644 index 000000000..80ae1cdf9 --- /dev/null +++ b/groups/db/migrate/008_create_user_preferences.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/009_add_hide_mail_pref.rb b/groups/db/migrate/009_add_hide_mail_pref.rb new file mode 100644 index 000000000..a22eafd93 --- /dev/null +++ b/groups/db/migrate/009_add_hide_mail_pref.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/010_create_comments.rb b/groups/db/migrate/010_create_comments.rb new file mode 100644 index 000000000..29e1116af --- /dev/null +++ b/groups/db/migrate/010_create_comments.rb @@ -0,0 +1,16 @@ +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 diff --git a/groups/db/migrate/011_add_news_comments_count.rb b/groups/db/migrate/011_add_news_comments_count.rb new file mode 100644 index 000000000..a24743999 --- /dev/null +++ b/groups/db/migrate/011_add_news_comments_count.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/012_add_comments_permissions.rb b/groups/db/migrate/012_add_comments_permissions.rb new file mode 100644 index 000000000..2bbf87b02 --- /dev/null +++ b/groups/db/migrate/012_add_comments_permissions.rb @@ -0,0 +1,14 @@ +class AddCommentsPermissions < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.create :controller => "news", :action => "add_comment", :description => "label_comment_add", :sort => 1130, :is_public => false, :mail_option => 0, :mail_enabled => 0 + Permission.create :controller => "news", :action => "destroy_comment", :description => "label_comment_delete", :sort => 1133, :is_public => false, :mail_option => 0, :mail_enabled => 0 + end + + def self.down + Permission.find(:first, :conditions => ["controller=? and action=?", 'news', 'add_comment']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'news', 'destroy_comment']).destroy + end +end diff --git a/groups/db/migrate/013_create_queries.rb b/groups/db/migrate/013_create_queries.rb new file mode 100644 index 000000000..e0e8c90c0 --- /dev/null +++ b/groups/db/migrate/013_create_queries.rb @@ -0,0 +1,15 @@ +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 diff --git a/groups/db/migrate/014_add_queries_permissions.rb b/groups/db/migrate/014_add_queries_permissions.rb new file mode 100644 index 000000000..34eba1e26 --- /dev/null +++ b/groups/db/migrate/014_add_queries_permissions.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/015_create_repositories.rb b/groups/db/migrate/015_create_repositories.rb new file mode 100644 index 000000000..d8c0524b3 --- /dev/null +++ b/groups/db/migrate/015_create_repositories.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/016_add_repositories_permissions.rb b/groups/db/migrate/016_add_repositories_permissions.rb new file mode 100644 index 000000000..341707639 --- /dev/null +++ b/groups/db/migrate/016_add_repositories_permissions.rb @@ -0,0 +1,22 @@ +class AddRepositoriesPermissions < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.create :controller => "repositories", :action => "show", :description => "button_view", :sort => 1450, :is_public => true + Permission.create :controller => "repositories", :action => "browse", :description => "label_browse", :sort => 1460, :is_public => true + Permission.create :controller => "repositories", :action => "entry", :description => "entry", :sort => 1462, :is_public => true + Permission.create :controller => "repositories", :action => "revisions", :description => "label_view_revisions", :sort => 1470, :is_public => true + Permission.create :controller => "repositories", :action => "revision", :description => "label_view_revisions", :sort => 1472, :is_public => true + Permission.create :controller => "repositories", :action => "diff", :description => "diff", :sort => 1480, :is_public => true + end + + def self.down + Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'show']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'browse']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'entry']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revisions']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'revision']).destroy + Permission.find(:first, :conditions => ["controller=? and action=?", 'repositories', 'diff']).destroy + end +end diff --git a/groups/db/migrate/017_create_settings.rb b/groups/db/migrate/017_create_settings.rb new file mode 100644 index 000000000..99f96adf8 --- /dev/null +++ b/groups/db/migrate/017_create_settings.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/018_set_doc_and_files_notifications.rb b/groups/db/migrate/018_set_doc_and_files_notifications.rb new file mode 100644 index 000000000..8c1d054c1 --- /dev/null +++ b/groups/db/migrate/018_set_doc_and_files_notifications.rb @@ -0,0 +1,18 @@ +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 diff --git a/groups/db/migrate/019_add_issue_status_position.rb b/groups/db/migrate/019_add_issue_status_position.rb new file mode 100644 index 000000000..ed24d27c1 --- /dev/null +++ b/groups/db/migrate/019_add_issue_status_position.rb @@ -0,0 +1,10 @@ +class AddIssueStatusPosition < ActiveRecord::Migration + def self.up + add_column :issue_statuses, :position, :integer, :default => 1 + IssueStatus.find(:all).each_with_index {|status, i| status.update_attribute(:position, i+1)} + end + + def self.down + remove_column :issue_statuses, :position + end +end diff --git a/groups/db/migrate/020_add_role_position.rb b/groups/db/migrate/020_add_role_position.rb new file mode 100644 index 000000000..e220bd9fd --- /dev/null +++ b/groups/db/migrate/020_add_role_position.rb @@ -0,0 +1,10 @@ +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 diff --git a/groups/db/migrate/021_add_tracker_position.rb b/groups/db/migrate/021_add_tracker_position.rb new file mode 100644 index 000000000..ef9775620 --- /dev/null +++ b/groups/db/migrate/021_add_tracker_position.rb @@ -0,0 +1,10 @@ +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 diff --git a/groups/db/migrate/022_serialize_possibles_values.rb b/groups/db/migrate/022_serialize_possibles_values.rb new file mode 100644 index 000000000..5158f37fd --- /dev/null +++ b/groups/db/migrate/022_serialize_possibles_values.rb @@ -0,0 +1,13 @@ +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 diff --git a/groups/db/migrate/023_add_tracker_is_in_roadmap.rb b/groups/db/migrate/023_add_tracker_is_in_roadmap.rb new file mode 100644 index 000000000..82ef87bba --- /dev/null +++ b/groups/db/migrate/023_add_tracker_is_in_roadmap.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/024_add_roadmap_permission.rb b/groups/db/migrate/024_add_roadmap_permission.rb new file mode 100644 index 000000000..5c37beac1 --- /dev/null +++ b/groups/db/migrate/024_add_roadmap_permission.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/025_add_search_permission.rb b/groups/db/migrate/025_add_search_permission.rb new file mode 100644 index 000000000..a942b01b3 --- /dev/null +++ b/groups/db/migrate/025_add_search_permission.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/026_add_repository_login_and_password.rb b/groups/db/migrate/026_add_repository_login_and_password.rb new file mode 100644 index 000000000..5fc919725 --- /dev/null +++ b/groups/db/migrate/026_add_repository_login_and_password.rb @@ -0,0 +1,11 @@ +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 diff --git a/groups/db/migrate/027_create_wikis.rb b/groups/db/migrate/027_create_wikis.rb new file mode 100644 index 000000000..ed6784296 --- /dev/null +++ b/groups/db/migrate/027_create_wikis.rb @@ -0,0 +1,14 @@ +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 diff --git a/groups/db/migrate/028_create_wiki_pages.rb b/groups/db/migrate/028_create_wiki_pages.rb new file mode 100644 index 000000000..535cbfb0a --- /dev/null +++ b/groups/db/migrate/028_create_wiki_pages.rb @@ -0,0 +1,14 @@ +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 diff --git a/groups/db/migrate/029_create_wiki_contents.rb b/groups/db/migrate/029_create_wiki_contents.rb new file mode 100644 index 000000000..c5c9f2a45 --- /dev/null +++ b/groups/db/migrate/029_create_wiki_contents.rb @@ -0,0 +1,30 @@ +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 diff --git a/groups/db/migrate/030_add_projects_feeds_permissions.rb b/groups/db/migrate/030_add_projects_feeds_permissions.rb new file mode 100644 index 000000000..7f97035bf --- /dev/null +++ b/groups/db/migrate/030_add_projects_feeds_permissions.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/031_add_repository_root_url.rb b/groups/db/migrate/031_add_repository_root_url.rb new file mode 100644 index 000000000..df57809c7 --- /dev/null +++ b/groups/db/migrate/031_add_repository_root_url.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/032_create_time_entries.rb b/groups/db/migrate/032_create_time_entries.rb new file mode 100644 index 000000000..9b9a54eb1 --- /dev/null +++ b/groups/db/migrate/032_create_time_entries.rb @@ -0,0 +1,24 @@ +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 diff --git a/groups/db/migrate/033_add_timelog_permissions.rb b/groups/db/migrate/033_add_timelog_permissions.rb new file mode 100644 index 000000000..ab9c809e6 --- /dev/null +++ b/groups/db/migrate/033_add_timelog_permissions.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/034_create_changesets.rb b/groups/db/migrate/034_create_changesets.rb new file mode 100644 index 000000000..612fd46bb --- /dev/null +++ b/groups/db/migrate/034_create_changesets.rb @@ -0,0 +1,16 @@ +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 diff --git a/groups/db/migrate/035_create_changes.rb b/groups/db/migrate/035_create_changes.rb new file mode 100644 index 000000000..fa0cfac3f --- /dev/null +++ b/groups/db/migrate/035_create_changes.rb @@ -0,0 +1,16 @@ +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 diff --git a/groups/db/migrate/036_add_changeset_commit_date.rb b/groups/db/migrate/036_add_changeset_commit_date.rb new file mode 100644 index 000000000..b9cc49b84 --- /dev/null +++ b/groups/db/migrate/036_add_changeset_commit_date.rb @@ -0,0 +1,10 @@ +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 diff --git a/groups/db/migrate/037_add_project_identifier.rb b/groups/db/migrate/037_add_project_identifier.rb new file mode 100644 index 000000000..0fd8c7513 --- /dev/null +++ b/groups/db/migrate/037_add_project_identifier.rb @@ -0,0 +1,9 @@ +class AddProjectIdentifier < ActiveRecord::Migration + def self.up + add_column :projects, :identifier, :string, :limit => 20 + end + + def self.down + remove_column :projects, :identifier + end +end diff --git a/groups/db/migrate/038_add_custom_field_is_filter.rb b/groups/db/migrate/038_add_custom_field_is_filter.rb new file mode 100644 index 000000000..519ee0bd5 --- /dev/null +++ b/groups/db/migrate/038_add_custom_field_is_filter.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/039_create_watchers.rb b/groups/db/migrate/039_create_watchers.rb new file mode 100644 index 000000000..9579e19a4 --- /dev/null +++ b/groups/db/migrate/039_create_watchers.rb @@ -0,0 +1,13 @@ +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 diff --git a/groups/db/migrate/040_create_changesets_issues.rb b/groups/db/migrate/040_create_changesets_issues.rb new file mode 100644 index 000000000..494d3cc46 --- /dev/null +++ b/groups/db/migrate/040_create_changesets_issues.rb @@ -0,0 +1,13 @@ +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 diff --git a/groups/db/migrate/041_rename_comment_to_comments.rb b/groups/db/migrate/041_rename_comment_to_comments.rb new file mode 100644 index 000000000..93677e575 --- /dev/null +++ b/groups/db/migrate/041_rename_comment_to_comments.rb @@ -0,0 +1,13 @@ +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 diff --git a/groups/db/migrate/042_create_issue_relations.rb b/groups/db/migrate/042_create_issue_relations.rb new file mode 100644 index 000000000..802c12437 --- /dev/null +++ b/groups/db/migrate/042_create_issue_relations.rb @@ -0,0 +1,14 @@ +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 diff --git a/groups/db/migrate/043_add_relations_permissions.rb b/groups/db/migrate/043_add_relations_permissions.rb new file mode 100644 index 000000000..32d464a58 --- /dev/null +++ b/groups/db/migrate/043_add_relations_permissions.rb @@ -0,0 +1,14 @@ +class AddRelationsPermissions < ActiveRecord::Migration + # model removed + class Permission < ActiveRecord::Base; end + + def self.up + Permission.create :controller => "issue_relations", :action => "new", :description => "label_relation_new", :sort => 1080, :is_public => false, :mail_option => 0, :mail_enabled => 0 + Permission.create :controller => "issue_relations", :action => "destroy", :description => "label_relation_delete", :sort => 1085, :is_public => false, :mail_option => 0, :mail_enabled => 0 + end + + def self.down + Permission.find_by_controller_and_action("issue_relations", "new").destroy + Permission.find_by_controller_and_action("issue_relations", "destroy").destroy + end +end diff --git a/groups/db/migrate/044_set_language_length_to_five.rb b/groups/db/migrate/044_set_language_length_to_five.rb new file mode 100644 index 000000000..a417f7d70 --- /dev/null +++ b/groups/db/migrate/044_set_language_length_to_five.rb @@ -0,0 +1,9 @@ +class SetLanguageLengthToFive < ActiveRecord::Migration + def self.up + change_column :users, :language, :string, :limit => 5, :default => "" + end + + def self.down + raise IrreversibleMigration + end +end diff --git a/groups/db/migrate/045_create_boards.rb b/groups/db/migrate/045_create_boards.rb new file mode 100644 index 000000000..17f2bbbe2 --- /dev/null +++ b/groups/db/migrate/045_create_boards.rb @@ -0,0 +1,18 @@ +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 diff --git a/groups/db/migrate/046_create_messages.rb b/groups/db/migrate/046_create_messages.rb new file mode 100644 index 000000000..d99aaf842 --- /dev/null +++ b/groups/db/migrate/046_create_messages.rb @@ -0,0 +1,21 @@ +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 diff --git a/groups/db/migrate/047_add_boards_permissions.rb b/groups/db/migrate/047_add_boards_permissions.rb new file mode 100644 index 000000000..5b1f6f779 --- /dev/null +++ b/groups/db/migrate/047_add_boards_permissions.rb @@ -0,0 +1,16 @@ +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 diff --git a/groups/db/migrate/048_allow_null_version_effective_date.rb b/groups/db/migrate/048_allow_null_version_effective_date.rb new file mode 100644 index 000000000..82d2a33ec --- /dev/null +++ b/groups/db/migrate/048_allow_null_version_effective_date.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/049_add_wiki_destroy_page_permission.rb b/groups/db/migrate/049_add_wiki_destroy_page_permission.rb new file mode 100644 index 000000000..c82152388 --- /dev/null +++ b/groups/db/migrate/049_add_wiki_destroy_page_permission.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/050_add_wiki_attachments_permissions.rb b/groups/db/migrate/050_add_wiki_attachments_permissions.rb new file mode 100644 index 000000000..c0697be9c --- /dev/null +++ b/groups/db/migrate/050_add_wiki_attachments_permissions.rb @@ -0,0 +1,14 @@ +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 diff --git a/groups/db/migrate/051_add_project_status.rb b/groups/db/migrate/051_add_project_status.rb new file mode 100644 index 000000000..fba36d237 --- /dev/null +++ b/groups/db/migrate/051_add_project_status.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/052_add_changes_revision.rb b/groups/db/migrate/052_add_changes_revision.rb new file mode 100644 index 000000000..6f58c1a70 --- /dev/null +++ b/groups/db/migrate/052_add_changes_revision.rb @@ -0,0 +1,9 @@ +class AddChangesRevision < ActiveRecord::Migration + def self.up + add_column :changes, :revision, :string + end + + def self.down + remove_column :changes, :revision + end +end diff --git a/groups/db/migrate/053_add_changes_branch.rb b/groups/db/migrate/053_add_changes_branch.rb new file mode 100644 index 000000000..998ce2ba5 --- /dev/null +++ b/groups/db/migrate/053_add_changes_branch.rb @@ -0,0 +1,9 @@ +class AddChangesBranch < ActiveRecord::Migration + def self.up + add_column :changes, :branch, :string + end + + def self.down + remove_column :changes, :branch + end +end diff --git a/groups/db/migrate/054_add_changesets_scmid.rb b/groups/db/migrate/054_add_changesets_scmid.rb new file mode 100644 index 000000000..188fa6ef6 --- /dev/null +++ b/groups/db/migrate/054_add_changesets_scmid.rb @@ -0,0 +1,9 @@ +class AddChangesetsScmid < ActiveRecord::Migration + def self.up + add_column :changesets, :scmid, :string + end + + def self.down + remove_column :changesets, :scmid + end +end diff --git a/groups/db/migrate/055_add_repositories_type.rb b/groups/db/migrate/055_add_repositories_type.rb new file mode 100644 index 000000000..599f70aac --- /dev/null +++ b/groups/db/migrate/055_add_repositories_type.rb @@ -0,0 +1,11 @@ +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 diff --git a/groups/db/migrate/056_add_repositories_changes_permission.rb b/groups/db/migrate/056_add_repositories_changes_permission.rb new file mode 100644 index 000000000..0d9b13b59 --- /dev/null +++ b/groups/db/migrate/056_add_repositories_changes_permission.rb @@ -0,0 +1,12 @@ +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 diff --git a/groups/db/migrate/057_add_versions_wiki_page_title.rb b/groups/db/migrate/057_add_versions_wiki_page_title.rb new file mode 100644 index 000000000..58b8fd9a8 --- /dev/null +++ b/groups/db/migrate/057_add_versions_wiki_page_title.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/058_add_issue_categories_assigned_to_id.rb b/groups/db/migrate/058_add_issue_categories_assigned_to_id.rb new file mode 100644 index 000000000..8653532e0 --- /dev/null +++ b/groups/db/migrate/058_add_issue_categories_assigned_to_id.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/059_add_roles_assignable.rb b/groups/db/migrate/059_add_roles_assignable.rb new file mode 100644 index 000000000..a1ba79634 --- /dev/null +++ b/groups/db/migrate/059_add_roles_assignable.rb @@ -0,0 +1,9 @@ +class AddRolesAssignable < ActiveRecord::Migration + def self.up + add_column :roles, :assignable, :boolean, :default => true + end + + def self.down + remove_column :roles, :assignable + end +end diff --git a/groups/db/migrate/060_change_changesets_committer_limit.rb b/groups/db/migrate/060_change_changesets_committer_limit.rb new file mode 100644 index 000000000..b05096379 --- /dev/null +++ b/groups/db/migrate/060_change_changesets_committer_limit.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/061_add_roles_builtin.rb b/groups/db/migrate/061_add_roles_builtin.rb new file mode 100644 index 000000000..a8d6fe9e6 --- /dev/null +++ b/groups/db/migrate/061_add_roles_builtin.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/062_insert_builtin_roles.rb b/groups/db/migrate/062_insert_builtin_roles.rb new file mode 100644 index 000000000..27c7475c3 --- /dev/null +++ b/groups/db/migrate/062_insert_builtin_roles.rb @@ -0,0 +1,15 @@ +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 diff --git a/groups/db/migrate/063_add_roles_permissions.rb b/groups/db/migrate/063_add_roles_permissions.rb new file mode 100644 index 000000000..107a3af0a --- /dev/null +++ b/groups/db/migrate/063_add_roles_permissions.rb @@ -0,0 +1,9 @@ +class AddRolesPermissions < ActiveRecord::Migration + def self.up + add_column :roles, :permissions, :text + end + + def self.down + remove_column :roles, :permissions + end +end diff --git a/groups/db/migrate/064_drop_permissions.rb b/groups/db/migrate/064_drop_permissions.rb new file mode 100644 index 000000000..f4ca470bf --- /dev/null +++ b/groups/db/migrate/064_drop_permissions.rb @@ -0,0 +1,10 @@ +class DropPermissions < ActiveRecord::Migration + def self.up + drop_table :permissions + drop_table :permissions_roles + end + + def self.down + raise IrreversibleMigration + end +end diff --git a/groups/db/migrate/065_add_settings_updated_on.rb b/groups/db/migrate/065_add_settings_updated_on.rb new file mode 100644 index 000000000..8c5fde33b --- /dev/null +++ b/groups/db/migrate/065_add_settings_updated_on.rb @@ -0,0 +1,11 @@ +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 diff --git a/groups/db/migrate/066_add_custom_value_customized_index.rb b/groups/db/migrate/066_add_custom_value_customized_index.rb new file mode 100644 index 000000000..1f4c40da2 --- /dev/null +++ b/groups/db/migrate/066_add_custom_value_customized_index.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/067_create_wiki_redirects.rb b/groups/db/migrate/067_create_wiki_redirects.rb new file mode 100644 index 000000000..dda6ba6d5 --- /dev/null +++ b/groups/db/migrate/067_create_wiki_redirects.rb @@ -0,0 +1,15 @@ +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 diff --git a/groups/db/migrate/068_create_enabled_modules.rb b/groups/db/migrate/068_create_enabled_modules.rb new file mode 100644 index 000000000..fd848ef96 --- /dev/null +++ b/groups/db/migrate/068_create_enabled_modules.rb @@ -0,0 +1,18 @@ +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 diff --git a/groups/db/migrate/069_add_issues_estimated_hours.rb b/groups/db/migrate/069_add_issues_estimated_hours.rb new file mode 100644 index 000000000..90b86e243 --- /dev/null +++ b/groups/db/migrate/069_add_issues_estimated_hours.rb @@ -0,0 +1,9 @@ +class AddIssuesEstimatedHours < ActiveRecord::Migration + def self.up + add_column :issues, :estimated_hours, :float + end + + def self.down + remove_column :issues, :estimated_hours + end +end diff --git a/groups/db/migrate/070_change_attachments_content_type_limit.rb b/groups/db/migrate/070_change_attachments_content_type_limit.rb new file mode 100644 index 000000000..ebf6d08c3 --- /dev/null +++ b/groups/db/migrate/070_change_attachments_content_type_limit.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/071_add_queries_column_names.rb b/groups/db/migrate/071_add_queries_column_names.rb new file mode 100644 index 000000000..acaf4dab0 --- /dev/null +++ b/groups/db/migrate/071_add_queries_column_names.rb @@ -0,0 +1,9 @@ +class AddQueriesColumnNames < ActiveRecord::Migration + def self.up + add_column :queries, :column_names, :text + end + + def self.down + remove_column :queries, :column_names + end +end diff --git a/groups/db/migrate/072_add_enumerations_position.rb b/groups/db/migrate/072_add_enumerations_position.rb new file mode 100644 index 000000000..e0beaf395 --- /dev/null +++ b/groups/db/migrate/072_add_enumerations_position.rb @@ -0,0 +1,15 @@ +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_value do |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 diff --git a/groups/db/migrate/073_add_enumerations_is_default.rb b/groups/db/migrate/073_add_enumerations_is_default.rb new file mode 100644 index 000000000..7365a1411 --- /dev/null +++ b/groups/db/migrate/073_add_enumerations_is_default.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/074_add_auth_sources_tls.rb b/groups/db/migrate/074_add_auth_sources_tls.rb new file mode 100644 index 000000000..3987f7036 --- /dev/null +++ b/groups/db/migrate/074_add_auth_sources_tls.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/075_add_members_mail_notification.rb b/groups/db/migrate/075_add_members_mail_notification.rb new file mode 100644 index 000000000..d83ba8dd0 --- /dev/null +++ b/groups/db/migrate/075_add_members_mail_notification.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/076_allow_null_position.rb b/groups/db/migrate/076_allow_null_position.rb new file mode 100644 index 000000000..ece0370db --- /dev/null +++ b/groups/db/migrate/076_allow_null_position.rb @@ -0,0 +1,14 @@ +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 diff --git a/groups/db/migrate/077_remove_issue_statuses_html_color.rb b/groups/db/migrate/077_remove_issue_statuses_html_color.rb new file mode 100644 index 000000000..a3e2c3f8f --- /dev/null +++ b/groups/db/migrate/077_remove_issue_statuses_html_color.rb @@ -0,0 +1,9 @@ +class RemoveIssueStatusesHtmlColor < ActiveRecord::Migration + def self.up + remove_column :issue_statuses, :html_color + end + + def self.down + raise IrreversibleMigration + end +end diff --git a/groups/db/migrate/078_add_custom_fields_position.rb b/groups/db/migrate/078_add_custom_fields_position.rb new file mode 100644 index 000000000..7ee8abb58 --- /dev/null +++ b/groups/db/migrate/078_add_custom_fields_position.rb @@ -0,0 +1,15 @@ +class AddCustomFieldsPosition < ActiveRecord::Migration + def self.up + add_column(:custom_fields, :position, :integer, :default => 1) + CustomField.find(:all).group_by(&:type).each_value do |fields| + fields.each_with_index do |field, i| + # do not call model callbacks + CustomField.update_all "position = #{i+1}", {:id => field.id} + end + end + end + + def self.down + remove_column :custom_fields, :position + end +end diff --git a/groups/db/migrate/079_add_user_preferences_time_zone.rb b/groups/db/migrate/079_add_user_preferences_time_zone.rb new file mode 100644 index 000000000..9e36790a9 --- /dev/null +++ b/groups/db/migrate/079_add_user_preferences_time_zone.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/080_add_users_type.rb b/groups/db/migrate/080_add_users_type.rb new file mode 100644 index 000000000..c907b472e --- /dev/null +++ b/groups/db/migrate/080_add_users_type.rb @@ -0,0 +1,10 @@ +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 diff --git a/groups/db/migrate/081_create_projects_trackers.rb b/groups/db/migrate/081_create_projects_trackers.rb new file mode 100644 index 000000000..70fea188e --- /dev/null +++ b/groups/db/migrate/081_create_projects_trackers.rb @@ -0,0 +1,19 @@ +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 diff --git a/groups/db/migrate/082_add_messages_locked.rb b/groups/db/migrate/082_add_messages_locked.rb new file mode 100644 index 000000000..20a172565 --- /dev/null +++ b/groups/db/migrate/082_add_messages_locked.rb @@ -0,0 +1,9 @@ +class AddMessagesLocked < ActiveRecord::Migration + def self.up + add_column :messages, :locked, :boolean, :default => false + end + + def self.down + remove_column :messages, :locked + end +end diff --git a/groups/db/migrate/083_add_messages_sticky.rb b/groups/db/migrate/083_add_messages_sticky.rb new file mode 100644 index 000000000..8fd5d2ce3 --- /dev/null +++ b/groups/db/migrate/083_add_messages_sticky.rb @@ -0,0 +1,9 @@ +class AddMessagesSticky < ActiveRecord::Migration + def self.up + add_column :messages, :sticky, :integer, :default => 0 + end + + def self.down + remove_column :messages, :sticky + end +end diff --git a/groups/db/migrate/084_change_auth_sources_account_limit.rb b/groups/db/migrate/084_change_auth_sources_account_limit.rb new file mode 100644 index 000000000..cc127b439 --- /dev/null +++ b/groups/db/migrate/084_change_auth_sources_account_limit.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb b/groups/db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb new file mode 100644 index 000000000..a59135be0 --- /dev/null +++ b/groups/db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/086_add_custom_fields_searchable.rb b/groups/db/migrate/086_add_custom_fields_searchable.rb new file mode 100644 index 000000000..53158d14e --- /dev/null +++ b/groups/db/migrate/086_add_custom_fields_searchable.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/087_change_projects_description_to_text.rb b/groups/db/migrate/087_change_projects_description_to_text.rb new file mode 100644 index 000000000..132e921b3 --- /dev/null +++ b/groups/db/migrate/087_change_projects_description_to_text.rb @@ -0,0 +1,8 @@ +class ChangeProjectsDescriptionToText < ActiveRecord::Migration + def self.up + change_column :projects, :description, :text, :null => true, :default => nil + end + + def self.down + end +end diff --git a/groups/db/migrate/088_add_custom_fields_default_value.rb b/groups/db/migrate/088_add_custom_fields_default_value.rb new file mode 100644 index 000000000..33a39ec6e --- /dev/null +++ b/groups/db/migrate/088_add_custom_fields_default_value.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/089_add_attachments_description.rb b/groups/db/migrate/089_add_attachments_description.rb new file mode 100644 index 000000000..411dfe4d6 --- /dev/null +++ b/groups/db/migrate/089_add_attachments_description.rb @@ -0,0 +1,9 @@ +class AddAttachmentsDescription < ActiveRecord::Migration + def self.up + add_column :attachments, :description, :string + end + + def self.down + remove_column :attachments, :description + end +end diff --git a/groups/db/migrate/090_change_versions_name_limit.rb b/groups/db/migrate/090_change_versions_name_limit.rb new file mode 100644 index 000000000..276429727 --- /dev/null +++ b/groups/db/migrate/090_change_versions_name_limit.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/091_change_changesets_revision_to_string.rb b/groups/db/migrate/091_change_changesets_revision_to_string.rb new file mode 100644 index 000000000..e621a3909 --- /dev/null +++ b/groups/db/migrate/091_change_changesets_revision_to_string.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/db/migrate/092_change_changes_from_revision_to_string.rb b/groups/db/migrate/092_change_changes_from_revision_to_string.rb new file mode 100644 index 000000000..b298a3f45 --- /dev/null +++ b/groups/db/migrate/092_change_changes_from_revision_to_string.rb @@ -0,0 +1,9 @@ +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 diff --git a/groups/doc/CHANGELOG b/groups/doc/CHANGELOG new file mode 100644 index 000000000..b39185151 --- /dev/null +++ b/groups/doc/CHANGELOG @@ -0,0 +1,562 @@ +== Redmine changelog + +Redmine - project management software +Copyright (C) 2006-2008 Jean-Philippe Lang +http://www.redmine.org/ + + +== 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’t 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’re 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 “done ratio” 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’t 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 “Watch” 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’s 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) diff --git a/groups/doc/COPYING b/groups/doc/COPYING new file mode 100644 index 000000000..d511905c1 --- /dev/null +++ b/groups/doc/COPYING @@ -0,0 +1,339 @@ + 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. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public 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. + + , 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. diff --git a/groups/doc/INSTALL b/groups/doc/INSTALL new file mode 100644 index 000000000..7a00b9367 --- /dev/null +++ b/groups/doc/INSTALL @@ -0,0 +1,56 @@ +== Redmine installation + +Redmine - project management software +Copyright (C) 2006-2007 Jean-Philippe Lang +http://www.redmine.org/ + + +== Requirements + +* Ruby on Rails 2.0.2 +* A database (see compatibility below) + +Optional: +* SVN binaries >= 1.3 (needed for repository browsing, must be available in PATH) +* RMagick (gantt export to png) + +Supported databases: +* MySQL (tested with MySQL 5) +* PostgreSQL (tested with PostgreSQL 8.1) +* SQLite (tested with SQLite 3) + + +== 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. 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 + +6. Use default administrator account to log in: + login: admin + password: admin + +7. Go to "Administration" to load the default configuration data (roles, + trackers, statuses, workflow) and adjust application settings + + +== SMTP server Configuration + +In config/environment.rb, you can set parameters for your SMTP server: +config.action_mailer.smtp_settings: SMTP server configuration +config.action_mailer.perform_deliveries: set to false to disable mail delivering + +Don't forget to restart the application after any change to this file. diff --git a/groups/doc/RUNNING_TESTS b/groups/doc/RUNNING_TESTS new file mode 100644 index 000000000..7a5e2b992 --- /dev/null +++ b/groups/doc/RUNNING_TESTS @@ -0,0 +1,37 @@ +Creating test repositories +=================== + +mkdir tmp/test + +Subversion +---------- +svnadmin create tmp/test/subversion_repository +gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository + +CVS +--- +gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test + +Bazaar +------ +gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test + +Mercurial +--------- +gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test + +Git +--- +gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test + + +Running Tests +============= + +Run + + rake --tasks | grep test + +to see available tests. + +RAILS_ENV=test rake test will run tests. diff --git a/groups/doc/UPGRADING b/groups/doc/UPGRADING new file mode 100644 index 000000000..2edb2952a --- /dev/null +++ b/groups/doc/UPGRADING @@ -0,0 +1,30 @@ +== 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) + into the new config directory + +4. Enter your SMTP settings in config/environment.rb + Do not replace this file with the old one + +5. Migrate your database (please make a backup before doing this): + rake db:migrate RAILS_ENV="production" + +6. Copy the RAILS_ROOT/files directory content into your new installation + This directory contains all the attached files + + +== Notes + +1. Rails 2.0.2 is required for version 0.7 and later. + +2. When upgrading your code with svn update, don't forget to clear + the application cache (RAILS_ROOT/tmp/cache) before restarting. diff --git a/groups/extra/sample_plugin/README b/groups/extra/sample_plugin/README new file mode 100644 index 000000000..fe3fac736 --- /dev/null +++ b/groups/extra/sample_plugin/README @@ -0,0 +1,21 @@ +== Sample plugin + +This is a sample plugin for Redmine + +== Installation + +=== Adding plugin support to Redmine + +To add plugin support to Redmine, install engines plugin: + See: http://rails-engines.org/ + +=== Plugin 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 on 'Admin -> Information' screen. diff --git a/groups/extra/sample_plugin/app/controllers/example_controller.rb b/groups/extra/sample_plugin/app/controllers/example_controller.rb new file mode 100644 index 000000000..0c6cd5b91 --- /dev/null +++ b/groups/extra/sample_plugin/app/controllers/example_controller.rb @@ -0,0 +1,19 @@ +# Sample plugin controller +class ExampleController < ApplicationController + unloadable + + layout 'base' + before_filter :find_project, :authorize + + 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 diff --git a/groups/extra/sample_plugin/app/views/example/say_goodbye.rhtml b/groups/extra/sample_plugin/app/views/example/say_goodbye.rhtml new file mode 100644 index 000000000..3f4d63dae --- /dev/null +++ b/groups/extra/sample_plugin/app/views/example/say_goodbye.rhtml @@ -0,0 +1,5 @@ +

<%= l(:text_say_goodbye) %>

+ +<% content_for :header_tags do %> + <%= stylesheet_link_tag "example.css", :plugin => "sample_plugin", :media => "screen" %> +<% end %> diff --git a/groups/extra/sample_plugin/app/views/example/say_hello.rhtml b/groups/extra/sample_plugin/app/views/example/say_hello.rhtml new file mode 100644 index 000000000..17aca7bc4 --- /dev/null +++ b/groups/extra/sample_plugin/app/views/example/say_hello.rhtml @@ -0,0 +1,9 @@ +

<%= l(:text_say_hello) %>

+ +

: <%= @value %>

+ +<%= 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 %> diff --git a/groups/extra/sample_plugin/app/views/settings/_settings.rhtml b/groups/extra/sample_plugin/app/views/settings/_settings.rhtml new file mode 100644 index 000000000..bf06e2666 --- /dev/null +++ b/groups/extra/sample_plugin/app/views/settings/_settings.rhtml @@ -0,0 +1,3 @@ +

<%= text_field_tag 'settings[sample_setting]', @settings['sample_setting'] %>

+ +

<%= text_field_tag 'settings[foo]', @settings['foo'] %>

diff --git a/groups/extra/sample_plugin/assets/images/it_works.png b/groups/extra/sample_plugin/assets/images/it_works.png new file mode 100644 index 000000000..441f368d2 Binary files /dev/null and b/groups/extra/sample_plugin/assets/images/it_works.png differ diff --git a/groups/extra/sample_plugin/assets/stylesheets/example.css b/groups/extra/sample_plugin/assets/stylesheets/example.css new file mode 100644 index 000000000..8038567a4 --- /dev/null +++ b/groups/extra/sample_plugin/assets/stylesheets/example.css @@ -0,0 +1 @@ +.icon-example-works { background-image: url(../images/it_works.png); } diff --git a/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb b/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb new file mode 100644 index 000000000..39d58a649 --- /dev/null +++ b/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb @@ -0,0 +1,13 @@ +# Sample plugin migration +# Use rake db:migrate_plugins to migrate installed plugins +class CreateSomeModels < ActiveRecord::Migration + def self.up + create_table :example_plugin_model, :force => true do |t| + t.column "example_attribute", :integer + end + end + + def self.down + drop_table :example_plugin_model + end +end diff --git a/groups/extra/sample_plugin/init.rb b/groups/extra/sample_plugin/init.rb new file mode 100644 index 000000000..7389aaa6f --- /dev/null +++ b/groups/extra/sample_plugin/init.rb @@ -0,0 +1,25 @@ +# 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/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]} + end + + # A new item is added to the project menu + menu :project_menu, :sample_plugin, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample' +end diff --git a/groups/extra/sample_plugin/lang/en.yml b/groups/extra/sample_plugin/lang/en.yml new file mode 100644 index 000000000..bf62bc344 --- /dev/null +++ b/groups/extra/sample_plugin/lang/en.yml @@ -0,0 +1,4 @@ +# Sample plugin +label_plugin_example: Sample Plugin +text_say_hello: Plugin say 'Hello' +text_say_goodbye: Plugin say 'Good bye' diff --git a/groups/extra/sample_plugin/lang/fr.yml b/groups/extra/sample_plugin/lang/fr.yml new file mode 100644 index 000000000..2c0829c32 --- /dev/null +++ b/groups/extra/sample_plugin/lang/fr.yml @@ -0,0 +1,4 @@ +# Sample plugin +label_plugin_example: Plugin exemple +text_say_hello: Plugin dit 'Bonjour' +text_say_goodbye: Plugin dit 'Au revoir' diff --git a/groups/extra/svn/Redmine.pm b/groups/extra/svn/Redmine.pm new file mode 100644 index 000000000..6f3ba4385 --- /dev/null +++ b/groups/extra/svn/Redmine.pm @@ -0,0 +1,237 @@ +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 + + ## if the module isn't in your perl path + PerlRequire /usr/local/apache/Redmine.pm + ## else + # PerlModule Apache::Authn::Redmine + + 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 + PerlSetVar dsn DBI:mysql:database=databasename;host=my.db.server + ## for postgres + # PerlSetVar dsn DBI:Pg:dbname=databasename;host=my.db.server + + PerlSetVar db_user redmine + PerlSetVar db_pass password + + +To be able to browse repository inside redmine, you must add something +like that : + + + DAV svn + SVNParentPath "/var/svn" + Order deny,allow + Deny from all + # only allow reading orders + + Allow from redmine.server.ip + + + +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 + + 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 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); +# use Apache2::Directive qw(); + +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 1 == $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; + $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 $sth = $dbh->prepare( + "SELECT hashed_password, auth_source_id FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;" + ); + $sth->execute($redmine_user, $project_id); + + my $ret; + while (my @row = $sth->fetchrow_array) { + unless ($row[1]) { + if ($row[0] eq $pass_digest) { + $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($row[1]); + 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(); + + $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 ($dsn, $db_user, $db_pass) = map { $r->dir_config($_) } qw/dsn db_user db_pass/; + return DBI->connect($dsn, $db_user, $db_pass); +} + +1; diff --git a/groups/extra/svn/create_views.sql b/groups/extra/svn/create_views.sql new file mode 100644 index 000000000..ce02e0817 --- /dev/null +++ b/groups/extra/svn/create_views.sql @@ -0,0 +1,24 @@ +/* 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; diff --git a/groups/extra/svn/reposman.pl b/groups/extra/svn/reposman.pl new file mode 100755 index 000000000..b8ce8f8af --- /dev/null +++ b/groups/extra/svn/reposman.pl @@ -0,0 +1,142 @@ +#!/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. + +use strict; +use SOAP::Lite; +use Getopt::Long; +Getopt::Long::Configure ("bundling", "no_auto_abbrev", "no_ignore_case"); +use Pod::Usage; +use vars qw/$VERSION/; + +$VERSION = "1.0"; + +my $warning = "This program is now deprecated. Use the reposman.rb for new features"; +print STDERR "*" x length($warning), "\n", + $warning, "\n", + "*" x length($warning), "\n\n"; + +my %opts = (verbose => 0); +GetOptions(\%opts, 'verbose|v+', 'version|V', 'help|h', 'man|m', 'quiet|q', 'svn-dir|s=s', 'redmine-host|r=s') or pod2usage(2); + +die "$VERSION\n" if $opts{version}; +pod2usage(1) if $opts{help}; +pod2usage( -verbose => 2 ) if $opts{man}; + +my $repos_base = $opts{'svn-dir'}; +my $redmine_host = $opts{'redmine-host'}; + +pod2usage(2) unless $repos_base and $redmine_host; + +unless (-d $repos_base) { + Log(text => "$repos_base doesn't exist", exit => 1); +} + +Log(level => 1, text => "querying redMine for projects..."); +my $wdsl = "http://$redmine_host/sys/service.wsdl"; +my $service = SOAP::Lite->service($wdsl); + +my $projects = $service->Projects(''); +my $project_count = @{$projects}; +Log(level => 1, text => "retrieved $project_count projects"); + +foreach my $project (@{$projects}) { + Log(level => 1, text => "treating project $project->{name}"); + my $repos_name = $project->{identifier}; + + if ($repos_name eq "") { + Log(text => "\tno identifier for project $project->{name}"); + next; + } + + unless ($repos_name =~ /^[a-z0-9\-]+$/) { + Log(text => "\tinvalid identifier for project $project->{name}"); + next; + } + + my $repos_path = "$repos_base/$repos_name"; + + if (-e $repos_path) { + # check unix right and change them if needed + my $other_read = (stat($repos_path))[2] & 00007; + my $right; + + if ($project->{is_public} and not $other_read) { + $right = "0775"; + } elsif (not $project->{is_public} and $other_read) { + $right = "0770"; + } else { + next; + } + + # change mode + system('chmod', '-R', $right, $repos_path) == 0 or + warn("\tunable to change mode on $repos_path : $?\n"), next; + + Log(text => "\tmode change on $repos_path"); + + } else { + # change umask to suit the repository's privacy + $project->{is_public} ? umask 0002 : umask 0007; + + # create the repository + system('svnadmin', 'create', $repos_path) == 0 or + warn("\tsystem svnadmin failed unable to create $repos_path\n"), next; + + # set the group owner + system('chown', '-R', "root:$repos_name", $repos_path) == 0 or + warn("\tunable to create $repos_path : $?\n"), next; + + Log(text => "\trepository $repos_path created"); + } +} + + +sub Log { + my %args = (level => 0, text => '', @_); + + my $level = delete $args{level}; + my $text = delete $args{text}; + return unless $level <= $opts{verbose}; + return if $opts{quiet}; + print "$text\n"; + + exit $args{exit} + if defined $args{exit}; +} + + +__END__ + +=head1 NAME + + reposman - manages your svn repositories with redMine + +=head1 SYNOPSIS + + reposman [options] arguments + example: reposman --svn-dir=/var/svn --redmine-host=redmine.mydomain.foo + reposman -s /var/svn -r redmine.mydomain.foo + +=head1 ARGUMENTS + + -s, --svn-dir=DIR use DIR as base directory for svn repositories + -r, --redmine-host=HOST assume redMine is hosted on HOST + +=head1 OPTIONS + + -v verbose + -V print version and exit + diff --git a/groups/extra/svn/reposman.rb b/groups/extra/svn/reposman.rb new file mode 100755 index 000000000..0b476cdc4 --- /dev/null +++ b/groups/extra/svn/reposman.rb @@ -0,0 +1,234 @@ +#!/usr/bin/ruby + +# == Synopsis +# +# reposman: manages your svn repositories with Redmine +# +# == Usage +# +# reposman [ -h | --help ] [ -v | --verbose ] [ -V | --version ] [ -q | --quiet ] -s /var/svn -r redmine.host.org +# example: reposman --svn-dir=/var/svn --redmine-host=redmine.mydomain.foo +# reposman -s /var/svn -r redmine.mydomain.foo +# +# == Arguments (mandatory) +# +# -s, --svn-dir=DIR +# use DIR as base directory for svn repositories +# +# -r, --redmine-host=HOST +# assume Redmine is hosted on HOST. +# you can use : +# * -r redmine.mydomain.foo (will add http://) +# * -r http://redmine.mydomain.foo +# * -r https://mydomain.foo/redmine +# +# == Options +# +# -o, --owner=OWNER +# owner of the repository. using the rails login allow user to browse +# the repository in Redmine even for private project +# +# -u, --url=URL +# the base url Redmine will use to access your repositories. This +# will be used to register the repository in Redmine so that user +# doesn't need to do anything. reposman will add the identifier to this url : +# +# -u https://my.svn.server/my/reposity/root # if the repository can be access by http +# -u file:///var/svn/ # if the repository is local +# if this option isn't set, reposman won't register the repository +# +# -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 +# + +require 'getoptlong' +require 'rdoc/usage' +require 'soap/wsdlDriver' +require 'find' +require 'etc' + +Version = "1.0" + +opts = GetoptLong.new( + ['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], + ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], + ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT], + ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT], + ['--test', '-t', 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' +$use_groupid = true +$svn_url = false +$test = false + +def log(text,level=0, exit=false) + return if $quiet or level > $verbose + puts text + exit 1 if exit +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 '--url'; $svn_url = arg.dup + when '--verbose'; $verbose += 1 + when '--test'; $test = 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 + +$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", 0, true) +end + +log("querying Redmine for projects...", 1); + +$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://") +$redmine_host.gsub!(/\/$/, '') + +wsdl_url = "#{$redmine_host}/sys/service.wsdl"; + +begin + soap = SOAP::WSDLDriverFactory.new(wsdl_url).create_rpc_driver +rescue => e + log("Unable to connect to #{wsdl_url} : #{e}", 0, true) +end + +projects = soap.Projects + +if projects.nil? + log('no project found, perhaps you forgot to "Enable WS for repository management"', 0, true) +end + +log("retrieved #{projects.size} projects", 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 : 0) + 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}", 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 = $repos_base + "/" + project.identifier + + 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 + 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 + raise "svnadmin create #{repos_path} failed" unless system("svnadmin", "create", repos_path) + end + rescue => e + log("\tunable to create #{repos_path} : #{e}\n") + next + end + + if $svn_url + ret = soap.RepositoryCreated project.identifier, "#{$svn_url}#{project.identifier}" + if ret > 0 + log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}"); + else + log("\trepository #{repos_path} not registered in Redmine. Look in your log to find why."); + end + end + + log("\trepository #{repos_path} created"); + end + +end + diff --git a/groups/extra/svn/svnserve.wrapper b/groups/extra/svn/svnserve.wrapper new file mode 100755 index 000000000..705a17e84 --- /dev/null +++ b/groups/extra/svn/svnserve.wrapper @@ -0,0 +1,25 @@ +#!/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'); diff --git a/groups/files/delete.me b/groups/files/delete.me new file mode 100644 index 000000000..18beddaa8 --- /dev/null +++ b/groups/files/delete.me @@ -0,0 +1 @@ +default directory for uploaded files \ No newline at end of file diff --git a/groups/lang/bg.yml b/groups/lang/bg.yml new file mode 100644 index 000000000..b341d989f --- /dev/null +++ b/groups/lang/bg.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Януари,Февруари,Март,Ðприл,Май,Юни,Юли,ÐвгуÑÑ‚,Септември,Октомври,Ðоември,Декември +actionview_datehelper_select_month_names_abbr: Яну,Фев,Мар,Ðпр,Май,Юни,Юли,Ðвг,Сеп,Окт,Ðое,Дек +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 ден +actionview_datehelper_time_in_words_day_plural: %d дни +actionview_datehelper_time_in_words_hour_about: около Ñ‡Ð°Ñ +actionview_datehelper_time_in_words_hour_about_plural: около %d чаÑа +actionview_datehelper_time_in_words_hour_about_single: около Ñ‡Ð°Ñ +actionview_datehelper_time_in_words_minute: 1 минута +actionview_datehelper_time_in_words_minute_half: половин минута +actionview_datehelper_time_in_words_minute_less_than: по-малко от минута +actionview_datehelper_time_in_words_minute_plural: %d минути +actionview_datehelper_time_in_words_minute_single: 1 минута +actionview_datehelper_time_in_words_second_less_than: по-малко от Ñекунда +actionview_datehelper_time_in_words_second_less_than_plural: по-малко от %d Ñекунди +actionview_instancetag_blank_option: Изберете + +activerecord_error_inclusion: не ÑъщеÑтвува в ÑпиÑъка +activerecord_error_exclusion: е запазено +activerecord_error_invalid: е невалидно +activerecord_error_confirmation: липÑва одобрение +activerecord_error_accepted: трÑбва да Ñе приеме +activerecord_error_empty: не може да е празно +activerecord_error_blank: не може да е празно +activerecord_error_too_long: е прекалено дълго +activerecord_error_too_short: е прекалено къÑо +activerecord_error_wrong_length: е Ñ Ð³Ñ€ÐµÑˆÐ½Ð° дължина +activerecord_error_taken: вече ÑъщеÑтвува +activerecord_error_not_a_number: не е чиÑло +activerecord_error_not_a_date: е невалидна дата +activerecord_error_greater_than_start_date: трÑбва да е Ñлед началната дата +activerecord_error_not_same_project: не е от ÑÑŠÑ‰Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚ +activerecord_error_circular_dependency: Тази Ñ€ÐµÐ»Ð°Ñ†Ð¸Ñ Ñ‰Ðµ доведе до безкрайна завиÑимоÑÑ‚ + +general_fmt_age: %d yr +general_fmt_age_plural: %d yrs +general_fmt_date: %%d.%%m.%%Y +general_fmt_datetime: %%d.%%m.%%Y %%H:%%M +general_fmt_datetime_short: %%b %%d, %%H:%%M +general_fmt_time: %%H:%%M +general_text_No: 'Ðе' +general_text_Yes: 'Да' +general_text_no: 'не' +general_text_yes: 'да' +general_lang_name: 'Bulgarian' +general_csv_separator: ',' +general_csv_encoding: UTF-8 +general_pdf_encoding: UTF-8 +general_day_names: Понеделник,Вторник,СрÑда,Четвъртък,Петък,Събота,ÐÐµÐ´ÐµÐ»Ñ +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 на %s +notice_email_error: Грешка при изпращане на e-mail (%s) +notice_feeds_access_key_reseted: Ð’Ð°ÑˆÐ¸Ñ ÐºÐ»ÑŽÑ‡ за RSS доÑтъп беше променен. + +error_scm_not_found: ÐеÑъщеÑтвуващ обект в хранилището. +error_scm_command_failed: "Грешка при опит за ÐºÐ¾Ð¼ÑƒÐ½Ð¸ÐºÐ°Ñ†Ð¸Ñ Ñ Ñ…Ñ€Ð°Ð½Ð¸Ð»Ð¸Ñ‰Ðµ: %s" + +mail_subject_lost_password: Вашата парола (%s) +mail_body_lost_password: 'За да Ñмените паролата Ñи, използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:' +mail_subject_register: ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð½Ð° профил (%s) +mail_body_register: 'За да активирате профила Ñи използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:' + +gui_validation_error: 1 грешка +gui_validation_error_plural: %d грешки + +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_id: Свързана задача +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_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_last_updates: ПоÑледно обновена +label_last_updates_plural: %d поÑледно обновени +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: %d Download +label_download_plural: %d 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_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: поÑледни %d промени +label_change_view_all: Виж вÑички промени +label_personalize_page: ПерÑонализиране +label_comment: Коментар +label_comment_plural: Коментари +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: %d промÑна +label_modification_plural: %d промени +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_on: 'от' +label_sort_highest: ПремеÑти най-горе +label_sort_higher: ПремеÑти по-горе +label_sort_lower: ПремеÑти по-долу +label_sort_lowest: ПремеÑти най-долу +label_roadmap: Пътна карта +label_roadmap_due_in: Излиза Ñлед +label_roadmap_overdue: %s закъÑнение +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: %.2f Ñ‡Ð°Ñ +label_f_hour_plural: %.2f чаÑа +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: Сортиране по %s +label_send_test_email: Изпращане на теÑтов e-mail +label_feeds_access_key_created_on: %s от Ñъздаването на RSS ключа +label_module_plural: Модули +label_added_time_by: Публикувана от %s преди %s +label_updated_time: Обновена преди %s +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_journal_changed: промÑна от %s на %s +text_journal_set_to: уÑтановено на %s +text_journal_deleted: изтрито +text_tip_task_begin_day: задача започваща този ден +text_tip_task_end_day: задача завършваща този ден +text_tip_task_begin_end_day: задача започваща и завършваща този ден +text_project_identifier_info: 'Позволени Ñа малки букви (a-z), цифри и тирета.
Ðевъзможна промÑна Ñлед запиÑ.' +text_caracters_maximum: До %d Ñимвола. +text_length_between: От %d до %d Ñимвола. +text_tracker_no_workflow: ÐÑма дефиниран работен Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð·Ð° този тракер +text_unallowed_characters: Ðепозволени Ñимволи +text_comma_separated: Позволено е изброÑване (Ñ Ñ€Ð°Ð·Ð´ÐµÐ»Ð¸Ñ‚ÐµÐ» запетаÑ). +text_issues_ref_in_commit_messages: ОтбелÑзване и приключване на задачи от ревизии +text_issue_added: Публикувана е нова задача Ñ Ð½Ð¾Ð¼ÐµÑ€ %s (от %s). +text_issue_updated: Задача %s е обновена (от %s). +text_wiki_destroy_confirmation: Сигурни ли Ñте, че иÑкате да изтриете това Wiki и цÑлото му Ñъдържание? +text_issue_category_destroy_question: Има задачи (%d) обвързани Ñ Ñ‚Ð°Ð·Ð¸ категориÑ. Какво ще изберете? +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_assigned: Възложена +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: "ÐеуÑпешен Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° %d задачи от %d избрани: %s." +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: Можете да използвате Ð²Ð°ÑˆÐ¸Ñ "%s" профил за вход. +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: ЗаÑвка за активиране на профил в %s +mail_body_account_activation_request: 'Има новорегиÑтриран потребител (%s), очакващ вашето одобрение:' +label_registration_automatic_activation: автоматично активиране +label_registration_manual_activation: ръчно активиране +notice_account_pending: "Профилът Ви е Ñъздаден и очаква одобрение от админиÑтратор." +field_time_zone: ЧаÑова зона +text_caracters_minimum: Минимум %d Ñимвола. +setting_bcc_recipients: Получатели на Ñкрито копие (bcc) +button_annotate: ÐÐ½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ +label_issues_by: Задачи по %s +field_searchable: С възможноÑÑ‚ за търÑене +label_display_per_page: 'Ðа Ñтраница по: %s' +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: "Грешка при зареждане на примерната информациÑ: %s" +button_update: ОбновÑване +label_change_properties: ПромÑна на наÑтройки +label_general: ОÑновни +label_repository_plural: Хранилища +label_associated_revisions: ÐÑоциирани ревизии +setting_user_format: ПотребителÑки формат +text_status_changed_by_changeset: Приложено Ñ Ñ€ÐµÐ²Ð¸Ð·Ð¸Ñ %s. +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: поÑледните %d дни +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: %.02f чаÑа Ñа отделени на задачите, които иÑкате да изтриете. Какво избирате? +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): %s will be also deleted.' diff --git a/groups/lang/cs.yml b/groups/lang/cs.yml new file mode 100644 index 000000000..250c602c2 --- /dev/null +++ b/groups/lang/cs.yml @@ -0,0 +1,625 @@ +# CZ translation by Maxim KruÅ¡ina | Massimo Filippi, s.r.o. | maxim@mxm.cz +# Based on original CZ translation by Jan KadleÄek + +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Leden,Únor,BÅ™ezen,Duben,KvÄ›ten,ÄŒerven,ÄŒervenec,Srpen,Září,Říjen,Listopad,Prosinec +actionview_datehelper_select_month_names_abbr: Led,Úno,BÅ™e,Dub,KvÄ›,ÄŒer,ÄŒvc,Srp,Zář,Říj,Lis,Pro +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 den +actionview_datehelper_time_in_words_day_plural: %d dny +actionview_datehelper_time_in_words_hour_about: asi hodinou +actionview_datehelper_time_in_words_hour_about_plural: asi %d hodinami +actionview_datehelper_time_in_words_hour_about_single: asi hodinou +actionview_datehelper_time_in_words_minute: 1 minutou +actionview_datehelper_time_in_words_minute_half: půl minutou +actionview_datehelper_time_in_words_minute_less_than: ménÄ› než minutou +actionview_datehelper_time_in_words_minute_plural: %d minutami +actionview_datehelper_time_in_words_minute_single: 1 minutou +actionview_datehelper_time_in_words_second_less_than: ménÄ› než sekundou +actionview_datehelper_time_in_words_second_less_than_plural: ménÄ› než %d sekundami +actionview_instancetag_blank_option: Prosím vyberte + +activerecord_error_inclusion: není zahrnuto v seznamu +activerecord_error_exclusion: je rezervováno +activerecord_error_invalid: je neplatné +activerecord_error_confirmation: se neshoduje s potvrzením +activerecord_error_accepted: musí být akceptováno +activerecord_error_empty: nemůže být prázdný +activerecord_error_blank: nemůže být prázdný +activerecord_error_too_long: je příliÅ¡ dlouhý +activerecord_error_too_short: je příliÅ¡ krátký +activerecord_error_wrong_length: má chybnou délku +activerecord_error_taken: je již použito +activerecord_error_not_a_number: není Äíslo +activerecord_error_not_a_date: není platné datum +activerecord_error_greater_than_start_date: musí být vÄ›tší než poÄáteÄní datum +activerecord_error_not_same_project: nepatří stejnému projektu +activerecord_error_circular_dependency: Tento vztah by vytvoÅ™il cyklickou závislost + +general_fmt_age: %d rok +general_fmt_age_plural: %d roků +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: UTF-8 +general_pdf_encoding: UTF-8 +general_day_names: PondÄ›lí,Úterý,StÅ™eda,ÄŒtvrtek,Pátek,Sobota,NedÄ›le +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 %s byl odeslán email +notice_email_error: PÅ™i odesílání emailu nastala chyba (%s) +notice_feeds_access_key_reseted: Váš klÃ­Ä pro přístup k RSS byl resetován. +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +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: %s" +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Ä›: %s" +error_issue_not_found_in_project: 'Úkol nebyl nalezen nebo nepatří k tomuto projektu' + +mail_subject_lost_password: VaÅ¡e heslo (%s) +mail_body_lost_password: 'Pro zmÄ›nu vaÅ¡eho hesla kliknÄ›te na následující odkaz:' +mail_subject_register: Aktivace úÄtu (%s) +mail_body_register: 'Pro aktivaci vaÅ¡eho úÄtu kliknÄ›te na následující odkaz:' +mail_body_account_information_external: Pomocí vaÅ¡eho úÄtu "%s" se můžete pÅ™ihlásit. +mail_body_account_information: Informace o vaÅ¡em úÄtu +mail_subject_account_activation_request: Aktivace %s úÄtu +mail_body_account_activation_request: Byl zaregistrován nový uživatel "%s". Aktivace jeho úÄtu závisí na vaÅ¡em potvrzení. + +gui_validation_error: 1 chyba +gui_validation_error_plural: %d 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_id: 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_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 %s +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_last_updates: Poslední zmÄ›na +label_last_updates_plural: %d poslední zmÄ›ny +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: %d Download +label_download_plural: %d 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_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 %d 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_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 %d 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: %d zmÄ›na +label_modification_plural: %d 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_on: 'zapnuto' +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á +label_roadmap_overdue: %s 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: %.2f hodina +label_f_hour_plural: %.2f 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 %s +label_send_test_email: Poslat testovací email +label_feeds_access_key_created_on: Přístupový klÃ­Ä pro RSS byl vytvoÅ™en pÅ™ed %s +label_module_plural: Moduly +label_added_time_by: 'PÅ™idáno uživatelem %s pÅ™ed %s' +label_updated_time: 'Aktualizováno pÅ™ed %s' +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: '%s 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_journal_changed: zmÄ›nÄ›no z %s na %s +text_journal_set_to: nastaveno na %s +text_journal_deleted: odstranÄ›no +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.
Po uložení již není možné identifikátor zmÄ›nit.' +text_caracters_maximum: %d znaků maximálnÄ›. +text_caracters_minimum: Musí být alespoň %d znaků dlouhé. +text_length_between: Délka mezi %d a %d 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 %s byl vytvoÅ™en uživatelem %s. +text_issue_updated: Úkol %s byl aktualizován uživatelem %s. +text_wiki_destroy_confirmation: Opravdu si pÅ™ejete odstranit tuto WIKI a celý její obsah? +text_issue_category_destroy_question: NÄ›které úkoly (%d) 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 %s. +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 %.02f 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_assigned: PÅ™iÅ™azený +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: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lang/da.yml b/groups/lang/da.yml new file mode 100644 index 000000000..ff2ed982d --- /dev/null +++ b/groups/lang/da.yml @@ -0,0 +1,622 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Januar,Februar,Marts,April,Maj,Juni,Juli,August,September,Oktober,November,December +actionview_datehelper_select_month_names_abbr: Jan,Feb,Mar,Apr,Maj,Jun,Jul,Aug,Sep,Okt,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dag +actionview_datehelper_time_in_words_day_plural: %d dage +actionview_datehelper_time_in_words_hour_about: cirka en time +actionview_datehelper_time_in_words_hour_about_plural: cirka %d timer +actionview_datehelper_time_in_words_hour_about_single: cirka en time +actionview_datehelper_time_in_words_minute: 1 minut +actionview_datehelper_time_in_words_minute_half: et halvt minut +actionview_datehelper_time_in_words_minute_less_than: mindre end et minut +actionview_datehelper_time_in_words_minute_plural: %d minutter +actionview_datehelper_time_in_words_minute_single: 1 minut +actionview_datehelper_time_in_words_second_less_than: mindre end et sekund +actionview_datehelper_time_in_words_second_less_than_plural: mindre end %d sekunder +actionview_instancetag_blank_option: Vælg venligst + +activerecord_error_inclusion: er ikke i listen +activerecord_error_exclusion: er reserveret +activerecord_error_invalid: er ugyldig +activerecord_error_confirmation: passer ikke bekræftelsen +activerecord_error_accepted: skal accepteres +activerecord_error_empty: kan ikke være tom +activerecord_error_blank: kan ikke være blank +activerecord_error_too_long: er for lang +activerecord_error_too_short: er for kort +activerecord_error_wrong_length: har den forkerte længde +activerecord_error_taken: er allerede valgt +activerecord_error_not_a_number: er ikke et nummer +activerecord_error_not_a_date: er en ugyldig dato +activerecord_error_greater_than_start_date: skal være senere end start datoen +activerecord_error_not_same_project: høre ikke til samme projekt +activerecord_error_circular_dependency: Denne relation vil skabe et afhængigheds forhold + +general_fmt_age: %d Ã¥r +general_fmt_age_plural: %d Ã¥r +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_day_names: Mandag,Tirsdag,Onsdag,Torsdag,Fredag,Lørdag,Søndag +general_first_day_of_week: '1' + +notice_account_updated: Kontoen er opdateret. +notice_account_invalid_creditentials: Ugyldig bruger og 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, ska du klikke pÃ¥ linket i den tilsendte email. +notice_account_unknown_email: Ukendt bruger. +notice_can_t_change_password: Denne konto benytter en ekstern sikkerheds godkendelse. 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 oprettelsen. +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 ike adgang til denne side. +notice_email_sent: En email er sendt til %s +notice_email_error: En fejl opstod under afsendelse af email (%s) +notice_feeds_access_key_reseted: Din RSS adgangs nøgle er nulstillet. +notice_failed_to_save_issues: "Det mislykkedes at gemme %d sage(r) pÃ¥ %d valgt: %s." +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 administratorens godkendelse." +notice_default_data_loaded: Default konfiguration er indlæst. + +error_can_t_load_default_data: "Standard konfiguration kunne ikke indlæses: %s" +error_scm_not_found: "Adgang og/eller revision blev ikke fundet i det valgte repository." +error_scm_command_failed: "En fejl opstod under fobindelsen til det valgte repository: %s" + +mail_subject_lost_password: Dit %s kodeord +mail_body_lost_password: 'For at ændre dit kodeord, klik pÃ¥ dette link:' +mail_subject_register: %s konto aktivering +mail_body_register: 'For at aktivere din konto, klik pÃ¥ dette link:' +mail_body_account_information_external: Du kan bruge din "%s" konto til at logge ind. +mail_body_account_information: Din konto information +mail_subject_account_activation_request: %s konto aktivering +mail_body_account_activation_request: 'En ny bruger (%s) er registreret. Godkend venligst kontoen:' + +gui_validation_error: 1 fejl +gui_validation_error_plural: %d 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: Minimum længde +field_max_length: Maximal 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: Standard væ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 notifikationer +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 bruger oprettelse +field_start_date: Start +field_done_ratio: %% Færdig +field_auth_source: Sikkerheds metode +field_hide_mail: Skjul min email +field_comments: Kommentar +field_url: URL +field_start_page: Start side +field_subproject: Underprojekt +field_hours: Timer +field_activity: Aktivitet +field_spent_on: Dato +field_identifier: Identificering +field_is_filter: Brugt som et filter +field_issue_to_id: Beslægtede sag +field_delay: Udsættelse +field_assignable: Sager kan tildeles denne rolle +field_redirect_existing_links: Videresend eksisterende links +field_estimated_hours: Estimeret tid +field_column_names: Kolonner +field_time_zone: Tids zone +field_searchable: Søgbar +field_default_value: Standard værdi + +setting_app_title: Applikations titel +setting_app_subtitle: Applikations undertekst +setting_welcome_text: Velkomst tekst +setting_default_language: Standard sporg +setting_login_required: Sikkerhed pÃ¥krævet +setting_self_registration: Bruger oprettelse +setting_attachment_max_size: Vedhæftede filers max størrelse +setting_issues_export_limit: Sags eksporterings begrænsning +setting_mail_from: Afsender email +setting_bcc_recipients: Blind carbon copy modtager (bcc) +setting_host_name: Værts navn +setting_text_formatting: Tekst formattering +setting_wiki_compression: Wiki historik komprimering +setting_feeds_limit: Feed indholds begrænsning +setting_autofetch_changesets: Automatisk hent commits +setting_sys_api_enabled: Aktiver web service for automatisk repository administration +setting_commit_ref_keywords: Reference nøgleord +setting_commit_fix_keywords: Afslutnings nøgleord +setting_autologin: Autologin +setting_date_format: Dato format +setting_time_format: Tids format +setting_cross_project_issue_relations: Tillad sags relationer pÃ¥ tværs af projekter +setting_issue_list_default_columns: Standard kolonner pÃ¥ sags listen +setting_repositories_encodings: Repository tegnsæt +setting_emails_footer: Email fodnote +setting_protocol: Protokol +setting_per_page_options: Objekter pr. side indstillinger +setting_user_format: Bruger visnings format + +project_module_issue_tracking: Sags søgning +project_module_time_tracking: Tids styring +project_module_news: Nyheder +project_module_documents: Dokumenter +project_module_files: Filer +project_module_wiki: Wiki +project_module_repository: Repository +project_module_boards: Opslagstavle + +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_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 %s +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: Sags status +label_issue_status_plural: Sags statuser +label_issue_status_new: Ny status +label_issue_category: Sags kategori +label_issue_category_plural: Sags kategorier +label_issue_category_new: Ny kategori +label_custom_field: Brugerdefineret felt +label_custom_field_plural: Brugerdefineret felt +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: Registrer +label_password_lost: Glemt kodeord +label_home: Forside +label_my_page: Min side +label_my_account: Min konto +label_my_projects: Mine projekter +label_administration: Administration +label_login: Log ind +label_logout: Log ud +label_help: Hjælp +label_reported_issues: Rapporterede sager +label_assigned_to_me_issues: Sager tildelt til mig +label_last_login: Sidste forbindelse +label_last_updates: Sidst opdateret +label_last_updates_plural: %d sidst opdateret +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: Sikkerheds metode +label_auth_source_new: Ny sikkerheds metode +label_auth_source_plural: Sikkerheds metoder +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: %d Download +label_download_plural: %d Downloads +label_no_data: Ingen data at vise +label_change_status: Ændrings status +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: Version +label_version_new: Ny version +label_version_plural: Versioner +label_confirmation: Bekræftigelser +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_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 en 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 %d ændringer +label_change_view_all: Vis alle ændringer +label_personalize_page: Tilret denne side +label_comment: Kommentar +label_comment_plural: 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: idag +label_all_time: altid +label_yesterday: igÃ¥r +label_this_week: denne uge +label_last_week: sidste uge +label_last_n_days: sidste %d 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: %d ændring +label_modification_plural: %d æ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: Maximal størrelse +label_on: 'til' +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: %s forsinket +label_roadmap_no_issues: Ingen sager til 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 ænringer +label_issue_tracking: Sags søgning +label_spent_time: Brugt tid +label_f_hour: %.2f time +label_f_hour_plural: %.2f timer +label_time_tracking: Tids styring +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 forskellighed +label_diff_inline: inline +label_diff_side_by_side: side ved side +label_options: Optioner +label_copy_workflow_from: Kopier arbejdsgang fra +label_permissions_report: Godkendelses rapport +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: Forblin 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: Sorter efter %s +label_send_test_email: Send en test email +label_feeds_access_key_created_on: RSS adgangsnøgle genereret %s siden +label_module_plural: Moduler +label_added_time_by: Tilføjet af %s for %s siden +label_updated_time: Opdateret for %s 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, kun 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: konto aktivering pÃ¥ email +label_registration_manual_activation: manuel konto aktivering +label_registration_automatic_activation: automatisk konto aktivering +label_display_per_page: 'Per side: %s' +label_age: Alder +label_change_properties: Ændre indstillinger +label_general: Generalt +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: Annuller +button_activate: Aktiver +button_sort: Sorter +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: Arkiver +button_unarchive: Fjern fra arkiv +button_reset: Nulstil +button_rename: Omdøb +button_change_password: Skift kodeord +button_copy: Kopier +button_annotate: Annotere +button_update: Opdater +button_configure: Konfigurer + +status_active: aktiv +status_registered: registreret +status_locked: lÃ¥st + +text_select_mail_notifications: Vælg handlinger for hvilke, der skal sendes en email besked. +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Ã¥ di 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_journal_changed: ændret fra %s til %s +text_journal_set_to: sat til %s +text_journal_deleted: slettet +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.
Når den er gemt, kan indifikatoren ikke rettes.' +text_caracters_maximum: max %d karakterer. +text_caracters_minimum: Skal være mindst %d karakterer lang. +text_length_between: Længde skal være mellem %d og %d karakterer. +text_tracker_no_workflow: Ingen arbejdsgang defineret for denne type +text_unallowed_characters: Ikke tilladte karakterer +text_comma_separated: Adskillige værdier tilladt (komma separeret). +text_issues_ref_in_commit_messages: Referer og løser sager i commit beskeder +text_issue_added: Sag %s er rapporteret af %s. +text_issue_updated: Sag %s er blevet opdateret af %s. +text_wiki_destroy_confirmation: Er du sikker på at du vil slette debbe wiki, og alt indholdet ? +text_issue_category_destroy_question: Nogle sgaer (%d) er tildelt denne kategori. Hvad ønsker du at gøre ? +text_issue_category_destroy_assignments: Slet kategori tildelinger +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 ahr indberettet eller ejer)." +text_no_configuration_data: "Roller, typer, sags statuser og arbejdsgange er endnu ikek konfigureret.\nDet er anbefalet at indlæse standard konfigurationen. Du vil kunne ændre denne når den er indlæst." +text_load_default_configuration: Indlæs standard konfiguration +text_status_changed_by_changeset: Anvendt i ændring %s. +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 administrator konto æ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_assigned: Tildelt +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: Bruger dokumentation +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: Sags prioriteter +enumeration_doc_categories: Dokument kategorier +enumeration_activities: Aktiviteter (tids styring) + +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 projekt aktivitet +text_destroy_time_entries_question: %.02f timer er reporteret 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 default +label_optional_description: Optionel beskrivelse +text_destroy_time_entries: Slet raportede timer +field_comments_sorting: Vis kommentar +text_reassign_time_entries: 'Tildel raportede 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 default +error_scm_annotate: "The entry does not exist or can not be annotated." +label_planning: Planlægning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lang/de.yml b/groups/lang/de.yml new file mode 100644 index 000000000..77184cf88 --- /dev/null +++ b/groups/lang/de.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Januar,Februar,März,April,Mai,Juni,Juli,August,September,Oktober,November,Dezember +actionview_datehelper_select_month_names_abbr: Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 Tag +actionview_datehelper_time_in_words_day_plural: %d Tagen +actionview_datehelper_time_in_words_hour_about: ungefähr einer Stunde +actionview_datehelper_time_in_words_hour_about_plural: ungefähr %d Stunden +actionview_datehelper_time_in_words_hour_about_single: ungefähr einer Stunde +actionview_datehelper_time_in_words_minute: 1 Minute +actionview_datehelper_time_in_words_minute_half: einer halben Minute +actionview_datehelper_time_in_words_minute_less_than: weniger als einer Minute +actionview_datehelper_time_in_words_minute_plural: %d Minuten +actionview_datehelper_time_in_words_minute_single: 1 Minute +actionview_datehelper_time_in_words_second_less_than: weniger als einer Sekunde +actionview_datehelper_time_in_words_second_less_than_plural: weniger als %d Sekunden +actionview_instancetag_blank_option: Bitte auswählen + +activerecord_error_inclusion: ist nicht inbegriffen +activerecord_error_exclusion: ist reserviert +activerecord_error_invalid: ist unzulässig +activerecord_error_confirmation: Bestätigung nötig +activerecord_error_accepted: muss angenommen werden +activerecord_error_empty: darf nicht leer sein +activerecord_error_blank: darf nicht leer sein +activerecord_error_too_long: ist zu lang +activerecord_error_too_short: ist zu kurz +activerecord_error_wrong_length: hat die falsche Länge +activerecord_error_taken: ist bereits vergeben +activerecord_error_not_a_number: ist keine Zahl +activerecord_error_not_a_date: ist kein gültiges Datum +activerecord_error_greater_than_start_date: muss größer als Anfangsdatum sein +activerecord_error_not_same_project: gehört nicht zum selben Projekt +activerecord_error_circular_dependency: Diese Beziehung würde eine zyklische Abhängigkeit erzeugen + +general_fmt_age: %d Jahr +general_fmt_age_plural: %d Jahre +general_fmt_date: %%d.%%m.%%y +general_fmt_datetime: %%d.%%m.%%y, %%H:%%M +general_fmt_datetime_short: %%d.%%m, %%H:%%M +general_fmt_time: %%H:%%M +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag,Sonntag +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 besteht 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 %s gesendet. +notice_email_error: Beim Senden einer E-Mail ist ein Fehler aufgetreten (%s). +notice_feeds_access_key_reseted: Ihr Atom-Zugriffsschlüssel wurde zurückgesetzt. +notice_failed_to_save_issues: "%d von %d ausgewählten Tickets konnte(n) nicht gespeichert werden: %s." +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. + +error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %s" +error_scm_not_found: Eintrag und/oder Revision besteht nicht im Projektarchiv. +error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %s" +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.' + +mail_subject_lost_password: Ihr %s Kennwort +mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:' +mail_subject_register: %s 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 "%s" an anmelden. +mail_body_account_information: Ihre Konto-Informationen +mail_subject_account_activation_request: Antrag auf %s Kontoaktivierung +mail_body_account_activation_request: 'Ein neuer Benutzer (%s) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:' + +gui_validation_error: 1 Fehler +gui_validation_error_plural: %d 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_id: 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 + +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_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_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 + +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_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 %s +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_last_updates: zuletzt aktualisiert +label_last_updates_plural: %d zuletzt aktualisierten +label_registered_on: Angemeldet am +label_activity: Aktivität +label_overall_activity: Aktivität aller Projekte anzeigen +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_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: %d Download +label_download_plural: %d 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_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 +label_internal: Intern +label_last_changes: %d letzte Änderungen +label_change_view_all: Alle Änderungen anzeigen +label_personalize_page: Diese Seite anpassen +label_comment: Kommentar +label_comment_plural: Kommentare +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 %d 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: %d Änderung +label_modification_plural: %d Änderungen +label_revision: Revision +label_revision_plural: Revisionen +label_associated_revisions: Zugehörige Revisionen +label_added: hinzugefügt +label_modified: geändert +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_on: von +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 +label_roadmap_overdue: %s 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: %.2f Stunde +label_f_hour_plural: %.2f 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_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: Nachrichten +label_message_last: Letzte Nachricht +label_message_new: Neue Nachricht +label_message_posted: Forums-Beitrag 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 %s +label_send_test_email: Test-E-Mail senden +label_feeds_access_key_created_on: Atom-Zugriffsschlüssel vor %s erstellt +label_module_plural: Module +label_added_time_by: Von %s vor %s hinzugefügt +label_updated_time: Vor %s 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: %s' +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 + +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 + +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_workflow_edit: Workflow zum Bearbeiten auswählen +text_are_you_sure: Sind Sie sicher? +text_journal_changed: geändert von %s zu %s +text_journal_set_to: gestellt zu %s +text_journal_deleted: gelöscht +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.
Einmal gespeichert, kann die Kennung nicht mehr geändert werden.' +text_caracters_maximum: Max. %d Zeichen. +text_caracters_minimum: Muss mindestens %d Zeichen lang sein. +text_length_between: Länge zwischen %d und %d 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 %s wurde erstellt by %s. +text_issue_updated: Ticket %s wurde aktualisiert by %s. +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 (%d) 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 %s. +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_rmagick_available: RMagick verfügbar (optional) +text_destroy_time_entries_question: Es wurden bereits %.02f 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:' + +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_assigned: Zugewiesen +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) +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lang/en.yml b/groups/lang/en.yml new file mode 100644 index 000000000..e39aec301 --- /dev/null +++ b/groups/lang/en.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: January,February,March,April,May,June,July,August,September,October,November,December +actionview_datehelper_select_month_names_abbr: Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 day +actionview_datehelper_time_in_words_day_plural: %d days +actionview_datehelper_time_in_words_hour_about: about an hour +actionview_datehelper_time_in_words_hour_about_plural: about %d hours +actionview_datehelper_time_in_words_hour_about_single: about an hour +actionview_datehelper_time_in_words_minute: 1 minute +actionview_datehelper_time_in_words_minute_half: half a minute +actionview_datehelper_time_in_words_minute_less_than: less than a minute +actionview_datehelper_time_in_words_minute_plural: %d minutes +actionview_datehelper_time_in_words_minute_single: 1 minute +actionview_datehelper_time_in_words_second_less_than: less than a second +actionview_datehelper_time_in_words_second_less_than_plural: less than %d seconds +actionview_instancetag_blank_option: Please select + +activerecord_error_inclusion: is not included in the list +activerecord_error_exclusion: is reserved +activerecord_error_invalid: is invalid +activerecord_error_confirmation: doesn't match confirmation +activerecord_error_accepted: must be accepted +activerecord_error_empty: can't be empty +activerecord_error_blank: can't be blank +activerecord_error_too_long: is too long +activerecord_error_too_short: is too short +activerecord_error_wrong_length: is the wrong length +activerecord_error_taken: has already been taken +activerecord_error_not_a_number: is not a number +activerecord_error_not_a_date: is not a valid date +activerecord_error_greater_than_start_date: must be greater than start date +activerecord_error_not_same_project: doesn't belong to the same project +activerecord_error_circular_dependency: This relation would create a circular dependency + +general_fmt_age: %d yr +general_fmt_age_plural: %d yrs +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday +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 %s +notice_email_error: An error occurred while sending mail (%s) +notice_feeds_access_key_reseted: Your RSS access key was reset. +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +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. + +error_can_t_load_default_data: "Default configuration could not be loaded: %s" +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: %s" +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' + +mail_subject_lost_password: Your %s password +mail_body_lost_password: 'To change your password, click on the following link:' +mail_subject_register: Your %s account activation +mail_body_register: 'To activate your account, click on the following link:' +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account is pending your approval:' + +gui_validation_error: 1 error +gui_validation_error_plural: %d 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_id: 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 + +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_host_name: Host name +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_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 + +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_project: Project +label_project_new: New project +label_project_plural: 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 %s +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_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_last_updates: Last updated +label_last_updates_plural: %d last updated +label_registered_on: Registered on +label_activity: Activity +label_overall_activity: Overall 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_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: %d Download +label_download_plural: %d 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_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 %d changes +label_change_view_all: View all changes +label_personalize_page: Personalize this page +label_comment: Comment +label_comment_plural: 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_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 %d 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: %d change +label_modification_plural: %d changes +label_revision: Revision +label_revision_plural: Revisions +label_associated_revisions: Associated revisions +label_added: added +label_modified: modified +label_deleted: deleted +label_latest_revision: Latest revision +label_latest_revision_plural: Latest revisions +label_view_revisions: View revisions +label_max_size: Maximum size +label_on: 'on' +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 +label_roadmap_overdue: %s 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: %.2f hour +label_f_hour_plural: %.2f 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_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 %s +label_send_test_email: Send a test email +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: Modules +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s 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: %s' +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 + +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_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_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 + +status_active: active +status_registered: registered +status_locked: locked + +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): %s 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: changed from %s to %s +text_journal_set_to: set to %s +text_journal_deleted: deleted +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: 'Lower case letters (a-z), numbers and dashes allowed.
Once saved, the identifier can not be changed.' +text_caracters_maximum: %d characters maximum. +text_caracters_minimum: Must be at least %d characters long. +text_length_between: Length between %d and %d 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 %s has been reported by %s. +text_issue_updated: Issue %s has been updated by %s. +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) 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 %s. +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' +text_select_project_modules: 'Select modules to enable for this project:' +text_default_administrator_account_changed: Default administrator account changed +text_file_repository_writable: File repository writable +text_rmagick_available: RMagick available (optional) +text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ? +text_destroy_time_entries: Delete reported hours +text_assign_time_entries_to_project: Assign reported hours to the project +text_reassign_time_entries: 'Reassign reported hours to this issue:' + +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_assigned: Assigned +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) diff --git a/groups/lang/es.yml b/groups/lang/es.yml new file mode 100644 index 000000000..c6eef021a --- /dev/null +++ b/groups/lang/es.yml @@ -0,0 +1,623 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Enero,Febrero,Marzo,Abril,Mayo,Junio,Julio,Agosto,Septiembre,Octubre,Noviembre,Diciembre +actionview_datehelper_select_month_names_abbr: Ene,Feb,Mar,Abr,Mayo,Jun,Jul,Ago,Sep,Oct,Nov,Dic +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 día +actionview_datehelper_time_in_words_day_plural: %d días +actionview_datehelper_time_in_words_hour_about: una hora aproximadamente +actionview_datehelper_time_in_words_hour_about_plural: aproximadamente %d horas +actionview_datehelper_time_in_words_hour_about_single: una hora aproximadamente +actionview_datehelper_time_in_words_minute: 1 minuto +actionview_datehelper_time_in_words_minute_half: medio minuto +actionview_datehelper_time_in_words_minute_less_than: menos de un minuto +actionview_datehelper_time_in_words_minute_plural: %d minutos +actionview_datehelper_time_in_words_minute_single: 1 minuto +actionview_datehelper_time_in_words_second_less_than: menos de un segundo +actionview_datehelper_time_in_words_second_less_than_plural: menos de %d segundos +actionview_instancetag_blank_option: Por favor seleccione + +activerecord_error_inclusion: no está incluído en la lista +activerecord_error_exclusion: está reservado +activerecord_error_invalid: no es válido +activerecord_error_confirmation: la confirmación no coincide +activerecord_error_accepted: debe ser aceptado +activerecord_error_empty: no puede estar vacío +activerecord_error_blank: no puede estar en blanco +activerecord_error_too_long: es demasiado largo +activerecord_error_too_short: es demasiado corto +activerecord_error_wrong_length: la longitud es incorrecta +activerecord_error_taken: ya está siendo usado +activerecord_error_not_a_number: no es un número +activerecord_error_not_a_date: no es una fecha válida +activerecord_error_greater_than_start_date: debe ser la fecha mayor que del comienzo +activerecord_error_not_same_project: no pertenece al mismo proyecto +activerecord_error_circular_dependency: Esta relación podría crear una dependencia anidada + +general_fmt_age: %d año +general_fmt_age_plural: %d años +general_fmt_date: %%d/%%m/%%Y +general_fmt_datetime: %%d/%%m/%%Y %%H:%%M +general_fmt_datetime_short: %%d/%%m %%H:%%M +general_fmt_time: %%H:%%M +general_text_No: 'No' +general_text_Yes: 'Sí' +general_text_no: 'no' +general_text_yes: 'sí' +general_lang_name: 'Español' +general_csv_separator: ';' +general_csv_encoding: ISO-8859-15 +general_pdf_encoding: ISO-8859-15 +general_day_names: Lunes,Martes,Miércoles,Jueves,Viernes,Sábado,Domingo +general_first_day_of_week: '1' + +notice_account_updated: Cuenta actualizada correctamente. +notice_account_invalid_creditentials: Usuario o contraseña inválido. +notice_account_password_updated: Contraseña modificada correctamente. +notice_account_wrong_password: Contraseña incorrecta. +notice_account_register_done: Cuenta creada correctamente. +notice_account_unknown_email: Usuario desconocido. +notice_can_t_change_password: Esta cuenta utiliza una fuente de autenticación externa. No es posible cambiar la contraseña. +notice_account_lost_email_sent: Se le ha enviado un correo con instrucciones para elegir una nueva contraseña. +notice_account_activated: Su cuenta ha sido activada. Ahora se encuentra conectado. +notice_successful_create: Creación correcta. +notice_successful_update: Modificación correcta. +notice_successful_delete: Borrado correcto. +notice_successful_connection: Conexión correcta. +notice_file_not_found: La página a la que intentas acceder no existe. +notice_locking_conflict: Los datos han sido modificados por otro usuario. +notice_not_authorized: No tiene autorización para acceder a esta página. + +error_scm_not_found: "La entrada y/o la revisión no existe en el repositorio." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Tu contraseña del %s +mail_body_lost_password: 'Para cambiar su contraseña, haga click en el siguiente enlace:' +mail_subject_register: Activación de la cuenta del %s +mail_body_register: 'Para activar su cuenta, haga click en el siguiente enlace:' + +gui_validation_error: 1 error +gui_validation_error_plural: %d errores + +field_name: Nombre +field_description: Descripción +field_summary: Resumen +field_is_required: Obligatorio +field_firstname: Nombre +field_lastname: Apellido +field_mail: Correo electrónico +field_filename: Fichero +field_filesize: Tamaño +field_downloads: Descargas +field_author: Autor +field_created_on: Creado +field_updated_on: Actualizado +field_field_format: Formato +field_is_for_all: Para todos los proyectos +field_possible_values: Valores posibles +field_regexp: Expresión regular +field_min_length: Longitud mínima +field_max_length: Longitud máxima +field_value: Valor +field_category: Categoría +field_title: Título +field_project: Proyecto +field_issue: Petición +field_status: Estado +field_notes: Notas +field_is_closed: Petición resuelta +field_is_default: Estado por defecto +field_tracker: Tracker +field_subject: Tema +field_due_date: Fecha fin +field_assigned_to: Asignado a +field_priority: Prioridad +field_fixed_version: Target version +field_user: Usuario +field_role: Perfil +field_homepage: Sitio web +field_is_public: Público +field_parent: Proyecto padre +field_is_in_chlog: Consultar las peticiones en el histórico +field_is_in_roadmap: Consultar las peticiones en el roadmap +field_login: Identificador +field_mail_notification: Notificaciones por correo +field_admin: Administrador +field_last_login_on: Última conexión +field_language: Idioma +field_effective_date: Fecha +field_password: Contraseña +field_new_password: Nueva contraseña +field_password_confirmation: Confirmación +field_version: Versión +field_type: Tipo +field_host: Anfitrión +field_port: Puerto +field_account: Cuenta +field_base_dn: DN base +field_attr_login: Cualidad del identificador +field_attr_firstname: Cualidad del nombre +field_attr_lastname: Cualidad del apellido +field_attr_mail: Cualidad del Email +field_onthefly: Creación del usuario "al vuelo" +field_start_date: Fecha de inicio +field_done_ratio: %% Realizado +field_auth_source: Modo de identificación +field_hide_mail: Ocultar mi dirección de correo +field_comment: Comentario +field_url: URL +field_start_page: Página principal +field_subproject: Proyecto secundario +field_hours: Horas +field_activity: Actividad +field_spent_on: Fecha +field_identifier: Identificador +field_is_filter: Usado como filtro +field_issue_to_id: Petición Relacionada +field_delay: Retraso +field_default_value: Estado por defecto + +setting_app_title: Título de la aplicación +setting_app_subtitle: Subtítulo de la aplicación +setting_welcome_text: Texto de bienvenida +setting_default_language: Idioma por defecto +setting_login_required: Se requiere identificación +setting_self_registration: Registro permitido +setting_attachment_max_size: Tamaño máximo del fichero +setting_issues_export_limit: Límite de exportación de peticiones +setting_mail_from: Correo desde el que enviar mensajes +setting_host_name: Nombre de host +setting_text_formatting: Formato de texto +setting_wiki_compression: Compresión del historial de Wiki +setting_feeds_limit: Límite de contenido para sindicación +setting_autofetch_changesets: Autorellenar los commits del repositorio +setting_sys_api_enabled: Habilitar WS para la gestión del repositorio +setting_commit_ref_keywords: Palabras clave para la referencia +setting_commit_fix_keywords: Palabras clave para la corrección +setting_autologin: Conexión automática +setting_date_format: Formato de la fecha + +label_user: Usuario +label_user_plural: Usuarios +label_user_new: Nuevo usuario +label_project: Proyecto +label_project_new: Nuevo proyecto +label_project_plural: Proyectos +label_project_all: Todos los proyectos +label_project_latest: Últimos proyectos +label_issue: Petición +label_issue_new: Nueva petición +label_issue_plural: Peticiones +label_issue_view_all: Ver todas las peticiones +label_document: Documento +label_document_new: Nuevo documento +label_document_plural: Documentos +label_role: Perfil +label_role_plural: Perfiles +label_role_new: Nuevo perfil +label_role_and_permissions: Perfiles y permisos +label_member: Miembro +label_member_new: Nuevo miembro +label_member_plural: Miembros +label_tracker: Tracker +label_tracker_plural: Trackers +label_tracker_new: Nuevo tracker +label_workflow: Flujo de trabajo +label_issue_status: Estado de petición +label_issue_status_plural: Estados de las peticiones +label_issue_status_new: Nuevo estado +label_issue_category: Categoría de las peticiones +label_issue_category_plural: Categorías de las peticiones +label_issue_category_new: Nueva categoría +label_custom_field: Campo personalizado +label_custom_field_plural: Campos personalizados +label_custom_field_new: Nuevo campo personalizado +label_enumerations: Listas de valores +label_enumeration_new: Nuevo valor +label_information: Información +label_information_plural: Información +label_please_login: Conexión +label_register: Registrar +label_password_lost: ¿Olvidaste la contraseña? +label_home: Inicio +label_my_page: Mi página +label_my_account: Mi cuenta +label_my_projects: Mis proyectos +label_administration: Administración +label_login: Conexión +label_logout: Desconexión +label_help: Ayuda +label_reported_issues: Peticiones registradas por mí +label_assigned_to_me_issues: Peticiones que me están asignadas +label_last_login: Última conexión +label_last_updates: Actualizado +label_last_updates_plural: %d Actualizados +label_registered_on: Inscrito el +label_activity: Actividad +label_new: Nuevo +label_logged_as: Conectado como +label_environment: Entorno +label_authentication: Autenticación +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_subproject_plural: Proyectos secundarios +label_min_max_length: Longitud mín - máx +label_list: Lista +label_date: Fecha +label_integer: Número +label_boolean: Boleano +label_string: Texto +label_text: Texto largo +label_attribute: Cualidad +label_attribute_plural: Cualidades +label_download: %d Descarga +label_download_plural: %d Descargas +label_no_data: Ningun dato a mostrar +label_change_status: Cambiar el estado +label_history: Histórico +label_attachment: Fichero +label_attachment_new: Nuevo fichero +label_attachment_delete: Borrar el fichero +label_attachment_plural: Ficheros +label_report: Informe +label_report_plural: Informes +label_news: Noticia +label_news_new: Nueva noticia +label_news_plural: Noticias +label_news_latest: Últimas noticias +label_news_view_all: Ver todas las noticias +label_change_log: Cambios +label_settings: Configuración +label_overview: Vistazo +label_version: Versión +label_version_new: Nueva versión +label_version_plural: Versiones +label_confirmation: Confirmación +label_export_to: Exportar a +label_read: Leer... +label_public_projects: Proyectos públicos +label_open_issues: abierta +label_open_issues_plural: abiertas +label_closed_issues: cerrada +label_closed_issues_plural: cerradas +label_total: Total +label_permissions: Permisos +label_current_status: Estado actual +label_new_statuses_allowed: Nuevos estados autorizados +label_all: todos +label_none: ninguno +label_next: Próximo +label_previous: Anterior +label_used_by: Utilizado por +label_details: Detalles +label_add_note: Añadir una nota +label_per_page: Por la página +label_calendar: Calendario +label_months_from: meses de +label_gantt: Gantt +label_internal: Interno +label_last_changes: %d cambios del último +label_change_view_all: Ver todos los cambios +label_personalize_page: Personalizar esta página +label_comment: Comentario +label_comment_plural: Comentarios +label_comment_add: Añadir un comentario +label_comment_added: Comentario añadido +label_comment_delete: Borrar comentarios +label_query: Consulta personalizada +label_query_plural: Consultas personalizadas +label_query_new: Nueva consulta +label_filter_add: Añadir el filtro +label_filter_plural: Filtros +label_equals: igual +label_not_equals: no igual +label_in_less_than: en menos que +label_in_more_than: en más que +label_in: en +label_today: hoy +label_less_than_ago: hace menos de +label_more_than_ago: hace más de +label_ago: hace +label_contains: contiene +label_not_contains: no contiene +label_day_plural: días +label_repository: Repositorio +label_browse: Hojear +label_modification: %d modificación +label_modification_plural: %d modificaciones +label_revision: Revisión +label_revision_plural: Revisiones +label_added: añadido +label_modified: modificado +label_deleted: suprimido +label_latest_revision: Última revisión +label_latest_revision_plural: Últimas revisiones +label_view_revisions: Ver las revisiones +label_max_size: Tamaño máximo +label_on: de +label_sort_highest: Primero +label_sort_higher: Subir +label_sort_lower: Bajar +label_sort_lowest: Último +label_roadmap: Roadmap +label_roadmap_due_in: Finaliza en +label_roadmap_no_issues: No hay peticiones para esta versión +label_search: Búsqueda +label_result: %d resultado +label_result_plural: Resultados +label_all_words: Todas las palabras +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_page_index: Ãndice +label_current_version: Versión actual +label_preview: Previsualizar +label_feed_plural: Feeds +label_changes_details: Detalles de todos los cambios +label_issue_tracking: Peticiones +label_spent_time: Tiempo dedicado +label_f_hour: %.2f hora +label_f_hour_plural: %.2f horas +label_time_tracking: Tiempo tracking +label_change_plural: Cambios +label_statistics: Estadísticas +label_commits_per_month: Commits por mes +label_commits_per_author: Commits por autor +label_view_diff: Ver diferencias +label_diff_inline: en línea +label_diff_side_by_side: cara a cara +label_options: Opciones +label_copy_workflow_from: Copiar flujo de trabajo desde +label_permissions_report: Informe de permisos +label_watched_issues: Peticiones monitorizadas +label_related_issues: Peticiones relacionadas +label_applied_status: Aplicar estado +label_loading: Cargando... +label_relation_new: Nueva relación +label_relation_delete: Eliminar relación +label_relates_to: relacionada con +label_duplicates: duplicada de +label_blocks: bloquea a +label_blocked_by: bloqueado por +label_precedes: anterior a +label_follows: posterior a +label_end_to_start: fin a principio +label_end_to_end: fin a fin +label_start_to_start: principio a principio +label_start_to_end: principio a fin +label_stay_logged_in: Recordar conexión +label_disabled: deshabilitado +label_show_completed_versions: Muestra las versiones completas +label_me: yo mismo +label_board: Foro +label_board_new: Nuevo foro +label_board_plural: Foros +label_topic_plural: Temas +label_message_plural: Mensajes +label_message_last: Último mensaje +label_message_new: Nuevo mensaje +label_reply_plural: Respuestas +label_send_information: Enviar información de la cuenta al usuario +label_year: Año +label_month: Mes +label_week: Semana +label_date_from: Desde +label_date_to: Hasta +label_language_based: Badado en el idioma + +button_login: Conexión +button_submit: Aceptar +button_save: Guardar +button_check_all: Seleccionar todo +button_uncheck_all: No seleccionar nada +button_delete: Borrar +button_create: Crear +button_test: Probar +button_edit: Modificar +button_add: Añadir +button_change: Cambiar +button_apply: Aceptar +button_clear: Anular +button_lock: Bloquear +button_unlock: Desbloquear +button_download: Descargar +button_list: Listar +button_view: Ver +button_move: Mover +button_back: Atrás +button_cancel: Cancelar +button_activate: Activar +button_sort: Ordenar +button_log_time: Tiempo dedicado +button_rollback: Volver a esta versión +button_watch: Monitorizar +button_unwatch: No monitorizar +button_reply: Responder +button_archive: Archivar +button_unarchive: Desarchivar + +status_active: activo +status_registered: registrado +status_locked: bloqueado + +text_select_mail_notifications: Seleccionar los eventos a notificar +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 para ninguna restricción +text_project_destroy_confirmation: ¿Estás seguro de querer eliminar el proyecto? +text_workflow_edit: Seleccionar un flujo de trabajo para actualizar +text_are_you_sure: ¿ Estás seguro ? +text_journal_changed: cambiado de %s a %s +text_journal_set_to: fijado a %s +text_journal_deleted: suprimido +text_tip_task_begin_day: tarea que comienza este día +text_tip_task_end_day: tarea que termina este día +text_tip_task_begin_end_day: tarea que comienza y termina este día +text_project_identifier_info: 'Letras minúsculas (a-z), números y signos de puntuación permitidos.
Una vez guardado, el identificador no puede modificarse.' +text_caracters_maximum: %d carácteres como máximo. +text_length_between: Longitud entre %d y %d carácteres. +text_tracker_no_workflow: No hay ningún flujo de trabajo definido para este tracker +text_unallowed_characters: Carácteres no permitidos +text_comma_separated: Múltiples valores permitidos (separados por coma). +text_issues_ref_in_commit_messages: Referencia y petición de corrección en los mensajes + +default_role_manager: Jefe de proyecto +default_role_developper: Desarrollador +default_role_reporter: Informador +default_tracker_bug: Errores +default_tracker_feature: Tareas +default_tracker_support: Soporte +default_issue_status_new: Nueva +default_issue_status_assigned: Asignada +default_issue_status_resolved: Resuelta +default_issue_status_feedback: Comentarios +default_issue_status_closed: Cerrada +default_issue_status_rejected: Rechazada +default_doc_category_user: Documentación de usuario +default_doc_category_tech: Documentación técnica +default_priority_low: Baja +default_priority_normal: Normal +default_priority_high: Alta +default_priority_urgent: Urgente +default_priority_immediate: Inmediata +default_activity_design: Diseño +default_activity_development: Desarrollo + +enumeration_issue_priorities: Prioridad de las peticiones +enumeration_doc_categories: Categorías del documento +enumeration_activities: Actividades (tiempo dedicado) +label_index_by_date: Ãndice por fecha +field_column_names: Columnas +button_rename: Renombrar +text_issue_category_destroy_question: Algunas peticiones (%d) están asignadas a esta categoría. ¿Qué desea hacer? +label_feeds_access_key_created_on: Clave de acceso por RSS creada hace %s +label_default_columns: Columnas por defecto +setting_cross_project_issue_relations: Permitir relacionar peticiones de distintos proyectos +label_roadmap_overdue: %s tarde +label_module_plural: Módulos +label_this_week: esta semana +label_index_by_title: Ãndice por título +label_jump_to_a_project: Ir al proyecto... +field_assignable: Se pueden asignar peticiones a este perfil +label_sort_by: Ordenar por %s +setting_issue_list_default_columns: Columnas por defecto para la lista de peticiones +text_issue_updated: La petición %s ha sido actualizada por %s. +notice_feeds_access_key_reseted: Su clave de acceso para RSS ha sido reiniciada +field_redirect_existing_links: Redireccionar enlaces existentes +text_issue_category_reassign_to: Reasignar las peticiones a la categoría +notice_email_sent: Se ha enviado un correo a %s +text_issue_added: Petición añadida por %s. +field_comments: Comentario +label_file_plural: Archivos +text_wiki_destroy_confirmation: ¿Seguro que quiere borrar el wiki y todo su contenido? +notice_email_error: Ha ocurrido un error mientras enviando el correo (%s) +label_updated_time: Actualizado hace %s +text_issue_category_destroy_assignments: Dejar las peticiones sin categoría +label_send_test_email: Enviar un correo de prueba +button_reset: Reestablecer +label_added_time_by: Añadido por %s hace %s +field_estimated_hours: Tiempo estimado +label_changeset_plural: Cambios +setting_repositories_encodings: Codificaciones del repositorio +notice_no_issue_selected: "Ninguna petición seleccionada. Por favor, compruebe la petición que quiere modificar" +label_bulk_edit_selected_issues: Editar las peticiones seleccionadas +label_no_change_option: (Sin cambios) +notice_failed_to_save_issues: "Imposible salvar %s peticion(es) en %d seleccionado: %s." +label_theme: Tema +label_default: Por defecto +label_search_titles_only: Buscar sólo en títulos +label_nobody: nadie +button_change_password: Cambiar contraseña +text_user_mail_option: "En 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)." +label_user_mail_option_selected: "Para cualquier evento del proyecto seleccionado..." +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" +setting_emails_footer: Pie de mensajes +label_float: Flotante +button_copy: Copiar +mail_body_account_information_external: Puede usar su cuenta "%s" para conectarse. +mail_body_account_information: Información sobre su cuenta +setting_protocol: Protocolo +text_caracters_minimum: %d carácteres como mínimo +field_time_zone: Zona horaria +label_registration_activation_by_email: activación de cuenta por correo +label_user_mail_no_self_notified: "No quiero ser avisado de cambios hechos por mí" +mail_subject_account_activation_request: Petición de activación de cuenta %s +mail_body_account_activation_request: "Un nuevo usuario (%s) ha sido registrado. Esta cuenta está pendiende de aprobación" +label_registration_automatic_activation: activación automática de cuenta +label_registration_manual_activation: activación manual de cuenta +notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte de administrador" +setting_time_format: Formato de hora +setting_bcc_recipients: Ocultar las copias de carbon (bcc) +button_annotate: Anotar +label_issues_by: Peticiones por %s +field_searchable: Incluir en las búsquedas +label_display_per_page: 'Por página: %s' +setting_per_page_options: Objetos por página +label_age: Edad +notice_default_data_loaded: Configuración por defecto cargada correctamente. +text_load_default_configuration: Cargar la configuración por defecto +text_no_configuration_data: "Todavía no se han configurado roles, ni trackers, ni estados y flujo de trabajo asociado a peticiones. Se recomiendo encarecidamente cargar la configuración por defecto. Una vez cargada, podrá modificarla." +error_can_t_load_default_data: "No se ha podido cargar la configuración por defecto: %s" +button_update: Actualizar +label_change_properties: Cambiar propiedades +label_general: General +label_repository_plural: Repositorios +label_associated_revisions: Revisiones asociadas +setting_user_format: Formato de nombre de usuario +text_status_changed_by_changeset: Aplicado en los cambios %s +label_more: Más +text_issues_destroy_confirmation: '¿Seguro que quiere borrar las peticiones seleccionadas?' +label_scm: SCM +text_select_project_modules: 'Seleccione los módulos a activar para este proyecto:' +label_issue_added: Petición añadida +label_issue_updated: Petición actualizada +label_document_added: Documento añadido +label_message_posted: Mensaje añadido +label_file_added: Fichero añadido +label_news_added: Noticia añadida +project_module_boards: Foros +project_module_issue_tracking: Peticiones +project_module_wiki: Wiki +project_module_files: Ficheros +project_module_documents: Documentos +project_module_repository: Repositorio +project_module_news: Noticias +project_module_time_tracking: Control de tiempo +text_file_repository_writable: Se puede escribir en el repositorio +text_default_administrator_account_changed: Cuenta de administrador por defecto modificada +text_rmagick_available: RMagick disponible (opcional) +button_configure: Configurar +label_plugins: Plugins +label_ldap_authentication: Autenticación LDAP +label_downloads_abbr: D/L +label_this_month: este mes +label_last_n_days: últimos %d días +label_all_time: todo el tiempo +label_this_year: este año +label_date_range: Rango de fechas +label_last_week: última semana +label_yesterday: ayer +label_last_month: último mes +label_add_another_file: Añadir otro fichero +label_optional_description: Descripción opcional +text_destroy_time_entries_question: Existen %.02f horas asignadas a la petición que quiere borrar. ¿Qué quiere hacer ? +error_issue_not_found_in_project: 'La petición no se encuentra o no está asociada a este proyecto' +text_assign_time_entries_to_project: Asignar las horas al proyecto +text_destroy_time_entries: Borrar las horas +text_reassign_time_entries: 'Reasignar las horas a esta petición:' +setting_activity_days_default: Días a mostrar en la actividad de proyecto +label_chronological_order: En orden cronológico +field_comments_sorting: Mostrar comentarios +label_reverse_chronological_order: En orden cronológico inverso +label_preferences: Preferencias +setting_display_subprojects_issues: Mostrar peticiones de un subproyecto en el proyecto padre por defecto +label_overall_activity: Actividad global +setting_default_projects_public: Los proyectos nuevos son públicos por defecto +error_scm_annotate: "No existe la entrada o no ha podido ser anotada" +label_planning: Planificación +text_subprojects_destroy_warning: 'Sus subprojectos: %s también se eliminarán' diff --git a/groups/lang/fi.yml b/groups/lang/fi.yml new file mode 100644 index 000000000..68b6c20d7 --- /dev/null +++ b/groups/lang/fi.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Tammikuu,Helmikuu,Maaliskuu,Huhtikuu,Toukokuu,Kesäkuu,Heinäkuu,Elokuu,Syyskuu,Lokakuu,Marraskuu,Joulukuu +actionview_datehelper_select_month_names_abbr: Tammi,Helmi,Maalis,Huhti,Touko,Kesä,Heinä,Elo,Syys,Loka,Marras,Joulu +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 päivä +actionview_datehelper_time_in_words_day_plural: %d päivää +actionview_datehelper_time_in_words_hour_about: noin tunti +actionview_datehelper_time_in_words_hour_about_plural: noin %d tuntia +actionview_datehelper_time_in_words_hour_about_single: noin tunnin +actionview_datehelper_time_in_words_minute: 1 minuutti +actionview_datehelper_time_in_words_minute_half: puoli minuuttia +actionview_datehelper_time_in_words_minute_less_than: vähemmän kuin minuuttia +actionview_datehelper_time_in_words_minute_plural: %d minuuttia +actionview_datehelper_time_in_words_minute_single: 1 minuutti +actionview_datehelper_time_in_words_second_less_than: vähemmän kuin sekuntin +actionview_datehelper_time_in_words_second_less_than_plural: vähemmän kuin %d sekunttia +actionview_instancetag_blank_option: Valitse, ole hyvä + +activerecord_error_inclusion: ei ole listalla +activerecord_error_exclusion: on varattu +activerecord_error_invalid: ei ole kelpaava +activerecord_error_confirmation: ei vastaa vahvistusta +activerecord_error_accepted: tulee hyväksyä +activerecord_error_empty: ei voi olla tyhjä +activerecord_error_blank: ei voi olla tyhjä +activerecord_error_too_long: on liian pitkä +activerecord_error_too_short: on liian lyhyt +activerecord_error_wrong_length: on väärän pituinen +activerecord_error_taken: on jo varattu +activerecord_error_not_a_number: ei ole numero +activerecord_error_not_a_date: ei ole oikea päivä +activerecord_error_greater_than_start_date: tulee olla aloituspäivän jälkeinen +activerecord_error_not_same_project: ei kuulu samaan projektiin +activerecord_error_circular_dependency: Tämä suhde loisi kiertävän suhteen. + +general_fmt_age: %d v. +general_fmt_age_plural: %d vuotta +general_fmt_date: %%d.%%m.%%Y +general_fmt_datetime: %%d.%%m.%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Maanantai,Tiistai,Keskiviikko,Torstai,Perjantai,Lauantai,Sunnuntai +general_first_day_of_week: '1' + +notice_account_updated: Tilin päivitys onnistui. +notice_account_invalid_creditentials: Väärä käyttäjä 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 autentikointi järjestelmää. Mahdotonta muuttaa salasanaa. +notice_account_lost_email_sent: Sinulle on lähetetty sähköposti jossa on ohje miten 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 %s +notice_email_error: Sähköpostilähetyksessä tapahtui virhe (%s) +notice_feeds_access_key_reseted: RSS pääsy avaimesi on nollaantunut. +notice_failed_to_save_issues: "%d Tapahtum(an/ien) tallennus epäonnistui %d valitut: %s." +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: Vakio asetusten palautus onnistui. + +error_can_t_load_default_data: "Vakio asetuksia ei voitu ladata: %s" +error_scm_not_found: "Syötettä ja/tai versiota ei löydy säiliöstä." +error_scm_command_failed: "Säiliöön pääsyssä tapahtui virhe: %s" + +mail_subject_lost_password: Sinun %s salasanasi +mail_body_lost_password: 'Vaihtaaksesi salasanasi, paina seuraavaa linkkiä:' +mail_subject_register: %s tilin aktivointi +mail_body_register: 'Aktivoidaksesi tilisi, paina seuraavaa linkkiä:' +mail_body_account_information_external: Voit nyt käyttää "%s" tiliäsi kirjautuaksesi järjestelmään. +mail_body_account_information: Sinun tilin tiedot +mail_subject_account_activation_request: %s tilin aktivointi pyyntö +mail_body_account_activation_request: 'Uusi käyttäjä (%s) on rekisteröitynyt. Hänen tili odottaa hyväksyntääsi:' + +gui_validation_error: 1 virhe +gui_validation_error_plural: %d virhettä + +field_name: Nimi +field_description: Kuvaus +field_summary: Yhteenveto +field_is_required: Vaaditaan +field_firstname: Etu nimi +field_lastname: Suku nimi +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önmukainen ilmentymä (reg exp) +field_min_length: Minimi pituus +field_max_length: Maksimi pituus +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: Vakio arvo +field_tracker: Tapahtuma +field_subject: Aihe +field_due_date: Määräaika +field_assigned_to: Nimetty +field_priority: Prioriteetti +field_fixed_version: Kohde versio +field_user: Käyttäjä +field_role: Rooli +field_homepage: Kotisivu +field_is_public: Julkinen +field_parent: Alaprojekti +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: Isäntä +field_port: Portti +field_account: Tili +field_base_dn: Base DN +field_attr_login: Kirjautumis määre +field_attr_firstname: Etuminen määre +field_attr_lastname: Sukunimen määre +field_attr_mail: Sähköpostin määre +field_onthefly: Automaattinen käyttäjien luonti +field_start_date: Alku +field_done_ratio: %% Tehty +field_auth_source: Autentikointi muoto +field_hide_mail: Piiloita sähköpostiosoitteeni +field_comments: Kommentti +field_url: URL +field_start_page: Aloitus sivu +field_subproject: Alaprojekti +field_hours: Tuntia +field_activity: Historia +field_spent_on: Päivä +field_identifier: Tunniste +field_is_filter: Käytetään suodattimena +field_issue_to_id: 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: Vakio arvo + +setting_app_title: Ohjelman otsikko +setting_app_subtitle: Ohjelman alaotsikko +setting_welcome_text: Tervetulo teksti +setting_default_language: Vakio kieli +setting_login_required: Pakollinen autentikointi +setting_self_registration: Tee-Se-Itse rekisteröinti +setting_attachment_max_size: Liitteen maksimi koko +setting_issues_export_limit: Tapahtumien vienti rajoite +setting_mail_from: Lähettäjän sähköpostiosoite +setting_bcc_recipients: Blind carbon copy vastaanottajat (bcc) +setting_host_name: Isännän nimi +setting_text_formatting: Tekstin muotoilu +setting_wiki_compression: Wiki historian pakkaus +setting_feeds_limit: Syötteen sisällön raja +setting_autofetch_changesets: Automaatisen haun souritukset +setting_sys_api_enabled: Salli WS säiliön 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: Vakio sarakkeiden näyttö tapahtuma listauksessa +setting_repositories_encodings: Säiliön 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_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 %s +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: Tapahtuma luokka +label_issue_category_plural: Tapahtuma luokat +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: Minun sivu +label_my_account: Minun tili +label_my_projects: Minun 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_last_updates: Viimeinen päivitys +label_last_updates_plural: %d päivitetty viimeksi +label_registered_on: Rekisteröity +label_activity: Historia +label_new: Uusi +label_logged_as: Kirjauduttu nimellä +label_environment: Ympäristö +label_authentication: Autentikointi +label_auth_source: Autentikointi tapa +label_auth_source_new: Uusi autentikointi tapa +label_auth_source_plural: Autentikointi tavat +label_subproject_plural: Alaprojektit +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: %d Lataus +label_download_plural: %d 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 +label_open_issues_plural: avointa +label_closed_issues: suljettu +label_closed_issues_plural: suljettua +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 %d muutokset +label_change_view_all: Näytä kaikki muutokset +label_personalize_page: Personoi tämä sivu +label_comment: Kommentti +label_comment_plural: Kommentit +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: yhtä kuin +label_not_equals: epäsuuri 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: Säiliö +label_repository_plural: Säiliöt +label_browse: Selaus +label_modification: %d muutos +label_modification_plural: %d 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: Maksimi 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 +label_roadmap_overdue: %s 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: %.2f tunti +label_f_hour_plural: %.2f 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: kaksoiskappale +label_blocks: estää +label_blocked_by: estetty +label_precedes: edeltää +label_follows: seuraa +label_end_to_start: loppu alkuun +label_end_to_end: loppu loppuun +label_start_to_start: alku alkuun +label_start_to_end: alku 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 %s +label_send_test_email: Lähetä testi sähköposti +label_feeds_access_key_created_on: RSS pääsy avain luotiin %s sitten +label_module_plural: Moduulit +label_added_time_by: Lisännyt %s %s sitten +label_updated_time: Päivitetty %s sitten +label_jump_to_a_project: Siirry projektiin... +label_file_plural: Tiedostot +label_changeset_plural: Muutosryhmät +label_default_columns: Vakio sarakkeet +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: manuaalinen tilin aktivointi +label_registration_automatic_activation: automaattinen tilin aktivointi +label_display_per_page: 'Per sivu: %s' +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 tarkoitta, 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_journal_changed: %s muutettu arvoksi %s +text_journal_set_to: muutettu %s +text_journal_deleted: poistettu +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.
Tallentamisen jälkeen tunnistetta ei voi muuttaa.' +text_caracters_maximum: %d merkkiä enintään. +text_caracters_minimum: Täytyy olla vähintään %d merkkiä pitkä. +text_length_between: Pituus välillä %d ja %d merkkiä. +text_tracker_no_workflow: Ei työnkulkua 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: Tapahtuma %s on kirjattu. +text_issue_updated: Tapahtuma %s on päivitetty. +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 (%d) 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: "Valitesemattomille projekteille, saat vain muistutuksen asioista joita seuraat tai olet mukana (esim. tapahtumat joissa olet tekijä tai nimettynä)." +text_no_configuration_data: "Rooleja, tikettejä, 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_assigned: Nimetty +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 prioriteetit +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 %s. +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: Säiliö +project_module_news: Uutiset +project_module_time_tracking: Ajan seuranta +text_file_repository_writable: Kirjoitettava tiedosto säiliö +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 autentikointi +label_downloads_abbr: D/L +label_add_another_file: Lisää uusi tiedosto +label_this_month: tässä kuussa +text_destroy_time_entries_question: %.02f tuntia on raportoitu tapahtumasta jonka aiot poistaa. Mitä haluat tehdä ? +label_last_n_days: viimeiset %d 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_on: '' +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ä alaprojektien 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: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lang/fr.yml b/groups/lang/fr.yml new file mode 100644 index 000000000..cbdda4f3d --- /dev/null +++ b/groups/lang/fr.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Janvier,Février,Mars,Avril,Mai,Juin,Juillet,Août,Septembre,Octobre,Novembre,Décembre +actionview_datehelper_select_month_names_abbr: Jan,Fév,Mars,Avril,Mai,Juin,Juil,Août,Sept,Oct,Nov,Déc +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 jour +actionview_datehelper_time_in_words_day_plural: %d jours +actionview_datehelper_time_in_words_hour_about: environ une heure +actionview_datehelper_time_in_words_hour_about_plural: environ %d heures +actionview_datehelper_time_in_words_hour_about_single: environ une heure +actionview_datehelper_time_in_words_minute: 1 minute +actionview_datehelper_time_in_words_minute_half: 30 secondes +actionview_datehelper_time_in_words_minute_less_than: moins d'une minute +actionview_datehelper_time_in_words_minute_plural: %d minutes +actionview_datehelper_time_in_words_minute_single: 1 minute +actionview_datehelper_time_in_words_second_less_than: moins d'une seconde +actionview_datehelper_time_in_words_second_less_than_plural: moins de %d secondes +actionview_instancetag_blank_option: Choisir + +activerecord_error_inclusion: n'est pas inclus dans la liste +activerecord_error_exclusion: est reservé +activerecord_error_invalid: est invalide +activerecord_error_confirmation: ne correspond pas à la confirmation +activerecord_error_accepted: doit être accepté +activerecord_error_empty: doit être renseigné +activerecord_error_blank: doit être renseigné +activerecord_error_too_long: est trop long +activerecord_error_too_short: est trop court +activerecord_error_wrong_length: n'est pas de la bonne longueur +activerecord_error_taken: est déjà utilisé +activerecord_error_not_a_number: n'est pas un nombre +activerecord_error_not_a_date: n'est pas une date valide +activerecord_error_greater_than_start_date: doit être postérieur à la date de début +activerecord_error_not_same_project: n'appartient pas au même projet +activerecord_error_circular_dependency: Cette relation créerait une dépendance circulaire + +general_fmt_age: %d an +general_fmt_age_plural: %d ans +general_fmt_date: %%d/%%m/%%Y +general_fmt_datetime: %%d/%%m/%%Y %%H:%%M +general_fmt_datetime_short: %%d/%%m %%H:%%M +general_fmt_time: %%H:%%M +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche +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é à %s" +notice_email_error: "Erreur lors de l'envoi de l'email (%s)" +notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée." +notice_failed_to_save_issues: "%d demande(s) sur les %d sélectionnées n'ont pas pu être mise(s) à jour: %s." +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. + +error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage: %s" +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: %s" +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" + +mail_subject_lost_password: Votre mot de passe %s +mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant:' +mail_subject_register: Activation de votre compte %s +mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant:' +mail_body_account_information_external: Vous pouvez utiliser votre compte "%s" 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 %s" +mail_body_account_activation_request: "Un nouvel utilisateur (%s) s'est inscrit. Son compte nécessite votre approbation:" + +gui_validation_error: 1 erreur +gui_validation_error_plural: %d 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: Date d'éché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é +label_overall_activity: Activité globale +field_spent_on: Date +field_identifier: Identifiant +field_is_filter: Utilisé comme filtre +field_issue_to_id: 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 + +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_host_name: Nom d'hôte +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_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 + +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_project: Projet +label_project_new: Nouveau projet +label_project_plural: 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 %s +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_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_last_updates: Dernière mise à jour +label_last_updates_plural: %d dernières mises à jour +label_registered_on: Inscrit le +label_activity: Activité +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_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: %d Téléchargement +label_download_plural: %d 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_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: %d derniers changements +label_change_view_all: Voir tous les changements +label_personalize_page: Personnaliser cette page +label_comment: Commentaire +label_comment_plural: 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 %d 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: %d modification +label_modification_plural: %d modifications +label_revision: Révision +label_revision_plural: Révisions +label_associated_revisions: Révisions associées +label_added: ajouté +label_modified: modifié +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_on: sur +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 +label_roadmap_overdue: En retard de %s +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: %.2f heure +label_f_hour_plural: %.2f 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: doublon de +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 %s +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 %s +label_module_plural: Modules +label_added_time_by: Ajouté par %s il y a %s +label_updated_time: Mis à jour il y a %s +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: %s' +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 + +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_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_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 + +status_active: actif +status_registered: enregistré +status_locked: vérouillé + +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: %s 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_journal_changed: changé de %s à %s +text_journal_set_to: mis à %s +text_journal_deleted: supprimé +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: 'Lettres minuscules (a-z), chiffres et tirets autorisés.
Un fois sauvegardé, l''identifiant ne pourra plus être modifié.' +text_caracters_maximum: %d caractères maximum. +text_caracters_minimum: %d caractères minimum. +text_length_between: Longueur comprise entre %d et %d 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 %s a été soumise par %s. +text_issue_updated: La demande %s a été mise à jour par %s. +text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ? +text_issue_category_destroy_question: %d 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 %s. +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_rmagick_available: Bibliothèque RMagick présente (optionnelle) +text_destroy_time_entries_question: %.02f heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ? +text_destroy_time_entries: Supprimer les heures +text_assign_time_entries_to_project: Reporter les heures sur le projet +text_reassign_time_entries: 'Reporter les heures sur cette demande:' + +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_assigned: Assigné +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) diff --git a/groups/lang/he.yml b/groups/lang/he.yml new file mode 100644 index 000000000..a611c8c39 --- /dev/null +++ b/groups/lang/he.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: ינו×ר,פברו×ר,מרץ,×פריל,מ××™,יוני,יולי,×וגוסט,ספטמבר,×וקטובר,נובמבר,דצבמבר +actionview_datehelper_select_month_names_abbr: ינו',פבו',מרץ,×פר',מ××™,יונ',יול',×וג',ספט',×וקט',נוב',דצמ' +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: ×™×•× 1 +actionview_datehelper_time_in_words_day_plural: %d ×™×ž×™× +actionview_datehelper_time_in_words_hour_about: כשעה +actionview_datehelper_time_in_words_hour_about_plural: ×›-%d שעות +actionview_datehelper_time_in_words_hour_about_single: כשעה +actionview_datehelper_time_in_words_minute: דקה 1 +actionview_datehelper_time_in_words_minute_half: חצי דקה +actionview_datehelper_time_in_words_minute_less_than: פחות מדקה +actionview_datehelper_time_in_words_minute_plural: %d דקות +actionview_datehelper_time_in_words_minute_single: דקה 1 +actionview_datehelper_time_in_words_second_less_than: פחות משניה +actionview_datehelper_time_in_words_second_less_than_plural: פחות מ-%d שניות +actionview_instancetag_blank_option: בחר בבקשה + +activerecord_error_inclusion: ×œ× ×›×œ×•×œ ברשימה +activerecord_error_exclusion: שמור +activerecord_error_invalid: ×œ× ×§×‘×™×œ +activerecord_error_confirmation: ×œ× ×ž×ª××™× ×œ×ישור +activerecord_error_accepted: חייב ×œ×”×¡×›×™× +activerecord_error_empty: ×œ× ×™×›×•×œ להיות ריק +activerecord_error_blank: ×œ× ×™×›×•×œ להיות חסר +activerecord_error_too_long: ×רוך מדי +activerecord_error_too_short: קצר מדי +activerecord_error_wrong_length: ב×רוך שגוי +activerecord_error_taken: כבר נלקח +activerecord_error_not_a_number: ×ינו מספר +activerecord_error_not_a_date: ×ינו ת×ריך קביל +activerecord_error_greater_than_start_date: חייב להיות מ×וחר יותר מת×ריך ההתחלה +activerecord_error_not_same_project: ×œ× ×©×™×™×š ל×ותו הפרויקט +activerecord_error_circular_dependency: הקשר ×”×–×” יצור תלות מעגלית + +general_fmt_age: שנה %d +general_fmt_age_plural: %d ×©× ×™× +general_fmt_date: %%d/%%m/%%Y +general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'ל×' +general_text_Yes: 'כן' +general_text_no: 'ל×' +general_text_yes: 'כן' +general_lang_name: 'Hebrew (עברית)' +general_csv_separator: ',' +general_csv_encoding: ISO-8859-8-I +general_pdf_encoding: ISO-8859-8-I +general_day_names: שני,שלישי,רביעי,חמישי,שישי,שבת,ר×שון +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: דו×"ל נשלח לכתובת %s +notice_email_error: ×רעה שגי××” בעט שליחת הדו×"ל (%s) +notice_feeds_access_key_reseted: מפתח ×”-RSS שלך ×ופס. +notice_failed_to_save_issues: "נכשרת בשמירת %d נוש×\×™× ×‘ %d נבחרו: %s." +notice_no_issue_selected: "×œ× × ×‘×—×¨ ××£ נוש×! בחר בבקשה ×ת הנוש××™× ×©×‘×¨×¦×•× ×š לערוך." + +error_scm_not_found: כניסה ו\×ו ×’×™×¨×¡× ××™× × ×§×™×™×ž×™× ×‘×ž×גר. +error_scm_command_failed: "×רעה שגי××” בעת ניסון גישה למ×גר: %s" + +mail_subject_lost_password: סיסמת ×”-%s שלך +mail_body_lost_password: 'לשינו סיסמת ×”-Redmine שלך,לחץ על הקישור הב×:' +mail_subject_register: הפעלת חשבון %s +mail_body_register: 'להפעלת חשבון ×”-Redmine שלך, לחץ על הקישור הב×:' + +gui_validation_error: שגי××” 1 +gui_validation_error_plural: %d שגי×ות + +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: העבר ×§×™×©×•×¨×™× ×§×™×™×ž×™× +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_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_last_updates: עידכון ×חרון +label_last_updates_plural: %d ×¢×™×“×›×•× ×™× ××—×¨×•× ×™× +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: הורדה %d +label_download_plural: %d הורדות +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_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: %d ×©×™× ×•×™× ××—×¨×•× ×™× +label_change_view_all: צפה בכל ×”×©×™× ×•×™× +label_personalize_page: הפוך דף ×–×” לשלך +label_comment: תגובה +label_comment_plural: תגובות +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: שינוי %d +label_modification_plural: %d ×©×™× ×•×™×™× +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_on: 'ב' +label_sort_highest: ×”×–×– לר×שית +label_sort_higher: ×”×–×– למעלה +label_sort_lower: ×”×–×– למטה +label_sort_lowest: ×”×–×– לתחתית +label_roadmap: מפת ×”×“×¨×›×™× +label_roadmap_due_in: נגמר בעוד +label_roadmap_overdue: %s מ×חר +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: %.2f שעה +label_f_hour_plural: %.2f שעות +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: מין לפי %s +label_send_test_email: שלח דו"ל בדיקה +label_feeds_access_key_created_on: מפתח הזנת RSS נוצר לפני%s +label_module_plural: ×ž×•×“×•×œ×™× +label_added_time_by: הוסף על ידי %s לפני %s +label_updated_time: עודכן לפני %s +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_journal_changed: שונה מ %s ל %s +text_journal_set_to: שונה ל %s +text_journal_deleted: נמחק +text_tip_task_begin_day: מטלה המתחילה ×”×™×•× +text_tip_task_end_day: מטלה המסתיימת ×”×™×•× +text_tip_task_begin_end_day: מתלה המתחילה ומסתיימת ×”×™×•× +text_project_identifier_info: '×ותיות לטיניות (a-z), ×ž×¡×¤×¨×™× ×•×ž×§×¤×™×.
ברגע שנשמר, ×œ× × ×™×ª×Ÿ לשנות ×ת המזהה.' +text_caracters_maximum: ×ž×§×¡×™×ž×•× %d תווי×. +text_length_between: ×ורך בין %d ל %d תווי×. +text_tracker_no_workflow: זרימת עבודה ×œ× ×”×•×’×“×¨×” עבור עוקב ×–×” +text_unallowed_characters: ×ª×•×•×™× ×œ× ×ž×•×¨×©×™× +text_comma_separated: הכנסת ×¢×¨×›×™× ×ž×¨×•×‘×™× ×ž×•×ª×¨×ª (×ž×•×¤×¨×“×™× ×‘×¤×¡×™×§×™×). +text_issues_ref_in_commit_messages: קישור ×•×ª×™×§×•× × ×•×©××™× ×‘×”×•×“×¢×•×ª הפקדות +text_issue_added: ×”× ×•×©× %s דווח (by %s). +text_issue_updated: ×”× ×•×©× %s עודכן (by %s). +text_wiki_destroy_confirmation: ×”×× ×תה בטוח שברצונך למחוק ×ת ×”WIKI ×”×–×” ו×ת כל תוכנו? +text_issue_category_destroy_question: כמה נוש××™× (%d) ×ž×•×¦×‘×™× ×œ×§×˜×’×•×¨×™×” הזו. מה ברצונך לעשות? +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_assigned: מוצב +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: ×תה יכול להשתמש בחשבון "%s" כדי להתחבר +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: בקשת הפעלה לחשבון %s +mail_body_account_activation_request: 'משתמש חדש (%s) נרש×. החשבון שלו מחכה ל×ישור שלך:' +label_registration_automatic_activation: הפעלת חשבון ×וטומטית +label_registration_manual_activation: הפעלת חשבון ידנית +notice_account_pending: "החשבון שלך נוצר ועתה מחכה ל×ישור מנהל המערכת." +field_time_zone: ×יזור זמן +text_caracters_minimum: חייב להיות לפחות ב×ורך של %d תווי×. +setting_bcc_recipients: מוסתר (bcc) +button_annotate: הוסף תי×ור מסגרת +label_issues_by: נוש××™× ×©×œ %s +field_searchable: ניתן לחיפוש +label_display_per_page: 'לכל דף: %s' +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: "×פשרויות ברירת המחדל ×œ× ×”×¦×œ×™×—×• להיטען: %s" +button_update: עדכן +label_change_properties: שנה מ××¤×™×™× ×™× +label_general: כללי +label_repository_plural: מ××’×¨×™× +label_associated_revisions: ×©×™× ×•×™×™× ×§×©×•×¨×™× +setting_user_format: פורמט הצגת ×ž×©×ª×ž×©×™× +text_status_changed_by_changeset: הוחל בסדרת ×”×©×™× ×•×™×™× %s. +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: ב-%d ×™×ž×™× ××—×¨×•× ×™× +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: %.02f שעות דווחו על ×”× ×•×©×™× ×©×ת\×” עומד\ת למחוק. מה ברצונך לעשות ? +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): %s will be also deleted.' diff --git a/groups/lang/it.yml b/groups/lang/it.yml new file mode 100644 index 000000000..3d1dea09e --- /dev/null +++ b/groups/lang/it.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Gennaio,Febbraio,Marzo,Aprile,Maggio,Giugno,Luglio,Agosto,Settembre,Ottobre,Novembre,Dicembre +actionview_datehelper_select_month_names_abbr: Gen,Feb,Mar,Apr,Mag,Giu,Lug,Ago,Set,Ott,Nov,Dic +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 giorno +actionview_datehelper_time_in_words_day_plural: %d giorni +actionview_datehelper_time_in_words_hour_about: circa un'ora +actionview_datehelper_time_in_words_hour_about_plural: circa %d ore +actionview_datehelper_time_in_words_hour_about_single: circa un'ora +actionview_datehelper_time_in_words_minute: 1 minuto +actionview_datehelper_time_in_words_minute_half: mezzo minuto +actionview_datehelper_time_in_words_minute_less_than: meno di un minuto +actionview_datehelper_time_in_words_minute_plural: %d minuti +actionview_datehelper_time_in_words_minute_single: 1 minuto +actionview_datehelper_time_in_words_second_less_than: meno di un secondo +actionview_datehelper_time_in_words_second_less_than_plural: meno di %d secondi +actionview_instancetag_blank_option: Scegli + +activerecord_error_inclusion: non è incluso nella lista +activerecord_error_exclusion: e' riservato +activerecord_error_invalid: non e' valido +activerecord_error_confirmation: non coincide con la conferma +activerecord_error_accepted: deve essere accettato +activerecord_error_empty: non puo' essere vuoto +activerecord_error_blank: non puo' essere blank +activerecord_error_too_long: e' troppo lungo/a +activerecord_error_too_short: e' troppo corto/a +activerecord_error_wrong_length: e' della lunghezza sbagliata +activerecord_error_taken: e' gia' stato/a preso/a +activerecord_error_not_a_number: non e' un numero +activerecord_error_not_a_date: non e' una data valida +activerecord_error_greater_than_start_date: deve essere maggiore della data di partenza +activerecord_error_not_same_project: doesn't belong to the same project +activerecord_error_circular_dependency: This relation would create a circular dependency + +general_fmt_age: %d yr +general_fmt_age_plural: %d yrs +general_fmt_date: %%d/%%m/%%Y +general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Lunedì,Martedì,Mercoledì,Giovedì,Venerdì,Sabato,Domenica +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: You are not authorized to access this page. +notice_email_sent: An email was sent to %s +notice_email_error: An error occurred while sending mail (%s) +notice_feeds_access_key_reseted: Your RSS access key was reseted. + +error_scm_not_found: "La risorsa e/o la versione non esistono nel repository." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Password %s +mail_body_lost_password: 'Per cambiare la password, usate il seguente collegamento:' +mail_subject_register: Attivazione utenza %s +mail_body_register: 'Per attivare la vostra utenza, usate il seguente collegamento:' + +gui_validation_error: 1 errore +gui_validation_error_plural: %d 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: Issue +field_status: Stato +field_notes: Note +field_is_closed: Chiude il contesto +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: Target version +field_user: Utente +field_role: Ruolo +field_homepage: Homepage +field_is_public: Pubblico +field_parent: Sottoprogetto di +field_is_in_chlog: Contesti mostrati nel changelog +field_is_in_roadmap: Contesti mostrati 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: %% completo +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: Hours +field_activity: Activity +field_spent_on: Data +field_identifier: Identifier +field_is_filter: Used as a filter +field_issue_to_id: 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_default_value: Stato predefinito + +setting_app_title: Titolo applicazione +setting_app_subtitle: Sottotitolo applicazione +setting_welcome_text: Testo di benvenuto +setting_default_language: Lingua di default +setting_login_required: Autenticazione richiesta +setting_self_registration: Auto-registrazione abilitata +setting_attachment_max_size: Massima dimensione allegati +setting_issues_export_limit: Limite esportazione contesti +setting_mail_from: Indirizzo sorgente e-mail +setting_host_name: Nome host +setting_text_formatting: Formattazione testo +setting_wiki_compression: Compressione di storia di 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: Referencing keywords +setting_commit_fix_keywords: Fixing keywords +setting_autologin: Autologin +setting_date_format: Date format +setting_cross_project_issue_relations: Allow cross-project issue relations + +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_project_all: All Projects +label_project_latest: Ultimi progetti registrati +label_issue: Contesto +label_issue_new: Nuovo contesto +label_issue_plural: Contesti +label_issue_view_all: Mostra tutti i contesti +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 contesti +label_issue_status_plural: Stati contesto +label_issue_status_new: Nuovo stato +label_issue_category: Categorie contesti +label_issue_category_plural: Categorie contesto +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: Contesti segnalati +label_assigned_to_me_issues: I miei contesti +label_last_login: Ultimo collegamento +label_last_updates: Ultimo aggiornamento +label_last_updates_plural: %d ultimo aggiornamento +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: %d Download +label_download_plural: %d 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: Change log +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_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 %d modifiche +label_change_view_all: Tutte le modifiche +label_personalize_page: Personalizza la pagina +label_comment: Commento +label_comment_plural: Commenti +label_comment_add: Aggiungi un commento +label_comment_added: Commento aggiunto +label_comment_delete: Elimina commenti +label_query: Custom query +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: this week +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: Browse +label_modification: %d modifica +label_modification_plural: %d 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_on: 'on' +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 +label_roadmap_overdue: %s late +label_roadmap_no_issues: Nessun contesto 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: 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: Versione corrente +label_preview: Anteprima +label_feed_plural: Feed +label_changes_details: Particolari di tutti i cambiamenti +label_issue_tracking: tracking dei contesti +label_spent_time: Tempo impiegato +label_f_hour: %.2f ora +label_f_hour_plural: %.2f 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: inline +label_diff_side_by_side: side by side +label_options: Opzioni +label_copy_workflow_from: Copia workflow da +label_permissions_report: Report permessi +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_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_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: Language based +label_sort_by: Sort by %s +label_send_test_email: Send a test email +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: Modules +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s ago +label_jump_to_a_project: Jump to a project... + +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: Watch +button_unwatch: Unwatch +button_reply: Reply +button_archive: Archive +button_unarchive: Unarchive +button_reset: Reset +button_rename: Rename + +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_journal_changed: cambiato da %s a %s +text_journal_set_to: impostato a %s +text_journal_deleted: cancellato +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: 'Lower case letters (a-z), numbers and dashes allowed.
Once saved, the identifier can not be changed.' +text_caracters_maximum: massimo %d caratteri. +text_length_between: Lunghezza compresa tra %d e %d caratteri. +text_tracker_no_workflow: Nessun workflow definito per questo 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: "E' stata segnalata l'anomalia %s da %s." +text_issue_updated: "L'anomalia %s e' stata aggiornata da %s." +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassing issues to this category + +default_role_manager: Manager +default_role_developper: Sviluppatore +default_role_reporter: Reporter +default_tracker_bug: Contesto +default_tracker_feature: Funzione +default_tracker_support: Supporto +default_issue_status_new: Nuovo/a +default_issue_status_assigned: Assegnato/a +default_issue_status_resolved: Risolto/a +default_issue_status_feedback: Feedback +default_issue_status_closed: Chiuso/a +default_issue_status_rejected: Rifiutato/a +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: Design +default_activity_development: Development + +enumeration_issue_priorities: Priorità contesti +enumeration_doc_categories: Categorie di documenti +enumeration_activities: Attività (time tracking) +label_file_plural: Files +label_changeset_plural: Changesets +field_column_names: Columns +label_default_columns: Default columns +setting_issue_list_default_columns: Default columns displayed on the issue list +setting_repositories_encodings: Repositories encodings +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +label_bulk_edit_selected_issues: Bulk edit selected issues +label_no_change_option: (No change) +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +label_theme: Theme +label_default: Default +label_search_titles_only: Search titles only +label_nobody: nobody +button_change_password: Change password +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)." +label_user_mail_option_selected: "For any event on the selected projects only..." +label_user_mail_option_all: "For any event on all my projects" +label_user_mail_option_none: "Only for things I watch or I'm involved in" +setting_emails_footer: Emails footer +label_float: Float +button_copy: Copy +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +setting_protocol: Protocol +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +setting_time_format: Time format +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' +label_registration_automatic_activation: automatic account activation +label_registration_manual_activation: manual account activation +notice_account_pending: "Your account was created and is now pending administrator approval." +field_time_zone: Time zone +text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: Searchable +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/ja.yml b/groups/lang/ja.yml new file mode 100644 index 000000000..680d29836 --- /dev/null +++ b/groups/lang/ja.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: 1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月 +actionview_datehelper_select_month_names_abbr: 1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月 +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_select_year_suffix: 月 +actionview_datehelper_time_in_words_day: 1æ—¥ +actionview_datehelper_time_in_words_day_plural: %dæ—¥ +actionview_datehelper_time_in_words_hour_about: ç´„1時間 +actionview_datehelper_time_in_words_hour_about_plural: ç´„%d時間 +actionview_datehelper_time_in_words_hour_about_single: ç´„1時間 +actionview_datehelper_time_in_words_minute: 1分 +actionview_datehelper_time_in_words_minute_half: ç´„30ç§’ +actionview_datehelper_time_in_words_minute_less_than: 1分以内 +actionview_datehelper_time_in_words_minute_plural: %d分 +actionview_datehelper_time_in_words_minute_single: 1分 +actionview_datehelper_time_in_words_second_less_than: 1秒以内 +actionview_datehelper_time_in_words_second_less_than_plural: %d秒以内 +actionview_instancetag_blank_option: é¸ã‚“ã§ãã ã•ã„ + +activerecord_error_inclusion: ãŒãƒªã‚¹ãƒˆã«å«ã¾ã‚Œã¦ã„ã¾ã›ã‚“ +activerecord_error_exclusion: ãŒäºˆç´„ã•れã¦ã„ã¾ã™ +activerecord_error_invalid: ãŒç„¡åйã§ã™ +activerecord_error_confirmation: 確èªã®ãƒ‘スワードã¨åˆã£ã¦ã„ã¾ã›ã‚“ +activerecord_error_accepted: を承諾ã—ã¦ãã ã•ã„ +activerecord_error_empty: ãŒç©ºã§ã™ +activerecord_error_blank: ãŒç©ºç™½ã§ã™ +activerecord_error_too_long: ãŒé•·ã™ãŽã¾ã™ +activerecord_error_too_short: ãŒçŸ­ã‹ã™ãŽã¾ã™ +activerecord_error_wrong_length: ã®é•·ã•ãŒé–“é•ã£ã¦ã„ã¾ã™ +activerecord_error_taken: ã¯ã™ã§ã«ç™»éŒ²ã•れã¦ã„ã¾ã™ +activerecord_error_not_a_number: ãŒæ•°å­—ã§ã¯ã‚りã¾ã›ã‚“ +activerecord_error_not_a_date: ã®æ—¥ä»˜ãŒé–“é•ã£ã¦ã„ã¾ã™ +activerecord_error_greater_than_start_date: を開始日より後ã«ã—ã¦ãã ã•ã„ +activerecord_error_not_same_project: åŒã˜ãƒ—ロジェクトã«å±žã—ã¦ã„ã¾ã›ã‚“ +activerecord_error_circular_dependency: ã“ã®é–¢ä¿‚ã§ã¯ã€å¾ªç’°ä¾å­˜ã«ãªã‚Šã¾ã™ + +general_fmt_age: %dæ­³ +general_fmt_age_plural: %dæ­³ +general_fmt_date: %%Yå¹´%%m月%%dæ—¥ +general_fmt_datetime: %%Yå¹´%%m月%%dæ—¥ %%H:%%M %%p +general_fmt_datetime_short: %%b %%d, %%H:%%M %%p +general_fmt_time: %%H:%%M %%p +general_text_No: 'ã„ã„ãˆ' +general_text_Yes: 'ã¯ã„' +general_text_no: 'ã„ã„ãˆ' +general_text_yes: 'ã¯ã„' +general_lang_name: 'Japanese (日本語)' +general_csv_separator: ',' +general_csv_encoding: SJIS +general_pdf_encoding: UTF-8 +general_day_names: 月曜日,ç«æ›œæ—¥,水曜日,木曜日,金曜日,土曜日,日曜日 +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: %så®›ã«ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚ +notice_email_error: メールé€ä¿¡ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ(%s) +notice_feeds_access_key_reseted: RSSã‚¢ã‚¯ã‚»ã‚¹ã‚­ãƒ¼ã‚’åˆæœŸåŒ–ã—ã¾ã—ãŸã€‚ + +error_scm_not_found: リãƒã‚¸ãƒˆãƒªã«ã€ã‚¨ãƒ³ãƒˆãƒª/リビジョンãŒå­˜åœ¨ã—ã¾ã›ã‚“。 +error_scm_command_failed: "リãƒã‚¸ãƒˆãƒªã¸ã‚¢ã‚¯ã‚»ã‚¹ã—よã†ã¨ã—ã¦ã‚¨ãƒ©ãƒ¼ã«ãªã‚Šã¾ã—ãŸ: %s" + +mail_subject_lost_password: %sパスワード +mail_body_lost_password: 'パスワードを変更ã™ã‚‹ã«ã¯ã€ä»¥ä¸‹ã®ãƒªãƒ³ã‚¯ã‚’ãŸã©ã£ã¦ãã ã•ã„:' +mail_subject_register: %sアカウントã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ–化 +mail_body_register: 'アカウントをアクティブã«ã™ã‚‹ã«ã¯ã€ä»¥ä¸‹ã®ãƒªãƒ³ã‚¯ã‚’ãŸã©ã£ã¦ãã ã•ã„:' + +gui_validation_error: 1ä»¶ã®ã‚¨ãƒ©ãƒ¼ +gui_validation_error_plural: %dä»¶ã®ã‚¨ãƒ©ãƒ¼ + +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: 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_id: 関連ã™ã‚‹ãƒã‚±ãƒƒãƒˆ +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: é€ä¿¡å…ƒãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ +setting_host_name: ホストå +setting_text_formatting: ãƒ†ã‚­ã‚¹ãƒˆã®æ›¸å¼ +setting_wiki_compression: Wiki履歴を圧縮ã™ã‚‹ +setting_feeds_limit: フィード内容ã®ä¸Šé™ +setting_autofetch_changesets: コミットを自動å–å¾—ã™ã‚‹ +setting_sys_api_enabled: リãƒã‚¸ãƒˆãƒªç®¡ç†ç”¨ã®Web Serviceを有効化ã™ã‚‹ +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_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_last_updates: æœ€è¿‘ã®æ›´æ–°1ä»¶ +label_last_updates_plural: æœ€è¿‘ã®æ›´æ–°%dä»¶ +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: %d ダウンロード +label_download_plural: %d ダウンロード +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_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: ヶ月 from +label_gantt: ガントãƒãƒ£ãƒ¼ãƒˆ +label_internal: Internal +label_last_changes: 最新ã®å¤‰æ›´%dä»¶ +label_change_view_all: å…¨ã¦ã®å¤‰æ›´ã‚’見る +label_personalize_page: ã“ã®ãƒšãƒ¼ã‚¸ã‚’パーソナライズã™ã‚‹ +label_comment: コメント +label_comment_plural: コメント +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: %d点ã®å¤‰æ›´ +label_modification_plural: %d点ã®å¤‰æ›´ +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_on: åˆè¨ˆ +label_sort_highest: 一番上㸠+label_sort_higher: 上㸠+label_sort_lower: 下㸠+label_sort_lowest: 一番下㸠+label_roadmap: ロードマップ +label_roadmap_due_in: 期日ã¾ã§ +label_roadmap_overdue: %s late +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 page +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: %.2f 時間 +label_f_hour_plural: %.2f 時間 +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: %sã§ä¸¦ã³æ›¿ãˆ +label_send_test_email: テストメールをé€ä¿¡ +label_feeds_access_key_created_on: RSSアクセスキーã¯%så‰ã«ä½œæˆã•れã¾ã—㟠+label_module_plural: モジュール +label_added_time_by: %sãŒ%så‰ã«è¿½åŠ ã—ã¾ã—㟠+label_updated_time: %så‰ã«æ›´æ–°ã•れã¾ã—㟠+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: ダウンロード +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_journal_changed: %sã‹ã‚‰%sã«å¤‰æ›´ +text_journal_set_to: %sã«ã‚»ãƒƒãƒˆ +text_journal_deleted: 削除 +text_tip_task_begin_day: ã“ã®æ—¥ã«é–‹å§‹ã™ã‚‹ã‚¿ã‚¹ã‚¯ +text_tip_task_end_day: ã“ã®æ—¥ã«çµ‚了ã™ã‚‹ã‚¿ã‚¹ã‚¯ +text_tip_task_begin_end_day: ã“ã®æ—¥ã®ã†ã¡ã«é–‹å§‹ã—ã¦çµ‚了ã™ã‚‹ã‚¿ã‚¹ã‚¯ +text_project_identifier_info: 'è‹±å°æ–‡å­—(a-z)ã¨æ•°å­—ã¨ãƒ€ãƒƒã‚·ãƒ¥(-)ãŒä½¿ãˆã¾ã™ã€‚
一度ä¿å­˜ã™ã‚‹ã¨ã€è­˜åˆ¥å­ã¯å¤‰æ›´ã§ãã¾ã›ã‚“。' +text_caracters_maximum: 最大 %d 文字ã§ã™ã€‚ +text_length_between: é•·ã•㯠%d ã‹ã‚‰ %d 文字ã¾ã§ã§ã™ã€‚ +text_tracker_no_workflow: ã“ã®ãƒˆãƒ©ãƒƒã‚«ãƒ¼ã«ãƒ¯ãƒ¼ã‚¯ãƒ•ローãŒå®šç¾©ã•れã¦ã„ã¾ã›ã‚“ +text_unallowed_characters: 使ãˆãªã„文字ã§ã™ +text_comma_separated: (カンマã§åŒºåˆ‡ã£ãŸ)複数ã®å€¤ãŒä½¿ãˆã¾ã™ +text_issues_ref_in_commit_messages: コミットメッセージ内ã§ãƒã‚±ãƒƒãƒˆã®å‚ç…§/修正 +text_issue_added: ãƒã‚±ãƒƒãƒˆ %s ãŒå ±å‘Šã•れã¾ã—ãŸã€‚ (by %s) +text_issue_updated: ãƒã‚±ãƒƒãƒˆ %s ãŒæ›´æ–°ã•れã¾ã—ãŸã€‚ (by %s) +text_wiki_destroy_confirmation: 本当ã«ã“ã®wikiã¨ãã®å†…容ã®å…¨ã¦ã‚’削除ã—ã¾ã™ã‹ï¼Ÿ +text_issue_category_destroy_question: ã“ã®ã‚«ãƒ†ã‚´ãƒªã«å‰²ã‚Šå½“ã¦æ¸ˆã¿ã®ãƒã‚±ãƒƒãƒˆ(%d)ãŒã‚りã¾ã™ã€‚何をã—よã†ã¨ã—ã¦ã„ã¾ã™ã‹ï¼Ÿ +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_assigned: 担当 +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_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: "%dä»¶ã®ãƒã‚±ãƒƒãƒˆãŒä¿å­˜ã§ãã¾ã›ã‚“ã§ã—ãŸ(%dä»¶é¸æŠžã®ã†ã¡) : %s." +label_theme: テーマ +label_default: 既定 +label_search_titles_only: タイトルã®ã¿ +label_nobody: 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: 「%sã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’使ã£ã¦ã«ãƒ­ã‚°ã‚¤ãƒ³ã§ãã¾ã™ã€‚ +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: %sã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æœ‰åŠ¹åŒ–è¦æ±‚ +mail_body_account_activation_request: æ–°ã—ã„ユーザ(%s)ãŒç™»éŒ²ã—ã¦ã„ã¾ã™ã€‚ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã‚ãªãŸã®æ‰¿èªå¾…ã¡ã§ã™ï¼š +label_registration_automatic_activation: 自動ã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’有効化 +label_registration_manual_activation: 手動ã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’有効化 +notice_account_pending: アカウントã¯ä½œæˆæ¸ˆã¿ã§ã€ç®¡ç†è€…ã®æ‰¿èªå¾…ã¡ã§ã™ã€‚ +field_time_zone: タイムゾーン +text_caracters_minimum: 最低%d文字ã®é•·ã•ãŒå¿…è¦ã§ã™ +setting_bcc_recipients: ブラインドカーボンコピーã§å—ä¿¡(bcc) +button_annotate: 注釈 +label_issues_by: %s別ã®ãƒã‚±ãƒƒãƒˆ +field_searchable: Searchable +label_display_per_page: '1ページã«: %s' +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: "デフォルト設定ãŒãƒ­ãƒ¼ãƒ‰ã§ãã¾ã›ã‚“ã§ã—ãŸ: %s" +button_update: æ›´æ–° +label_change_properties: プロパティã®å¤‰æ›´ +label_general: 全般 +label_repository_plural: リãƒã‚¸ãƒˆãƒª +label_associated_revisions: 関係ã—ã¦ã„るリビジョン +setting_user_format: ユーザåã®è¡¨ç¤ºæ›¸å¼ +text_status_changed_by_changeset: ãƒã‚§ãƒ³ã‚¸ã‚»ãƒƒãƒˆ%sã§é©ç”¨ã•れã¾ã—ãŸã€‚ +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: DL +label_this_month: 今月 +label_last_n_days: 最後ã®%d日間 +label_all_time: 全期間 +label_this_year: 今年 +label_date_range: 日付ã®ç¯„囲 +label_last_week: 先週 +label_yesterday: 昨日 +label_last_month: 先月 +label_add_another_file: 別ã®ãƒ•ァイルを追加 +text_destroy_time_entries_question: ãƒã‚±ãƒƒãƒˆã«è¨˜éŒ²ã•れãŸ%.02f時間を削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚何ãŒã—ãŸã„ã®ã§ã™ã‹ï¼Ÿ +error_issue_not_found_in_project: 'ãƒã‚±ãƒƒãƒˆãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã€ã‚‚ã—ãã¯ã“ã®ãƒ—ロジェクトã«å±žã—ã¦ã„ã¾ã›ã‚“' +text_assign_time_entries_to_project: 記録ã•ã‚ŒãŸæ™‚間をプロジェクトã«å‰²ã‚Šå½“㦠+label_optional_description: ä»»æ„ã®ã‚³ãƒ¡ãƒ³ãƒˆ +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): %s will be also deleted.' diff --git a/groups/lang/ko.yml b/groups/lang/ko.yml new file mode 100644 index 000000000..4281f3881 --- /dev/null +++ b/groups/lang/ko.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: 1ì›”,2ì›”,3ì›”,4ì›”,5ì›”,6ì›”,7ì›”,8ì›”,9ì›”,10ì›”,11ì›”,12ì›” +actionview_datehelper_select_month_names_abbr: 1ì›”,2ì›”,3ì›”,4ì›”,5ì›”,6ì›”,7ì›”,8ì›”,9ì›”,10ì›”,11ì›”,12ì›” +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 하루 +actionview_datehelper_time_in_words_day_plural: %d ì¼ +actionview_datehelper_time_in_words_hour_about: 약 한 시간 +actionview_datehelper_time_in_words_hour_about_plural: 약 %d 시간 +actionview_datehelper_time_in_words_hour_about_single: 약 한 시간 +actionview_datehelper_time_in_words_minute: 1 ë¶„ +actionview_datehelper_time_in_words_minute_half: 30ì´ˆ +actionview_datehelper_time_in_words_minute_less_than: 1ë¶„ ì´ë‚´ +actionview_datehelper_time_in_words_minute_plural: %d ë¶„ +actionview_datehelper_time_in_words_minute_single: 1 ë¶„ +actionview_datehelper_time_in_words_second_less_than: 1ì´ˆ ì´ë‚´ +actionview_datehelper_time_in_words_second_less_than_plural: %d ì´ˆ ì´ì „ +actionview_instancetag_blank_option: ì„ íƒí•˜ì„¸ìš” + +activerecord_error_inclusion: ì€(는) 목ë¡ì— í¬í•¨ë˜ì–´ 있지 않습니다. +activerecord_error_exclusion: ì€(는) 예약ë˜ì–´ 있습니다. +activerecord_error_invalid: ì€(는) 유효하지 않습니다. +activerecord_error_confirmation: 는 제약조건(confirmation)ì— ë§žì§€ 않습니다. +activerecord_error_accepted: must be accepted +activerecord_error_empty: 는 길ì´ê°€ 0ì¼ ìˆ˜ê°€ 없습니다. +activerecord_error_blank: 는 빈 ê°’ì´ì–´ì„œëŠ” 안ë©ë‹ˆë‹¤. +activerecord_error_too_long: 는 너무 ê¹ë‹ˆë‹¤. +activerecord_error_too_short: 는 너무 짧습니다. +activerecord_error_wrong_length: 는 ìž˜ëª»ëœ ê¸¸ì´ìž…니다. +activerecord_error_taken: ê°€ ì´ë¯¸ ê°’ì„ ê°€ì§€ê³  있습니다. +activerecord_error_not_a_number: 는 숫ìžê°€ 아닙니다. +activerecord_error_not_a_date: 는 ìž˜ëª»ëœ ë‚ ì§œ 값입니다. +activerecord_error_greater_than_start_date: 는 시작날짜보다 커야 합니다. +activerecord_error_not_same_project: 는 ê°™ì€ í”„ë¡œì íŠ¸ì— ì†í•´ 있지 않습니다. +activerecord_error_circular_dependency: ì´ ê´€ê³„ëŠ” 순환 ì˜ì¡´ê´€ê³„를 만들 수있습니다. + +general_fmt_age: %d ë…„ +general_fmt_age_plural: %d ë…„ +general_fmt_date: %%Y-%%m-%%d +general_fmt_datetime: %%Y-%%m-%%d %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: '아니오' +general_text_Yes: '예' +general_text_no: '아니오' +general_text_yes: '예' +general_lang_name: 'Korean (한국어)' +general_csv_separator: ',' +general_csv_encoding: CP949 +general_pdf_encoding: CP949 +general_day_names: 월요ì¼,화요ì¼,수요ì¼,목요ì¼,금요ì¼,토요ì¼,ì¼ìš”ì¼ +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: %s 님ì—게 Emailì´ ë°œì†¡ë˜ì—ˆìŠµë‹ˆë‹¤. +notice_email_error: ë©”ì¼ì„ 전송하는 ê³¼ì •ì— ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. (%s) +notice_feeds_access_key_reseted: RSSì— ì ‘ê·¼ê°€ëŠ¥í•œ 열쇠(key)ê°€ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +notice_no_issue_selected: "ì´ìŠˆê°€ ì„ íƒë˜ì§€ 않았습니다. 수정하기 ì›í•˜ëŠ” ì´ìŠˆë¥¼ ì„ íƒí•˜ì„¸ìš”" + +error_scm_not_found: 소스 ì €ìž¥ì†Œì— í•´ë‹¹ ë‚´ìš©ì´ ì¡´ìž¬í•˜ì§€ 않습니다. +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: ë‹¹ì‹ ì˜ ë¹„ë°€ë²ˆí˜¸ (%s) +mail_body_lost_password: '비밀번호를 변경하기 위해서 ë§í¬ë¥¼ ì´ìš©í•˜ì„¸ìš”' +mail_subject_register: ë‹¹ì‹ ì˜ ê³„ì • 활성화 (%s) +mail_body_register: 'ê³„ì •ì„ í™œì„±í™” 하기 위해서 ë§í¬ë¥¼ ì´ìš©í•˜ì„¸ìš” :' + +gui_validation_error: 1 ì—러 +gui_validation_error_plural: %d ì—러 + +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: 변경ì´ë ¥(changelog)ì—서 보여지는 ì´ìŠˆë“¤ +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: On-the-fly user creation +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 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: Self-registration +setting_attachment_max_size: 최대 ì²¨ë¶€íŒŒì¼ í¬ê¸° +setting_issues_export_limit: Issues export limit +setting_mail_from: Emission mail address +setting_host_name: 호스트 ì´ë¦„ +setting_text_formatting: í…스트 í˜•ì‹ +setting_wiki_compression: 위키 기ë¡(history) ì••ì¶• +setting_feeds_limit: Feed content limit +setting_autofetch_changesets: Autofetch commits +setting_sys_api_enabled: Enable WS for repository management +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: 저장소 ì¸ì½”딩 +setting_emails_footer: ë©”ì¼ ê¼¬ë¦¬ + +label_user: ì‚¬ìš©ìž +label_user_plural: 사용ìžê´€ë¦¬ +label_user_new: ì‹ ê·œ 유저 +label_project: 프로ì íЏ +label_project_new: ì‹ ê·œ 프로ì íЏ +label_project_plural: 프로ì íЏ +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: 워í¬í”Œë¡œ(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_last_updates: 최종 변경 ë‚´ì—­ +label_last_updates_plural: 최종변경 %d +label_registered_on: 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: %d 다운로드 +label_download_plural: %d 다운로드 +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_total: 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: Internal +label_last_changes: 지난 변경사항 %d ê±´ +label_change_view_all: 모든 변경 ë‚´ì—­ 보기 +label_personalize_page: 입맛대로 구성하기(Drag & Drop) +label_comment: 댓글 +label_comment_plural: 댓글 +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: %d 변경 +label_modification_plural: %d 변경 +label_revision: 개정íŒ(Revision) +label_revision_plural: 개정íŒ(Revisions) +label_added: added +label_modified: modified +label_deleted: deleted +label_latest_revision: 최근 ê°œì •íŒ +label_latest_revision_plural: 최근 ê°œì •íŒ +label_view_revisions: ê°œì •íŒ ë³´ê¸° +label_max_size: 최대 í¬ê¸° +label_on: 'on' +label_sort_highest: 최ìƒë‹¨ìœ¼ë¡œ +label_sort_higher: 위로 +label_sort_lower: 아래로 +label_sort_lowest: 최하단으로 +label_roadmap: 로드맵 +label_roadmap_due_in: 기한 +label_roadmap_overdue: %s 지연 +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: %.2f 시간 +label_f_hour_plural: %.2f 시간 +label_time_tracking: ì‹œê°„ì¶”ì  +label_change_plural: 변경사항들 +label_statistics: 통계 +label_commits_per_month: 월별 커밋 ë‚´ì—­ +label_commits_per_author: ì•„ì´ë””별 커밋 ë‚´ì—­ +label_view_diff: diff 보기 +label_diff_inline: 한줄로 +label_diff_side_by_side: ë‘줄로 +label_options: Options +label_copy_workflow_from: Copy workflow from +label_permissions_report: 권한 보고서 +label_watched_issues: ê°ì‹œì¤‘ì¸ ì´ìŠˆ +label_related_issues: ì—°ê²°ëœ ì´ìŠˆ +label_applied_status: Applied status +label_loading: ì½ëŠ” 중... +label_relation_new: New relation +label_relation_delete: Delete relation +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: Language based +label_sort_by: 정렬방법(%s) +label_send_test_email: 테스트 ë©”ì¼ ë³´ë‚´ê¸° +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: 모듈 +label_added_time_by: %sì´(ê°€) %s ì „ì— ì¶”ê°€í•¨ +label_updated_time: %s ì „ì— ìˆ˜ì •ë¨ +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: "ë‚´ê°€ ì†í•˜ê±°ë‚˜ ê°ì‹œ ì¤‘ì¸ ì‚¬í•­ì— ëŒ€í•´ì„œë§Œ" + +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_journal_changed: %sì—서 %s(으)로 변경 +text_journal_set_to: %s로 설정 +text_journal_deleted: ì‚­ì œë¨ +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), ë° ìˆ«ìž ëŒ€ì‰¬(-) 가능.
저장ëœí›„ì—는 ì‹ë³„ìž ë³€ê²½ 불가능.' +text_caracters_maximum: 최대 %d ê¸€ìž ê°€ëŠ¥. +text_length_between: %d ì—서 %d ê¸€ìž +text_tracker_no_workflow: ì´ ì¶”ì íƒ€ìž…(tracker)ì— ì›Œí¬í”Œë¡œìš°ê°€ ì •ì˜ë˜ì§€ 않았습니다. +text_unallowed_characters: 허용ë˜ì§€ 않는 문ìžì—´ +text_comma_separated: ë³µìˆ˜ì˜ ê°’ë“¤ì´ í—ˆìš©ë©ë‹ˆë‹¤.(êµ¬ë¶„ìž ,) +text_issues_ref_in_commit_messages: 커밋메시지ì—서 ì´ìŠˆë¥¼ 참조하거나 해결하기 +text_issue_added: ì´ìŠˆ[%s]ê°€ ë³´ê³ ë˜ì—ˆìŠµë‹ˆë‹¤. +text_issue_updated: ì´ìŠˆ[%s]ê°€ 수정ë˜ì—ˆìŠµë‹ˆë‹¤. +text_wiki_destroy_confirmation: ì´ ìœ„í‚¤ì™€ 모든 ë‚´ìš©ì„ ì§€ìš°ì‹œê² ìŠµë‹ˆê¹Œ? +text_issue_category_destroy_question: ì¼ë¶€ ì´ìŠˆë“¤(%dê°œ)ì´ ì´ ì¹´í…Œê³ ë¦¬ì— í• ë‹¹ë˜ì–´ 있습니다. 어떻게 하시겠습니까? +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_assigned: í™•ì¸ +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: 진행활ë™(시간 ì¶”ì ) +button_copy: 복사 +mail_body_account_information_external: 레드마ì¸ì— 로그ì¸í•  때 "%s" ê³„ì •ì„ ì‚¬ìš©í•˜ì‹¤ 수 있습니다. +button_change_password: 비밀번호 변경 +label_nobody: nobody +setting_protocol: 프로토콜 +mail_body_account_information: 계정 ì •ë³´ +label_user_mail_no_self_notified: "ë‚´ê°€ 만든 ë³€ê²½ì‚¬í•­ë“¤ì— ëŒ€í•´ì„œëŠ” 알림메ì¼ì„ 받지 않습니다." +setting_time_format: 시간 í˜•ì‹ +label_registration_activation_by_email: ë©”ì¼ë¡œ ê³„ì •ì„ í™œì„±í™”í•˜ê¸° +mail_subject_account_activation_request: ë ˆë“œë§ˆì¸ ê³„ì • 활성화 요청 (%s) +mail_body_account_activation_request: '새 계정(%s)ì´ ë“±ë¡ë˜ì—ˆìŠµë‹ˆë‹¤. 관리ìžë‹˜ì˜ 승ì¸ì„ 기다리고 있습니다.:' +label_registration_automatic_activation: ìžë™ 계정 활성화 +label_registration_manual_activation: ìˆ˜ë™ ê³„ì • 활성화 +notice_account_pending: "ê³„ì •ì´ ë§Œë“¤ì–´ 졌습니다. 관리ìžì˜ 승ì¸ì´ ìžˆì„ ë•Œê¹Œì§€ 기다려야 합니다." +field_time_zone: 타임존 +text_caracters_minimum: 최소한 %d ê¸€ìž ì´ìƒì´ì–´ì•¼ 합니다. +setting_bcc_recipients: 참조ìžë“¤ì„ bcc로 숨기기 +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: 검색가능 +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +notice_default_data_loaded: 기본 ì„¤ì •ì„ ì„±ê³µì ìœ¼ë¡œ 로드하였습니다. +text_load_default_configuration: 기본 ì„¤ì •ì„ ë¡œë”©í•˜ê¸° +text_no_configuration_data: "ì—­í• , ì´ìŠˆ 타입, ì´ìŠˆ ìƒíƒœë“¤ê³¼ 워í¬í”Œë¡œê°€ ì•„ì§ ì„¤ì •ë˜ì§€ 않았습니다.\n기본 ì„¤ì •ì„ ë¡œë”©í•˜ëŠ” ê²ƒì„ ê¶Œìž¥í•©ë‹ˆë‹¤. ë¡œë“œëœ í›„ì— ìˆ˜ì •í•  수 있습니다." +error_can_t_load_default_data: "기본 ì„¤ì •ì„ ë¡œë“œí•  수 없습니다.: %s" +button_update: ë³€ê²½ì‚¬í•­ê¸°ë¡ +label_change_properties: ì†ì„± 변경 +label_general: ì¼ë°˜ +label_repository_plural: 저장소들 +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +text_issues_destroy_confirmation: 'ì„ íƒí•œ ì´ìŠˆë¥¼ ì •ë§ë¡œ 삭제하시겠습니까?' +label_scm: SCM +text_select_project_modules: 'ì´ í”„ë¡œì íЏì—서 활성화시킬 ëª¨ë“ˆì„ ì„ íƒí•˜ì„¸ìš”:' +label_issue_added: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +project_module_boards: ê²Œì‹œíŒ +project_module_issue_tracking: ì´ìŠˆê´€ë¦¬ +project_module_wiki: 위키 +project_module_files: ê´€ë ¨íŒŒì¼ +project_module_documents: 문서 +project_module_repository: 저장소 +project_module_news: 뉴스 +project_module_time_tracking: Time tracking +text_file_repository_writable: 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_add_another_file: Add another file +label_this_month: this month +text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ? +label_last_n_days: last %d days +label_all_time: all time +error_issue_not_found_in_project: 'The issue was not found or does not belong to this project' +label_this_year: this year +text_assign_time_entries_to_project: Assign reported hours to the project +label_date_range: Date range +label_last_week: last week +label_yesterday: yesterday +label_optional_description: Optional description +label_last_month: last month +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): %s will be also deleted.' diff --git a/groups/lang/lt.yml b/groups/lang/lt.yml new file mode 100644 index 000000000..df7cd960b --- /dev/null +++ b/groups/lang/lt.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: sausis,vasaris,kovas,balandis,gegužė,birželis,liepa,rugpjÅ«tis,rugsÄ—jis,spalis,lapkritis,gruodis +actionview_datehelper_select_month_names_abbr: Sau,Vas,Kov,Bal,Geg,Brž,Lie,Rgp,Rgs,Spl,Lap,Grd +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 diena +actionview_datehelper_time_in_words_day_plural: %d dienų +actionview_datehelper_time_in_words_hour_about: apytiksliai valanda +actionview_datehelper_time_in_words_hour_about_plural: apie %d valandas +actionview_datehelper_time_in_words_hour_about_single: apytiksliai valanda +actionview_datehelper_time_in_words_minute: 1 minutÄ— +actionview_datehelper_time_in_words_minute_half: pusÄ— minutÄ—s +actionview_datehelper_time_in_words_minute_less_than: mažiau kaip minutÄ— +actionview_datehelper_time_in_words_minute_plural: %d minutÄ—s +actionview_datehelper_time_in_words_minute_single: 1 minutÄ— +actionview_datehelper_time_in_words_second_less_than: mažiau kaip sekundÄ— +actionview_datehelper_time_in_words_second_less_than_plural: mažiau, negu %d sekundÄ—s +actionview_instancetag_blank_option: praÅ¡om iÅ¡rinkti + +activerecord_error_inclusion: nÄ—ra įtrauktas į sÄ…rašą +activerecord_error_exclusion: yra rezervuota(as) +activerecord_error_invalid: yra negaliojanti(is) +activerecord_error_confirmation: neatitinka patvirtinimo +activerecord_error_accepted: turi bÅ«ti priimtas +activerecord_error_empty: negali bÅ«ti tuÅ¡Äiu +activerecord_error_blank: negali bÅ«ti tuÅ¡Äiu +activerecord_error_too_long: yra per ilgas +activerecord_error_too_short: yra per trumpas +activerecord_error_wrong_length: neteisingas ilgis +activerecord_error_taken: buvo jau paimtas +activerecord_error_not_a_number: nÄ—ra skaiÄius +activerecord_error_not_a_date: data nÄ—ra galiojanti +activerecord_error_greater_than_start_date: turi bÅ«ti didesnÄ— negu pradžios data +activerecord_error_not_same_project: nepriklauso tam paÄiam projektui +activerecord_error_circular_dependency: Å is ryÅ¡ys sukurtų ciklinÄ™ priklausomybÄ™ + +general_fmt_age: %d m. +general_fmt_age_plural: %d metų(ai) +general_fmt_date: %%Y-%%m-%%d +general_fmt_datetime: %%Y-%%m-%%d %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: UTF-8 +general_pdf_encoding: UTF-8 +general_day_names: pirmadienis,antradienis,treÄiadienis,ketvirtadienis,penktadienis,Å¡eÅ¡tadienis,sekmadienis +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_scm_error: Duomenys ir/ar pakeitimai saugykloje(repozitorojoje) neegzistuoja. +notice_not_authorized: JÅ«s neturite teisių gauti prieigÄ… prie Å¡io puslapio. +notice_email_sent: LaiÅ¡kas iÅ¡siųstas %s +notice_email_error: LaiÅ¡ko siųntimo metu įvyko klaida (%s) +notice_feeds_access_key_reseted: JÅ«sų RSS raktas buvo atnaujintas. +notice_failed_to_save_issues: "Nepavyko iÅ¡saugoti %d problemos(ų) iÅ¡ %d pasirinkto: %s." +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." + +error_scm_not_found: "Duomenys ir/ar pakeitimai saugykloje(repozitorojoje) neegzistuoja." +error_scm_command_failed: "Ä®vyko klaida jungiantis prie saugyklos: %s" + +mail_subject_lost_password: JÅ«sų %s slaptažodis +mail_body_lost_password: 'NorÄ—dami pakeisti slaptažodį, spauskite nuorodÄ…:' +mail_subject_register: '%s paskyros aktyvavymas' +mail_body_register: 'NorÄ—dami aktyvuoti paskyrÄ…, spauskite nuorodÄ…:' +mail_body_account_information_external: JÅ«s galite naudoti JÅ«sų "%s" paskyrÄ…, norÄ—dami prisijungti. +mail_body_account_information: Informacija apie JÅ«sų paskyrÄ… +mail_subject_account_activation_request: %s paskyros aktyvavimo praÅ¡ymas +mail_body_account_activation_request: 'Užsiregistravo naujas vartotojas (%s). Jo paskyra laukia jÅ«sų patvirtinimo:' + +gui_validation_error: 1 klaida +gui_validation_error_plural: %d 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: Target version +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: Jungtis +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: Vartotojų sukÅ«rimas paskubomis +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_id: 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Ä— +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 pagal dydį: Darbų eksportavimo riba +setting_mail_from: Emisijos elektroninio paÅ¡to adresas +setting_bcc_recipients: Akli tikslios kopijos gavÄ—jai (bcc) +setting_host_name: Pagrindinio kompiuterio vardas +setting_text_formatting: Teksto apipavidalinimas +setting_wiki_compression: Wiki istorijos suspaudimas +setting_feeds_limit: Perdavimo turinio riba +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 enkodingas +setting_emails_footer: elektroninio paÅ¡to puslapinÄ— poraÅ¡tÄ— +setting_protocol: Protokolas + +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_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 %s +label_document: Dokumentas +label_document_new: Naujas dokumentas +label_document_plural: Dokumentai +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_last_updates: Paskutinis atnaujinimas +label_last_updates_plural: %d paskutinis atnaujinimas +label_registered_on: Užregistruota +label_activity: Veikla +label_new: Naujas +label_logged_as: PrisijungÄ™s kaip +label_environment: Aplinka +label_authentication: AutentiÅ¡kumo nustatymas +label_auth_source: AutentiÅ¡kumo nustatymo bÅ«das +label_auth_source_new: Naujas autentiÅ¡kumo nustatymo bÅ«das +label_auth_source_plural: AutentiÅ¡kumo nustatymo bÅ«dai +label_subproject_plural: 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: %d Persiuntimas +label_download_plural: %d 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_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_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_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 %d, 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_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_this_week: Å¡iÄ… savaitÄ™ +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_browse: NarÅ¡yti +label_modification: %d pakeitimas +label_modification_plural: %d pakeitimai +label_revision: Revizija +label_revision_plural: Revizijos +label_added: pridÄ—tas +label_modified: pakeistas +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_on: 'iÅ¡' +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 +label_roadmap_overdue: %s 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: %.2f valanda +label_f_hour_plural: %.2f 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: dublikatai +label_blocks: blokai +label_blocked_by: blokuotas +label_precedes: įvyksta pirma +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_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 %s +label_send_test_email: Nusiųsti bandomÄ…jį elektroninį laiÅ¡kÄ… +label_feeds_access_key_created_on: RSS prieigos raktas sukÅ«rtas prieÅ¡ %s +label_module_plural: Moduliai +label_added_time_by: PridÄ—jo %s prieÅ¡ %s +label_updated_time: Atnaujinta prieÅ¡ %s +label_jump_to_a_project: Å uolis į projektÄ…... +label_file_plural: Bylos +label_changeset_plural: Changesets +label_default_columns: Numatytosios skiltys +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" + +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Ä… + +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_workflow_edit: IÅ¡rinkite vaidmenį ir pÄ—dsekį, kad redaguotumÄ—te darbų eigÄ… +text_are_you_sure: Ar esate įsitikinÄ™s? +text_journal_changed: pakeistas iÅ¡ %s į %s +text_journal_set_to: nustatyta į %s +text_journal_deleted: iÅ¡trintas +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.
IÅ¡saugojus, identifikuotojas negali bÅ«ti keiÄiamas.' +text_caracters_maximum: %d simbolių maksimumas. +text_caracters_minimum: Turi bÅ«ti mažiausiai %d simbolių ilgio. +text_length_between: Ilgis tarp %d ir %d 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 %s buvo praneÅ¡tas (by %s). +text_issue_updated: Darbas %s buvo atnaujintas (by %s). +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 (%d) 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)." + +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_assigned: Priskirta +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) +label_display_per_page: '%s įrašų puslapyje' +setting_per_page_options: Objects per page options +notice_default_data_loaded: Numatytoji konfiguracija sÄ—kmingai užkrauta. +label_age: Amžius +label_general: Bendri +button_update: Atnaujinti +setting_issues_export_limit: Darbų eksportavimo limitas +label_change_properties: Pakeisti nustatymus +text_load_default_configuration: Užkrauti numatytÄ…j konfiguracijÄ… +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." +label_repository_plural: Saugiklos +error_can_t_load_default_data: "Numatytoji konfiguracija negali bÅ«ti užkrauta: %s" +label_associated_revisions: susijusios revizijos +setting_user_format: Vartotojo atvaizdavimo formatas +text_status_changed_by_changeset: Pakeista %s revizijoi. +label_more: Daugiau +text_issues_destroy_confirmation: 'Ar jÅ«s tikrai norite panaikinti pažimÄ—tÄ…(us) darbÄ…(us)?' +label_scm: SCM +text_select_project_modules: 'Parinkite modulius, kuriuos norite naudoti Å¡iame projekte:' +label_issue_added: Darbas pridÄ—tas +label_issue_updated: Darbas atnaujintas +label_document_added: Dokumentas pridÄ—tas +label_message_posted: PraneÅ¡imas pridÄ—tas +label_file_added: Byla pridÄ—ta +label_news_added: Naujiena pridÄ—ta +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/nl.yml b/groups/lang/nl.yml new file mode 100644 index 000000000..e487a7a6d --- /dev/null +++ b/groups/lang/nl.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Januari,Februari,Maart,April,Mei,Juni,Juli,Augustus,September,Oktober,November,December +actionview_datehelper_select_month_names_abbr: Jan,Feb,Maa,Apr,Mei,Jun,Jul,Aug,Sep,Okt,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dag +actionview_datehelper_time_in_words_day_plural: %d dagen +actionview_datehelper_time_in_words_hour_about: ongeveer een uur +actionview_datehelper_time_in_words_hour_about_plural: ongeveer %d uur +actionview_datehelper_time_in_words_hour_about_single: ongeveer een uur +actionview_datehelper_time_in_words_minute: 1 minuut +actionview_datehelper_time_in_words_minute_half: een halve minuut +actionview_datehelper_time_in_words_minute_less_than: minder dan een minuut +actionview_datehelper_time_in_words_minute_plural: %d minuten +actionview_datehelper_time_in_words_minute_single: 1 minuut +actionview_datehelper_time_in_words_second_less_than: minder dan een seconde +actionview_datehelper_time_in_words_second_less_than_plural: minder dan %d seconden +actionview_instancetag_blank_option: Selecteer + +activerecord_error_inclusion: staat niet in de lijst +activerecord_error_exclusion: is gereserveerd +activerecord_error_invalid: is ongeldig +activerecord_error_confirmation: komt niet overeen met confirmatie +activerecord_error_accepted: moet geaccepteerd worden +activerecord_error_empty: mag niet leeg zijn +activerecord_error_blank: mag niet blanco zijn +activerecord_error_too_long: is te lang +activerecord_error_too_short: is te kort +activerecord_error_wrong_length: heeft de verkeerde lengte +activerecord_error_taken: is al in gebruik +activerecord_error_not_a_number: is geen getal +activerecord_error_not_a_date: is geen valide datum +activerecord_error_greater_than_start_date: moet hoger zijn dan startdatum +activerecord_error_not_same_project: hoort niet bij hetzelfde project +activerecord_error_circular_dependency: Deze relatie zou een circulaire afhankelijkheid tot gevolg hebben + +general_fmt_age: %d jr +general_fmt_age_plural: %d jr +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'Nee' +general_text_Yes: 'Ja' +general_text_no: 'nee' +general_text_yes: 'ja' +general_lang_name: 'Nederlands' +general_csv_separator: ',' +general_csv_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Maandag, Dinsdag, Woensdag, Donderdag, Vrijdag, Zaterdag, Zondag +general_first_day_of_week: '7' + +notice_account_updated: Account is met succes gewijzigd +notice_account_invalid_creditentials: Incorrecte gebruikersnaam of wachtwoord +notice_account_password_updated: Wachtwoord is met succes gewijzigd +notice_account_wrong_password: Incorrect wachtwoord +notice_account_register_done: Account is met succes aangemaakt. +notice_account_unknown_email: Onbekende gebruiker. +notice_can_t_change_password: Dit account gebruikt een externe bron voor authenticatie. Het is niet mogelijk om het wachtwoord te veranderen. +notice_account_lost_email_sent: Er is een email naar U verstuurd met instructies over het kiezen van een nieuw wachtwoord. +notice_account_activated: Uw account is geactiveerd. U kunt nu inloggen. +notice_successful_create: Maken succesvol. +notice_successful_update: Wijzigen succesvol. +notice_successful_delete: Verwijderen succesvol. +notice_successful_connection: Verbinding succesvol. +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_not_authorized: Het is U niet toegestaan om deze pagina te raadplegen. +notice_email_sent: An email was sent to %s +notice_email_error: An error occurred while sending mail (%s) +notice_feeds_access_key_reseted: Your RSS access key was reseted. + +error_scm_not_found: "Deze ingang of revisie bestaat niet in de repository." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Uw %s wachtwoord +mail_body_lost_password: 'Gebruik de volgende link om Uw wachtwoord te wijzigen:' +mail_subject_register: Uw %s account activatie +mail_body_register: 'Gebruik de volgende link om Uw account te activeren:' + +gui_validation_error: 1 fout +gui_validation_error_plural: %d fouten + +field_name: Naam +field_description: Beschrijving +field_summary: Samenvatting +field_is_required: Verplicht +field_firstname: Voornaam +field_lastname: Achternaam +field_mail: Email +field_filename: Bestand +field_filesize: Grootte +field_downloads: Downloads +field_author: Auteur +field_created_on: Aangemaakt +field_updated_on: Gewijzigd +field_field_format: Formaat +field_is_for_all: Voor alle projecten +field_possible_values: Mogelijke waarden +field_regexp: Reguliere expressie +field_min_length: Minimale lengte +field_max_length: Maximale lengte +field_value: Waarde +field_category: Categorie +field_title: Titel +field_project: Project +field_issue: Issue +field_status: Status +field_notes: Notities +field_is_closed: Issue gesloten +field_is_default: Default +field_tracker: Tracker +field_subject: Onderwerp +field_due_date: Verwachte datum gereed +field_assigned_to: Toegewezen aan +field_priority: Prioriteit +field_fixed_version: Target version +field_user: Gebruiker +field_role: Rol +field_homepage: Homepage +field_is_public: Publiek +field_parent: Subproject van +field_is_in_chlog: Issues weergegeven in wijzigingslog +field_is_in_roadmap: Issues weergegeven in roadmap +field_login: Inloggen +field_mail_notification: Mail mededelingen +field_admin: Administrateur +field_last_login_on: Laatste bezoek +field_language: Taal +field_effective_date: Datum +field_password: Wachtwoord +field_new_password: Nieuw wachtwoord +field_password_confirmation: Bevestigen +field_version: Versie +field_type: Type +field_host: Host +field_port: Port +field_account: Account +field_base_dn: Base DN +field_attr_login: Login attribuut +field_attr_firstname: Voornaam attribuut +field_attr_lastname: Achternaam attribuut +field_attr_mail: Email attribuut +field_onthefly: On-the-fly aanmaken van een gebruiker +field_start_date: Start +field_done_ratio: %% Gereed +field_auth_source: Authenticatiemethode +field_hide_mail: Verberg mijn emailadres +field_comments: Commentaar +field_url: URL +field_start_page: Startpagina +field_subproject: Subproject +field_hours: Uren +field_activity: Activiteit +field_spent_on: Datum +field_identifier: Identificatiecode +field_is_filter: Gebruikt als een filter +field_issue_to_id: Gerelateerd issue +field_delay: Vertraging +field_assignable: Issues can be assigned to this role +field_redirect_existing_links: Redirect existing links +field_estimated_hours: Estimated time +field_default_value: Default value + +setting_app_title: Applicatie titel +setting_app_subtitle: Applicatie ondertitel +setting_welcome_text: Welkomsttekst +setting_default_language: Default taal +setting_login_required: Authent. nodig +setting_self_registration: Zelf-registratie toegestaan +setting_attachment_max_size: Attachment max. grootte +setting_issues_export_limit: Limiet export issues +setting_mail_from: Afzender mail adres +setting_host_name: Host naam +setting_text_formatting: Tekst formaat +setting_wiki_compression: Wiki geschiedenis comprimeren +setting_feeds_limit: Feed inhoud limiet +setting_autofetch_changesets: Haal commits automatisch op +setting_sys_api_enabled: Gebruik WS voor repository beheer +setting_commit_ref_keywords: Referencing keywords +setting_commit_fix_keywords: Fixing keywords +setting_autologin: Autologin +setting_date_format: Date format +setting_cross_project_issue_relations: Allow cross-project issue relations + +label_user: Gebruiker +label_user_plural: Gebruikers +label_user_new: Nieuwe gebruiker +label_project: Project +label_project_new: Nieuw project +label_project_plural: Projecten +label_project_all: Alle Projecten +label_project_latest: Nieuwste projecten +label_issue: Issue +label_issue_new: Nieuw issue +label_issue_plural: Issues +label_issue_view_all: Bekijk alle issues +label_document: Document +label_document_new: Nieuw document +label_document_plural: Documenten +label_role: Rol +label_role_plural: Rollen +label_role_new: Nieuwe rol +label_role_and_permissions: Rollen en permissies +label_member: Lid +label_member_new: Nieuw lid +label_member_plural: Leden +label_tracker: Tracker +label_tracker_plural: Trackers +label_tracker_new: Nieuwe tracker +label_workflow: Workflow +label_issue_status: Issue status +label_issue_status_plural: Issue statussen +label_issue_status_new: Nieuwe status +label_issue_category: Issue categorie +label_issue_category_plural: Issue categorieën +label_issue_category_new: Nieuwe categorie +label_custom_field: Custom veld +label_custom_field_plural: Custom velden +label_custom_field_new: Nieuw custom veld +label_enumerations: Enumeraties +label_enumeration_new: Nieuwe waarde +label_information: Informatie +label_information_plural: Informatie +label_please_login: Gaarne inloggen +label_register: Registreer +label_password_lost: Wachtwoord verloren +label_home: Home +label_my_page: Mijn pagina +label_my_account: Mijn account +label_my_projects: Mijn projecten +label_administration: Administratie +label_login: Inloggen +label_logout: Uitloggen +label_help: Help +label_reported_issues: Gemelde issues +label_assigned_to_me_issues: Aan mij toegewezen issues +label_last_login: Laatste bezoek +label_last_updates: Laatste wijziging +label_last_updates_plural: %d laatste wijziging +label_registered_on: Geregistreerd op +label_activity: Activiteit +label_new: Nieuw +label_logged_as: Ingelogd als +label_environment: Omgeving +label_authentication: Authenticatie +label_auth_source: Authenticatie modus +label_auth_source_new: Nieuwe authenticatie modus +label_auth_source_plural: Authenticatie modi +label_subproject_plural: Subprojecten +label_min_max_length: Min - Max lengte +label_list: Lijst +label_date: Datum +label_integer: Integer +label_boolean: Boolean +label_string: Tekst +label_text: Lange tekst +label_attribute: Attribuut +label_attribute_plural: Attributen +label_download: %d Download +label_download_plural: %d Downloads +label_no_data: Geen gegevens om te tonen +label_change_status: Wijzig status +label_history: Geschiedenis +label_attachment: Bestand +label_attachment_new: Nieuw bestand +label_attachment_delete: Verwijder bestand +label_attachment_plural: Bestanden +label_report: Rapport +label_report_plural: Rapporten +label_news: Nieuws +label_news_new: Voeg nieuws toe +label_news_plural: Nieuws +label_news_latest: Laatste nieuws +label_news_view_all: Bekijk al het nieuws +label_change_log: Wijzigingslog +label_settings: Instellingen +label_overview: Overzicht +label_version: Versie +label_version_new: Nieuwe versie +label_version_plural: Versies +label_confirmation: Bevestiging +label_export_to: Exporteer naar +label_read: Lees... +label_public_projects: Publieke projecten +label_open_issues: open +label_open_issues_plural: open +label_closed_issues: gesloten +label_closed_issues_plural: gesloten +label_total: Totaal +label_permissions: Permissies +label_current_status: Huidige status +label_new_statuses_allowed: Nieuwe statuses toegestaan +label_all: alle +label_none: geen +label_next: Volgende +label_previous: Vorige +label_used_by: Gebruikt door +label_details: Details +label_add_note: Voeg een notitie toe +label_per_page: Per pagina +label_calendar: Kalender +label_months_from: maanden vanaf +label_gantt: Gantt +label_internal: Intern +label_last_changes: laatste %d wijzigingen +label_change_view_all: Bekijk alle wijzigingen +label_personalize_page: Personaliseer deze pagina +label_comment: Commentaar +label_comment_plural: Commentaar +label_comment_add: Voeg commentaar toe +label_comment_added: Commentaar toegevoegd +label_comment_delete: Verwijder commentaar +label_query: Eigen zoekvraag +label_query_plural: Eigen zoekvragen +label_query_new: Nieuwe zoekvraag +label_filter_add: Voeg filter toe +label_filter_plural: Filters +label_equals: is gelijk +label_not_equals: is niet gelijk +label_in_less_than: in minder dan +label_in_more_than: in meer dan +label_in: in +label_today: vandaag +label_this_week: this week +label_less_than_ago: minder dan dagen geleden +label_more_than_ago: meer dan dagen geleden +label_ago: dagen geleden +label_contains: bevat +label_not_contains: bevat niet +label_day_plural: dagen +label_repository: Repository +label_browse: Blader +label_modification: %d wijziging +label_modification_plural: %d wijzigingen +label_revision: Revisie +label_revision_plural: Revisies +label_added: toegevoegd +label_modified: gewijzigd +label_deleted: verwijderd +label_latest_revision: Meest recente revisie +label_latest_revision_plural: Meest recente revisies +label_view_revisions: Bekijk revisies +label_max_size: Maximum grootte +label_on: 'van' +label_sort_highest: Verplaats naar begin +label_sort_higher: Verplaats naar boven +label_sort_lower: Verplaats naar beneden +label_sort_lowest: Verplaats naar eind +label_roadmap: Roadmap +label_roadmap_due_in: Due in +label_roadmap_overdue: %s late +label_roadmap_no_issues: Geen issues voor deze versie +label_search: Zoeken +label_result_plural: Resultaten +label_all_words: Alle woorden +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: Huidige versie +label_preview: Testweergave +label_feed_plural: Feeds +label_changes_details: Details van alle wijzigingen +label_issue_tracking: Issue tracking +label_spent_time: Gespendeerde tijd +label_f_hour: %.2f uur +label_f_hour_plural: %.2f uren +label_time_tracking: Tijd tracking +label_change_plural: Wijzigingen +label_statistics: Statistieken +label_commits_per_month: Commits per maand +label_commits_per_author: Commits per auteur +label_view_diff: Bekijk verschillen +label_diff_inline: inline +label_diff_side_by_side: naast elkaar +label_options: Opties +label_copy_workflow_from: Kopieer workflow van +label_permissions_report: Permissies rapport +label_watched_issues: Gemonitorde issues +label_related_issues: Gerelateerde issues +label_applied_status: Toegekende status +label_loading: Laden... +label_relation_new: Nieuwe relatie +label_relation_delete: Verwijder relatie +label_relates_to: gerelateerd aan +label_duplicates: dupliceert +label_blocks: blokkeert +label_blocked_by: geblokkeerd door +label_precedes: gaat vooraf aan +label_follows: volgt op +label_end_to_start: eind tot start +label_end_to_end: eind tot eind +label_start_to_start: start tot start +label_start_to_end: start tot eind +label_stay_logged_in: Blijf ingelogd +label_disabled: uitgeschakeld +label_show_completed_versions: Toon afgeronde versies +label_me: ik +label_board: Forum +label_board_new: Nieuw forum +label_board_plural: Forums +label_topic_plural: Onderwerpen +label_message_plural: Berichten +label_message_last: Laatste bericht +label_message_new: Nieuw bericht +label_reply_plural: Antwoorden +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: Language based +label_sort_by: Sort by %s +label_send_test_email: Send a test email +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: Modules +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s ago +label_jump_to_a_project: Jump to a project... + +button_login: Inloggen +button_submit: Toevoegen +button_save: Bewaren +button_check_all: Selecteer alle +button_uncheck_all: Deselecteer alle +button_delete: Verwijder +button_create: Maak +button_test: Test +button_edit: Bewerk +button_add: Voeg toe +button_change: Wijzig +button_apply: Pas toe +button_clear: Leeg maken +button_lock: Lock +button_unlock: Unlock +button_download: Download +button_list: Lijst +button_view: Bekijken +button_move: Verplaatsen +button_back: Terug +button_cancel: Annuleer +button_activate: Activeer +button_sort: Sorteer +button_log_time: Log tijd +button_rollback: Rollback naar deze versie +button_watch: Monitor +button_unwatch: Niet meer monitoren +button_reply: Antwoord +button_archive: Archive +button_unarchive: Unarchive +button_reset: Reset +button_rename: Rename + +status_active: Actief +status_registered: geregistreerd +status_locked: gelockt + +text_select_mail_notifications: Selecteer acties waarvoor mededelingen via mail moeten worden verstuurd. +text_regexp_info: bv. ^[A-Z0-9]+$ +text_min_max_length_info: 0 betekent geen restrictie +text_project_destroy_confirmation: Weet U zeker dat U dit project en alle gerelateerde gegevens wilt verwijderen ? +text_workflow_edit: Selecteer een rol en een tracker om de workflow te wijzigen +text_are_you_sure: Weet U het zeker ? +text_journal_changed: gewijzigd van %s naar %s +text_journal_set_to: ingesteld op %s +text_journal_deleted: verwijderd +text_tip_task_begin_day: taak die op deze dag begint +text_tip_task_end_day: taak die op deze dag eindigt +text_tip_task_begin_end_day: taak die op deze dag begint en eindigt +text_project_identifier_info: 'kleine letters (a-z), cijfers en liggende streepjes toegestaan.
Eenmaal bewaard kan de identificatiecode niet meer worden gewijzigd.' +text_caracters_maximum: %d van maximum aantal tekens. +text_length_between: Lengte tussen %d en %d tekens. +text_tracker_no_workflow: Geen workflow gedefinieerd voor deze tracker +text_unallowed_characters: Niet toegestane tekens +text_coma_separated: Meerdere waarden toegestaan (door komma's gescheiden). +text_issues_ref_in_commit_messages: Opzoeken en aanpassen van issues in commit berichten +text_issue_added: Issue %s is gerapporteerd (by %s). +text_issue_updated: Issue %s is gewijzigd (by %s). +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassing issues to this category + +default_role_manager: Manager +default_role_developper: Ontwikkelaar +default_role_reporter: Rapporteur +default_tracker_bug: Bug +default_tracker_feature: Feature +default_tracker_support: Support +default_issue_status_new: Nieuw +default_issue_status_assigned: Toegewezen +default_issue_status_resolved: Opgelost +default_issue_status_feedback: Terugkoppeling +default_issue_status_closed: Gesloten +default_issue_status_rejected: Afgewezen +default_doc_category_user: Gebruikersdocumentatie +default_doc_category_tech: Technische documentatie +default_priority_low: Laag +default_priority_normal: Normaal +default_priority_high: Hoog +default_priority_urgent: Spoed +default_priority_immediate: Onmiddellijk +default_activity_design: Design +default_activity_development: Development + +enumeration_issue_priorities: Issue prioriteiten +enumeration_doc_categories: Document categorieën +enumeration_activities: Activiteiten (tijd tracking) +text_comma_separated: Multiple values allowed (comma separated). +label_file_plural: Files +label_changeset_plural: Changesets +field_column_names: Columns +label_default_columns: Default columns +setting_issue_list_default_columns: Default columns displayed on the issue list +setting_repositories_encodings: Repositories encodings +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +label_bulk_edit_selected_issues: Bulk edit selected issues +label_no_change_option: (No change) +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +label_theme: Theme +label_default: Default +label_search_titles_only: Search titles only +label_nobody: nobody +button_change_password: Change password +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)." +label_user_mail_option_selected: "For any event on the selected projects only..." +label_user_mail_option_all: "For any event on all my projects" +label_user_mail_option_none: "Only for things I watch or I'm involved in" +setting_emails_footer: Emails footer +label_float: Float +button_copy: Copy +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +setting_protocol: Protocol +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +setting_time_format: Time format +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' +label_registration_automatic_activation: automatic account activation +label_registration_manual_activation: manual account activation +notice_account_pending: "Your account was created and is now pending administrator approval." +field_time_zone: Time zone +text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: Searchable +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/no.yml b/groups/lang/no.yml new file mode 100644 index 000000000..22b8c10af --- /dev/null +++ b/groups/lang/no.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Januar,Februar,Mars,April,Mai,Juni,Juli,August,September,Oktober,November,Desember +actionview_datehelper_select_month_names_abbr: Jan,Feb,Mar,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Des +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dag +actionview_datehelper_time_in_words_day_plural: %d dager +actionview_datehelper_time_in_words_hour_about: ca. en time +actionview_datehelper_time_in_words_hour_about_plural: ca. %d timer +actionview_datehelper_time_in_words_hour_about_single: ca. en time +actionview_datehelper_time_in_words_minute: 1 minutt +actionview_datehelper_time_in_words_minute_half: et halvt minutt +actionview_datehelper_time_in_words_minute_less_than: mindre enn et minutt +actionview_datehelper_time_in_words_minute_plural: %d minutter +actionview_datehelper_time_in_words_minute_single: 1 minutt +actionview_datehelper_time_in_words_second_less_than: mindre enn et sekund +actionview_datehelper_time_in_words_second_less_than_plural: mindre enn %d sekunder +actionview_instancetag_blank_option: Vennligst velg + +activerecord_error_inclusion: finnes ikke i listen +activerecord_error_exclusion: er reservert +activerecord_error_invalid: er ugyldig +activerecord_error_confirmation: stemmer ikke med bekreftelsen +activerecord_error_accepted: må aksepteres +activerecord_error_empty: kan ikke være tom +activerecord_error_blank: kan ikke være blank +activerecord_error_too_long: er for langt +activerecord_error_too_short: er for kort +activerecord_error_wrong_length: har feil lengde +activerecord_error_taken: er opptatt +activerecord_error_not_a_number: er ikke et nummer +activerecord_error_not_a_date: er ikke en gyldig dato +activerecord_error_greater_than_start_date: må være større enn startdato +activerecord_error_not_same_project: hører ikke til samme prosjekt +activerecord_error_circular_dependency: Denne relasjonen ville lagd en sirkulær avhengighet + +general_fmt_age: %d år +general_fmt_age_plural: %d år +general_fmt_date: %%d. %%B %%Y +general_fmt_datetime: %%d. %%B %%H:%%M +general_fmt_datetime_short: %%d.%%m.%%Y, %%H:%%M +general_fmt_time: %%H:%%M +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Mandag,Tirsdag,Onsdag,Torsdag,Fredag,Lørdag,Søndag +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 %s +notice_email_error: En feil oppstod under sending av e-post (%s) +notice_feeds_access_key_reseted: Din RSS-tilgangsnøkkel er nullstilt. +notice_failed_to_save_issues: "Lykkes ikke å lagre %d sak(er) på %d valgt: %s." +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: %s" +error_scm_not_found: "Elementet og/eller revisjonen eksisterer ikke i depoet." +error_scm_command_failed: "En feil oppstod under tilkobling til depoet: %s" +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 %s passord +mail_body_lost_password: 'Klikk følgende lenke for å endre ditt passord:' +mail_subject_register: %s kontoaktivering +mail_body_register: 'Klikk følgende lenke for å aktivere din konto:' +mail_body_account_information_external: Du kan bruke din "%s"-konto for å logge inn. +mail_body_account_information: Informasjon om din konto +mail_subject_account_activation_request: %s kontoaktivering +mail_body_account_activation_request: 'En ny bruker (%s) er registrert, og avventer din godkjenning:' + +gui_validation_error: 1 feil +gui_validation_error_plural: %d 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_id: 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 + +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_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 %s +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_last_updates: Sist oppdatert +label_last_updates_plural: %d siste oppdaterte +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_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: %d Nedlasting +label_download_plural: %d 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_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 %d endringer +label_change_view_all: Vis alle endringer +label_personalize_page: Tilpass denne siden +label_comment: Kommentar +label_comment_plural: Kommentarer +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 %d 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: %d endring +label_modification_plural: %d 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_on: 'av' +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 +label_roadmap_overdue: %s 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: %.2f time +label_f_hour_plural: %.2f 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: duplikater +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 %s +label_send_test_email: Send en e-post-test +label_feeds_access_key_created_on: RSS tilgangsnøkkel opprettet for %s siden +label_module_plural: Moduler +label_added_time_by: Lagt til av %s for %s siden +label_updated_time: Oppdatert for %s 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: %s' +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): %s vil ogsÃ¥ bli slettet.' +text_workflow_edit: Velg en rolle og en sakstype for å endre arbeidsflyten +text_are_you_sure: Er du sikker ? +text_journal_changed: endret fra %s til %s +text_journal_set_to: satt til %s +text_journal_deleted: slettet +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.
Identifikatoren kan ikke endres etter den er lagret.' +text_caracters_maximum: %d tegn maksimum. +text_caracters_minimum: MÃ¥ være minst %d tegn langt. +text_length_between: Lengde mellom %d og %d 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: Sak %s er rapportert. +text_issue_updated: Sak %s er oppdatert. +text_wiki_destroy_confirmation: Er du sikker pÃ¥ at du vil slette denne wikien og alt innholdet ? +text_issue_category_destroy_question: Noen saker (%d) 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 %s. +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: %.02f 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:' + +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_assigned: Tildelt +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) diff --git a/groups/lang/pl.yml b/groups/lang/pl.yml new file mode 100644 index 000000000..81f03a62f --- /dev/null +++ b/groups/lang/pl.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: StyczeÅ„,Luty,Marzec,KwiecieÅ„,Maj,Czerwiec,Lipiec,SierpieÅ„,WrzesieÅ„,Październik,Listopad,GrudzieÅ„ +actionview_datehelper_select_month_names_abbr: Sty,Lut,Mar,Kwi,Maj,Cze,Lip,Sie,Wrz,Paź,Lis,Gru +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dzieÅ„ +actionview_datehelper_time_in_words_day_plural: %d dni +actionview_datehelper_time_in_words_hour_about: okoÅ‚o godziny +actionview_datehelper_time_in_words_hour_about_plural: okoÅ‚o %d godzin +actionview_datehelper_time_in_words_hour_about_single: okoÅ‚o godziny +actionview_datehelper_time_in_words_minute: 1 minuta +actionview_datehelper_time_in_words_minute_half: pół minuty +actionview_datehelper_time_in_words_minute_less_than: mniej niż minuta +actionview_datehelper_time_in_words_minute_plural: %d minut +actionview_datehelper_time_in_words_minute_single: 1 minuta +actionview_datehelper_time_in_words_second_less_than: mniej niż sekunda +actionview_datehelper_time_in_words_second_less_than_plural: mniej niż %d sekund +actionview_instancetag_blank_option: ProszÄ™ wybierz + +activerecord_error_inclusion: nie jest zawarte na liÅ›cie +activerecord_error_exclusion: jest zarezerwowane +activerecord_error_invalid: jest nieprawidÅ‚owe +activerecord_error_confirmation: nie pasuje do potwierdzenia +activerecord_error_accepted: musi być zaakceptowane +activerecord_error_empty: nie może być puste +activerecord_error_blank: nie może być czyste +activerecord_error_too_long: jest za dÅ‚ugie +activerecord_error_too_short: jest za krótkie +activerecord_error_wrong_length: ma złą dÅ‚ugość +activerecord_error_taken: jest już wybrane +activerecord_error_not_a_number: nie jest numerem +activerecord_error_not_a_date: nie jest prawidÅ‚owÄ… datÄ… +activerecord_error_greater_than_start_date: musi być wiÄ™ksze niż poczÄ…tkowa data +activerecord_error_not_same_project: nie należy do tego samego projektu +activerecord_error_circular_dependency: Ta relacja może wytworzyć koÅ‚owÄ… zależność + +general_fmt_age: %d lat +general_fmt_age_plural: %d lat +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'Nie' +general_text_Yes: 'Tak' +general_text_no: 'nie' +general_text_yes: 'tak' +general_lang_name: 'Polski' +general_csv_separator: ',' +general_csv_encoding: ISO-8859-2 +general_pdf_encoding: ISO-8859-2 +general_day_names: PoniedziaÅ‚ek,Wtorek,Åšroda,Czwartek,PiÄ…tek,Sobota,Niedziela +general_first_day_of_week: '1' + +notice_account_updated: Konto prawidÅ‚owo zaktualizowane. +notice_account_invalid_creditentials: ZÅ‚y użytkownik lub hasÅ‚o +notice_account_password_updated: HasÅ‚o prawidÅ‚owo zmienione. +notice_account_wrong_password: ZÅ‚e hasÅ‚o +notice_account_register_done: Konto prawidÅ‚owo stworzone. +notice_account_unknown_email: Nieznany użytkownik. +notice_can_t_change_password: To konto ma zewnÄ™trzne źródÅ‚o identyfikacji. Nie możesz zmienić hasÅ‚a. +notice_account_lost_email_sent: Email z instrukcjami zmiany hasÅ‚a zostaÅ‚ wysÅ‚any do Ciebie. +notice_account_activated: Twoje konto zostaÅ‚o aktywowane. Możesz siÄ™ zalogować. +notice_successful_create: Udane stworzenie. +notice_successful_update: Udane poprawienie. +notice_successful_delete: Udane usuniÄ™cie. +notice_successful_connection: Udane nawiÄ…zanie połączenia. +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_not_authorized: Nie jesteÅ› autoryzowany by zobaczyć stronÄ™. + +error_scm_not_found: "WejÅ›cie i/lub zmiana nie istnieje w repozytorium." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Twoje hasÅ‚o do %s +mail_body_lost_password: 'W celu zmiany swojego hasÅ‚a użyj poniższego odnoÅ›nika:' +mail_subject_register: Aktywacja konta w %s +mail_body_register: 'W celu aktywacji Twojego konta, użyj poniższego odnoÅ›nika:' + +gui_validation_error: 1 błąd +gui_validation_error_plural: %d błędów + +field_name: Nazwa +field_description: Opis +field_summary: Podsumowanie +field_is_required: Wymagane +field_firstname: ImiÄ™ +field_lastname: Nazwisko +field_mail: Email +field_filename: Plik +field_filesize: Rozmiar +field_downloads: PobraÅ„ +field_author: Autor +field_created_on: Stworzone +field_updated_on: Zmienione +field_field_format: Format +field_is_for_all: Dla wszystkich projektów +field_possible_values: Możliwe wartoÅ›ci +field_regexp: Wyrażenie regularne +field_min_length: Minimalna dÅ‚ugość +field_max_length: Maksymalna dÅ‚ugość +field_value: Wartość +field_category: Kategoria +field_title: TytuÅ‚ +field_project: Projekt +field_issue: Zagadnienie +field_status: Status +field_notes: Notatki +field_is_closed: Zagadnienie zamkniÄ™te +field_is_default: DomyÅ›lny status +field_tracker: Typ zagadnienia +field_subject: Temat +field_due_date: Data oddania +field_assigned_to: Przydzielony do +field_priority: Priorytet +field_fixed_version: Target version +field_user: Użytkownik +field_role: Rola +field_homepage: Strona www +field_is_public: Publiczny +field_parent: Podprojekt +field_is_in_chlog: Zagadnienie pokazywane w zapisie zmian +field_is_in_roadmap: Zagadnienie pokazywane na mapie +field_login: Login +field_mail_notification: Powiadomienia Email +field_admin: Administrator +field_last_login_on: Ostatnie połączenie +field_language: JÄ™zyk +field_effective_date: Data +field_password: HasÅ‚o +field_new_password: Nowe hasÅ‚o +field_password_confirmation: Potwierdzenie +field_version: Wersja +field_type: Typ +field_host: Host +field_port: Port +field_account: Konto +field_base_dn: Base DN +field_attr_login: Login atrybut +field_attr_firstname: ImiÄ™ atrybut +field_attr_lastname: Nazwisko atrybut +field_attr_mail: Email atrybut +field_onthefly: Tworzenie użytkownika w locie +field_start_date: Start +field_done_ratio: %% Wykonane +field_auth_source: Tryb identyfikacji +field_hide_mail: Ukryj mój adres email +field_comments: Komentarz +field_url: URL +field_start_page: Strona startowa +field_subproject: Podprojekt +field_hours: Godzin +field_activity: Aktywność +field_spent_on: Data +field_identifier: Identifikator +field_is_filter: Atrybut filtrowania +field_issue_to_id: PowiÄ…zania zagadnienia +field_delay: Opóźnienie +field_default_value: DomyÅ›lny + +setting_app_title: TytuÅ‚ aplikacji +setting_app_subtitle: PodtytuÅ‚ aplikacji +setting_welcome_text: Tekst powitalny +setting_default_language: DomyÅ›lny jÄ™zyk +setting_login_required: Identyfikacja wymagana +setting_self_registration: WÅ‚asna rejestracja umożliwiona +setting_attachment_max_size: Maks. rozm. załącznika +setting_issues_export_limit: Limit eksportu zagadnieÅ„ +setting_mail_from: Adres email wysyÅ‚ki +setting_host_name: Nazwa hosta +setting_text_formatting: Formatowanie tekstu +setting_wiki_compression: Kompresja historii Wiki +setting_feeds_limit: Limit danych RSS +setting_autofetch_changesets: Auto-odÅ›wieżanie CVS +setting_sys_api_enabled: Włączenie WS do zarzÄ…dzania repozytorium +setting_commit_ref_keywords: Terminy odnoszÄ…ce (CVS) +setting_commit_fix_keywords: Terminy ustalajÄ…ce (CVS) +setting_autologin: Auto logowanie +setting_date_format: Format daty + +label_user: Użytkownik +label_user_plural: Użytkownicy +label_user_new: Nowy użytkownik +label_project: Projekt +label_project_new: Nowy projekt +label_project_plural: Projekty +label_project_all: Wszystkie projekty +label_project_latest: Ostatnie projekty +label_issue: Zagadnienie +label_issue_new: Nowe zagadnienie +label_issue_plural: Zagadnienia +label_issue_view_all: Zobacz wszystkie zagadnienia +label_document: Dokument +label_document_new: Nowy dokument +label_document_plural: Dokumenty +label_role: Rola +label_role_plural: Role +label_role_new: Nowa rola +label_role_and_permissions: Role i Uprawnienia +label_member: Uczestnik +label_member_new: Nowy uczestnik +label_member_plural: Uczestnicy +label_tracker: Typ zagadnienia +label_tracker_plural: Typy zagadnieÅ„ +label_tracker_new: Nowy typ zagadnienia +label_workflow: PrzepÅ‚yw +label_issue_status: Status zagadnienia +label_issue_status_plural: Statusy zagadnieÅ„ +label_issue_status_new: Nowy status +label_issue_category: Kategoria zagadnienia +label_issue_category_plural: Kategorie zagadnieÅ„ +label_issue_category_new: Nowa kategoria +label_custom_field: Dowolne pole +label_custom_field_plural: Dowolne pola +label_custom_field_new: Nowe dowolne pole +label_enumerations: Wyliczenia +label_enumeration_new: Nowa wartość +label_information: Informacja +label_information_plural: Informacje +label_please_login: Zaloguj siÄ™ +label_register: Rejestracja +label_password_lost: Zapomniane hasÅ‚o +label_home: Główna +label_my_page: Moja strona +label_my_account: Moje konto +label_my_projects: Moje projekty +label_administration: Administracja +label_login: Login +label_logout: Wylogowanie +label_help: Pomoc +label_reported_issues: Wprowadzone zagadnienia +label_assigned_to_me_issues: Zagadnienia przypisane do mnie +label_last_login: Ostatnie połączenie +label_last_updates: Ostatnia zmieniana +label_last_updates_plural: %d ostatnie zmiany +label_registered_on: Zarejestrowany +label_activity: Aktywność +label_new: Nowy +label_logged_as: Zalogowany jako +label_environment: Åšrodowisko +label_authentication: Identyfikacja +label_auth_source: Tryb identyfikacji +label_auth_source_new: Nowy tryb identyfikacji +label_auth_source_plural: Tryby identyfikacji +label_subproject_plural: Podprojekty +label_min_max_length: Min - Maks dÅ‚ugość +label_list: Lista +label_date: Data +label_integer: Liczba caÅ‚kowita +label_boolean: Wartość logiczna +label_string: Tekst +label_text: DÅ‚ugi tekst +label_attribute: Atrybut +label_attribute_plural: Atrybuty +label_download: %d Pobranie +label_download_plural: %d Pobrania +label_no_data: Brak danych do pokazania +label_change_status: Status zmian +label_history: Historia +label_attachment: Plik +label_attachment_new: Nowy plik +label_attachment_delete: Skasuj plik +label_attachment_plural: Pliki +label_report: Raport +label_report_plural: Raporty +label_news: Wiadomość +label_news_new: Dodaj wiadomość +label_news_plural: WiadomoÅ›ci +label_news_latest: Ostatnie wiadomoÅ›ci +label_news_view_all: Pokaż wszystkie wiadomoÅ›ci +label_change_log: Lista zmian +label_settings: Ustawienia +label_overview: PrzeglÄ…d +label_version: Wersja +label_version_new: Nowa wersja +label_version_plural: Wersje +label_confirmation: Potwierdzenie +label_export_to: Eksportuj do +label_read: Czytanie... +label_public_projects: Projekty publiczne +label_open_issues: otwarte +label_open_issues_plural: otwarte +label_closed_issues: zamkniÄ™te +label_closed_issues_plural: zamkniÄ™te +label_total: Ogółem +label_permissions: Uprawnienia +label_current_status: Obecny status +label_new_statuses_allowed: Uprawnione nowe statusy +label_all: wszystko +label_none: brak +label_next: NastÄ™pne +label_previous: Poprzednie +label_used_by: Używane przez +label_details: Szczegóły +label_add_note: Dodaj notatkÄ™ +label_per_page: Na stronÄ™ +label_calendar: Kalendarz +label_months_from: miesiÄ…ce od +label_gantt: Gantt +label_internal: WewnÄ™trzny +label_last_changes: ostatnie %d zmian +label_change_view_all: Pokaż wszystkie zmiany +label_personalize_page: Personalizuj tÄ… stronÄ™ +label_comment: Komentarz +label_comment_plural: Komentarze +label_comment_add: Dodaj komentarz +label_comment_added: Komentarz dodany +label_comment_delete: UsuÅ„ komentarze +label_query: Dowolne zapytanie +label_query_plural: Dowolne zapytania +label_query_new: Nowe zapytanie +label_filter_add: Dodaj filtr +label_filter_plural: Filtry +label_equals: jest +label_not_equals: nie jest +label_in_less_than: w mniejszych od +label_in_more_than: w wiÄ™kszych niż +label_in: w +label_today: dzisiaj +label_less_than_ago: dni mniej +label_more_than_ago: dni wiÄ™cej +label_ago: dni temu +label_contains: zawiera +label_not_contains: nie zawiera +label_day_plural: dni +label_repository: Repozytorium +label_browse: PrzeglÄ…d +label_modification: %d modyfikacja +label_modification_plural: %d modyfikacja +label_revision: Zmiana +label_revision_plural: Zmiany +label_added: dodane +label_modified: zmodufikowane +label_deleted: usuniÄ™te +label_latest_revision: Ostatnia zmiana +label_latest_revision_plural: Ostatnie zmiany +label_view_revisions: Pokaż zmiany +label_max_size: Maksymalny rozmiar +label_on: 'z' +label_sort_highest: PrzesuÅ„ na górÄ™ +label_sort_higher: Do góry +label_sort_lower: Do doÅ‚u +label_sort_lowest: PrzesuÅ„ na dół +label_roadmap: Mapa +label_roadmap_due_in: W czasie +label_roadmap_no_issues: Brak zagadnieÅ„ do tej wersji +label_search: Szukaj +label_result_plural: Rezultatów +label_all_words: Wszystkie sÅ‚owa +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_index_by_title: Indeks +label_index_by_date: Index by date +label_current_version: Obecna wersja +label_preview: PodglÄ…d +label_feed_plural: Ilość RSS +label_changes_details: Szczegóły wszystkich zmian +label_issue_tracking: Åšledzenie zagadnieÅ„ +label_spent_time: SpÄ™dzony czas +label_f_hour: %.2f godzina +label_f_hour_plural: %.2f godzin +label_time_tracking: Åšledzenie czasu +label_change_plural: Zmiany +label_statistics: Statystyki +label_commits_per_month: Wrzutek CVS w miesiÄ…cu +label_commits_per_author: Wrzutek CVS przez autora +label_view_diff: Pokaż różnice +label_diff_inline: w linii +label_diff_side_by_side: obok siebie +label_options: Opcje +label_copy_workflow_from: Kopiuj przepÅ‚yw z +label_permissions_report: Raport uprawnieÅ„ +label_watched_issues: Obserwowane zagadnienia +label_related_issues: PowiÄ…zane zagadnienia +label_applied_status: Stosowany status +label_loading: Åadowanie... +label_relation_new: Nowe powiÄ…zanie +label_relation_delete: UsuÅ„ powiÄ…zanie +label_relates_to: powiÄ…zane z +label_duplicates: duplikaty +label_blocks: blokady +label_blocked_by: zablokowane przez +label_precedes: poprzedza +label_follows: podąża +label_end_to_start: koniec do poczÄ…tku +label_end_to_end: koniec do koÅ„ca +label_start_to_start: poczÄ…tek do poczÄ…tku +label_start_to_end: poczÄ…tek do koÅ„ca +label_stay_logged_in: PozostaÅ„ zalogowany +label_disabled: zablokowany +label_show_completed_versions: Pokaż kompletne wersje +label_me: ja +label_board: Forum +label_board_new: Nowe forum +label_board_plural: Fora +label_topic_plural: Tematy +label_message_plural: WiadomoÅ›ci +label_message_last: Ostatnia wiadomość +label_message_new: Nowa wiadomość +label_reply_plural: Odpowiedzi +label_send_information: WyÅ›lij informacjÄ™ użytkownikowi +label_year: Rok +label_month: MiesiÄ…c +label_week: TydzieÅ„ +label_date_from: Z +label_date_to: Do +label_language_based: Na podstawie jÄ™zyka + +button_login: Login +button_submit: WyÅ›lij +button_save: Zapisz +button_check_all: Zaznacz wszystko +button_uncheck_all: Odznacz wszystko +button_delete: UsuÅ„ +button_create: Stwórz +button_test: Testuj +button_edit: Edytuj +button_add: Dodaj +button_change: ZmieÅ„ +button_apply: Ustaw +button_clear: Wyczyść +button_lock: Zablokuj +button_unlock: Odblokuj +button_download: Pobierz +button_list: Lista +button_view: Pokaż +button_move: PrzenieÅ› +button_back: Wstecz +button_cancel: Anuluj +button_activate: Aktywuj +button_sort: Sortuj +button_log_time: Log czasu +button_rollback: Przywróc do tej wersji +button_watch: Obserwuj +button_unwatch: Nie obserwuj +button_reply: Odpowiedz +button_archive: Archiwizuj +button_unarchive: Przywróc z archiwum + +status_active: aktywny +status_registered: zarejestrowany +status_locked: zablokowany + +text_select_mail_notifications: Zaznacz czynnoÅ›ci przy których użytkownik powinien być powiadomiony mailem. +text_regexp_info: np. ^[A-Z0-9]+$ +text_min_max_length_info: 0 oznacza brak restrykcji +text_project_destroy_confirmation: JesteÅ› pewien, że chcesz usunąć ten projekt i wszyskie powiÄ…zane dane? +text_workflow_edit: Zaznacz rolÄ™ i typ zagadnienia do edycji przepÅ‚ywu +text_are_you_sure: JesteÅ› pewien ? +text_journal_changed: zmienione %s do %s +text_journal_set_to: ustawione na %s +text_journal_deleted: usuniÄ™te +text_tip_task_begin_day: zadanie zaczynajÄ…ce siÄ™ dzisiaj +text_tip_task_end_day: zadanie koÅ„czÄ…ce siÄ™ dzisiaj +text_tip_task_begin_end_day: zadanie zaczynajÄ…ce i koÅ„czÄ…ce siÄ™ dzisiaj +text_project_identifier_info: 'MaÅ‚e litery (a-z), liczby i myÅ›lniki dozwolone.
Raz zapisany, identyfikator nie może być zmieniony.' +text_caracters_maximum: %d znaków maksymalnie. +text_length_between: Długość pomiędzy %d i %d znaków. +text_tracker_no_workflow: Brak przepływu zefiniowanego dla tego typu zagadnienia +text_unallowed_characters: Niedozwolone znaki +text_comma_separated: Wielokrotne wartości dozwolone (rozdzielone przecinkami). +text_issues_ref_in_commit_messages: Zagadnienia odnoszące i ustalające we wrzutkach CVS + +default_role_manager: Kierownik +default_role_developper: Programista +default_role_reporter: Wprowadzajacy +default_tracker_bug: Błąd +default_tracker_feature: Cecha +default_tracker_support: Wsparcie +default_issue_status_new: Nowy +default_issue_status_assigned: Przypisany +default_issue_status_resolved: Rozwiązany +default_issue_status_feedback: Odpowiedź +default_issue_status_closed: Zamknięty +default_issue_status_rejected: Odrzucony +default_doc_category_user: Dokumentacja użytkownika +default_doc_category_tech: Dokumentacja techniczna +default_priority_low: Niski +default_priority_normal: Normalny +default_priority_high: Wysoki +default_priority_urgent: Pilny +default_priority_immediate: Natyczmiastowy +default_activity_design: Projektowanie +default_activity_development: Rozwój + +enumeration_issue_priorities: Priorytety zagadnień +enumeration_doc_categories: Kategorie dokumentów +enumeration_activities: Działania (śledzenie czasu) +button_rename: Zmień nazwę +text_issue_category_destroy_question: Zagadnienia (%d) są przypisane do tej kategorii. Co chcesz uczynić? +label_feeds_access_key_created_on: Klucz dostępu RSS stworzony %s dni temu +setting_cross_project_issue_relations: Zezwól na powiązania zagadnień między projektami +label_roadmap_overdue: %s spóźnienia +label_module_plural: Moduły +label_this_week: ten tydzień +label_jump_to_a_project: Skocz do projektu... +field_assignable: Zagadnienia mogą być przypisane do tej roli +label_sort_by: Sortuj po %s +text_issue_updated: Zagadnienie %s zostało zaktualizowane (by %s). +notice_feeds_access_key_reseted: Twój klucz dostępu RSS został zrestetowany. +field_redirect_existing_links: Przekierowanie istniejących odnośników +text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii +notice_email_sent: Email został wysłany do %s +text_issue_added: Zagadnienie %s zostało wprowadzone (by %s). +text_wiki_destroy_confirmation: Jesteś pewien, że chcesz usunąć to wiki i całą jego zawartość ? +notice_email_error: Wystąpił błąd w trakcie wysyłania maila (%s) +label_updated_time: Zaktualizowane %s temu +text_issue_category_destroy_assignments: Usuń przydziały kategorii +label_send_test_email: Wyślij próbny email +button_reset: Resetuj +label_added_time_by: Dodane przez %s %s temu +field_estimated_hours: Szacowany czas +label_file_plural: Pliki +label_changeset_plural: Zestawienia zmian +field_column_names: Nazwy kolumn +label_default_columns: Domyślne kolumny +setting_issue_list_default_columns: Domyślne kolumny wiświetlane na liście zagadnień +setting_repositories_encodings: Kodowanie repozytoriów +notice_no_issue_selected: "Nie wybrano zagadnienia! Zaznacz zagadnienie, które chcesz edytować." +label_bulk_edit_selected_issues: Zbiorowa edycja zagadnień +label_no_change_option: (Bez zmian) +notice_failed_to_save_issues: "Błąd podczas zapisu zagadnień %d z %d zaznaczonych: %s." +label_theme: Temat +label_default: Domyślne +label_search_titles_only: Przeszukuj tylko tytuły +label_nobody: nikt +button_change_password: Zmień hasło +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)." +label_user_mail_option_selected: "Tylko dla każdego zdarzenia w wybranych projektach..." +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ł" +setting_emails_footer: Stopka e-mail +label_float: Liczba rzeczywista +button_copy: Kopia +mail_body_account_information_external: Możesz użyć twojego "%s" konta do zalogowania. +mail_body_account_information: Twoje konto +setting_protocol: Protokoł +label_user_mail_no_self_notified: "Nie chcę powiadomień o zmianach, które sam wprowadzam." +setting_time_format: Format czasu +label_registration_activation_by_email: aktywacja konta przez e-mail +mail_subject_account_activation_request: Zapytanie aktywacyjne konta %s +mail_body_account_activation_request: 'Zarejestrowano nowego użytkownika: (%s). Konto oczekuje na twoje zatwierdzenie:' +label_registration_automatic_activation: automatyczna aktywacja kont +label_registration_manual_activation: manualna aktywacja kont +notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdzenie administratora." +field_time_zone: Strefa czasowa +text_caracters_minimum: Musi być nie krótsze niż %d znaków. +setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) +button_annotate: Adnotuj +label_issues_by: Zagadnienia wprowadzone przez %s +field_searchable: Przeszukiwalne +label_display_per_page: 'Na stronę: %s' +setting_per_page_options: Opcje ilości obiektów na stronie +label_age: Wiek +notice_default_data_loaded: Domyślna konfiguracja została pomyślnie załadowana. +text_load_default_configuration: Załaduj domyślną konfigurację +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." +error_can_t_load_default_data: "Domyślna konfiguracja nie może być załadowana: %s" +button_update: Uaktualnij +label_change_properties: Zmień właściwości +label_general: Ogólne +label_repository_plural: Repozytoria +label_associated_revisions: Skojarzone rewizje +setting_user_format: Personalny format wyświetlania +text_status_changed_by_changeset: Zastosowane w zmianach %s. +label_more: Więcej +text_issues_destroy_confirmation: 'Czy jestes pewien, że chcesz usunąć wskazane zagadnienia?' +label_scm: SCM +text_select_project_modules: 'Wybierz moduły do aktywacji w tym projekcie:' +label_issue_added: Dodano zagadnienie +label_issue_updated: Uaktualniono zagadnienie +label_document_added: Dodano dokument +label_message_posted: Dodano wiadomość +label_file_added: Dodano plik +label_news_added: Dodano wiadomość +project_module_boards: Fora +project_module_issue_tracking: Śledzenie zagadnień +project_module_wiki: Wiki +project_module_files: Pliki +project_module_documents: Dokumenty +project_module_repository: Repozytorium +project_module_news: Wiadomości +project_module_time_tracking: Śledzenie czasu +text_file_repository_writable: Zapisywalne repozytorium plików +text_default_administrator_account_changed: Zmieniono domyślne hasło administratora +text_rmagick_available: RMagick dostępne (opcjonalnie) +button_configure: Konfiguruj +label_plugins: Wtyczki +label_ldap_authentication: Autoryzacja LDAP +label_downloads_abbr: Pobieranie +label_this_month: ten miesiąc +label_last_n_days: ostatnie %d dni +label_all_time: cały czas +label_this_year: ten rok +label_date_range: Zakres datowy +label_last_week: ostatni tydzień +label_yesterday: wczoraj +label_last_month: ostatni miesiąc +label_add_another_file: Dodaj kolejny plik +label_optional_description: Opcjonalny opis +text_destroy_time_entries_question: Zalogowano %.02f godzin przy zagadnieniu, które chcesz usunąć. Co chcesz zrobić? +error_issue_not_found_in_project: 'Zaganienie nie zostało znalezione lub nie należy do tego projektu' +text_assign_time_entries_to_project: Przypisz logowany czas do projektu +text_destroy_time_entries: Usuń zalogowany czas +text_reassign_time_entries: 'Przepnij zalogowany czas do tego zagadnienia:' +label_chronological_order: W kolejności chronologicznej +setting_activity_days_default: Dni wyświetlane w aktywności projektu +setting_display_subprojects_issues: Domyślnie pokazuj zagadnienia podprojektów w głównym projekcie +field_comments_sorting: Pokazuj komentarze +label_reverse_chronological_order: W kolejności odwrotnej do chronologicznej +label_preferences: Preferencje +label_overall_activity: Ogólna aktywność +setting_default_projects_public: Nowe projekty są domyślnie publiczne +error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji." +label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lang/pt-br.yml b/groups/lang/pt-br.yml new file mode 100644 index 000000000..9facd8d19 --- /dev/null +++ b/groups/lang/pt-br.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Janeiro,Fevereiro,Marco,Abrill,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro +actionview_datehelper_select_month_names_abbr: Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dia +actionview_datehelper_time_in_words_day_plural: %d dias +actionview_datehelper_time_in_words_hour_about: sobre uma hora +actionview_datehelper_time_in_words_hour_about_plural: sobra %d horas +actionview_datehelper_time_in_words_hour_about_single: sobre uma hora +actionview_datehelper_time_in_words_minute: 1 minuto +actionview_datehelper_time_in_words_minute_half: meio minuto +actionview_datehelper_time_in_words_minute_less_than: menos que um minuto +actionview_datehelper_time_in_words_minute_plural: %d minutos +actionview_datehelper_time_in_words_minute_single: 1 minuto +actionview_datehelper_time_in_words_second_less_than: menos que um segundo +actionview_datehelper_time_in_words_second_less_than_plural: menos que %d segundos +actionview_instancetag_blank_option: Selecione + +activerecord_error_inclusion: nao esta incluido na lista +activerecord_error_exclusion: esta reservado +activerecord_error_invalid: e invalido +activerecord_error_confirmation: confirmacao nao confere +activerecord_error_accepted: deve ser aceito +activerecord_error_empty: nao pode ser vazio +activerecord_error_blank: nao pode estar em branco +activerecord_error_too_long: e muito longo +activerecord_error_too_short: e muito comprido +activerecord_error_wrong_length: esta com o comprimento errado +activerecord_error_taken: ja esta examinado +activerecord_error_not_a_number: nao e um numero +activerecord_error_not_a_date: nao e uma data valida +activerecord_error_greater_than_start_date: deve ser maior que a data inicial +activerecord_error_not_same_project: doesn't belong to the same project +activerecord_error_circular_dependency: This relation would create a circular dependency + +general_fmt_age: %d yr +general_fmt_age_plural: %d yrs +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'Nao' +general_text_Yes: 'Sim' +general_text_no: 'nao' +general_text_yes: 'sim' +general_lang_name: 'Portugues Brasileiro' +general_csv_separator: ',' +general_csv_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Segunda,Terca,Quarta,Quinta,Sexta,Sabado,Domingo +general_first_day_of_week: '1' + +notice_account_updated: Conta foi alterada com sucesso. +notice_account_invalid_creditentials: Usuario ou senha invalido. +notice_account_password_updated: Senha foi alterada com sucesso. +notice_account_wrong_password: Senha errada. +notice_account_register_done: Conta foi criada com sucesso. +notice_account_unknown_email: Usuario desconhecido. +notice_can_t_change_password: Esta conta usa autenticacao externa. E impossivel trocar a senha. +notice_account_lost_email_sent: Um email com instrucoes para escolher uma nova senha foi enviado para voce. +notice_account_activated: Sua conta foi ativada. Voce pode logar agora +notice_successful_create: Criado com sucesso. +notice_successful_update: Alterado com sucesso. +notice_successful_delete: Apagado com sucesso. +notice_successful_connection: Conectado com sucesso. +notice_file_not_found: A pagina que voce esta tentando acessar nao existe ou foi excluida. +notice_locking_conflict: Os dados foram atualizados por um outro usuario. +notice_not_authorized: You are not authorized to access this page. +notice_email_sent: An email was sent to %s +notice_email_error: An error occurred while sending mail (%s) +notice_feeds_access_key_reseted: Your RSS access key was reseted. + +error_scm_not_found: "A entrada e/ou a revisao nao existem no repositorio." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Sua senha do %s. +mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:' +mail_subject_register: Ativacao de conta do %s. +mail_body_register: 'Para ativar sua conta, clique no link abaixo:' + +gui_validation_error: 1 erro +gui_validation_error_plural: %d erros + +field_name: Nome +field_description: Descricao +field_summary: Sumario +field_is_required: Obrigatorio +field_firstname: Primeiro nome +field_lastname: Ultimo nome +field_mail: Email +field_filename: Arquivo +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 projetos +field_possible_values: Possiveis valores +field_regexp: Expressao regular +field_min_length: Tamanho minimo +field_max_length: Tamanho maximo +field_value: Valor +field_category: Categoria +field_title: Titulo +field_project: Projeto +field_issue: Tarefa +field_status: Status +field_notes: Notas +field_is_closed: Tarefa fechada +field_is_default: Status padrao +field_tracker: Tipo +field_subject: Titulo +field_due_date: Data devida +field_assigned_to: Atribuido para +field_priority: Prioridade +field_fixed_version: Target version +field_user: Usuario +field_role: Regra +field_homepage: Pagina inicial +field_is_public: Publico +field_parent: Sub-projeto de +field_is_in_chlog: Tarefas mostradas no changelog +field_is_in_roadmap: Tarefas mostradas no roadmap +field_login: Login +field_mail_notification: Notificacoes por email +field_admin: Administrador +field_last_login_on: Ultima conexao +field_language: Lingua +field_effective_date: Data +field_password: Senha +field_new_password: Nova senha +field_password_confirmation: Confirmacao +field_version: Versao +field_type: Tipo +field_host: Servidor +field_port: Porta +field_account: Conta +field_base_dn: Base DN +field_attr_login: Atributo login +field_attr_firstname: Atributo primeiro nome +field_attr_lastname: Atributo ultimo nome +field_attr_mail: Atributo email +field_onthefly: Criacao de usuario on-the-fly +field_start_date: Inicio +field_done_ratio: %% Terminado +field_auth_source: Modo de autenticacao +field_hide_mail: Esconder meu email +field_comments: Comentario +field_url: URL +field_start_page: Pagina inicial +field_subproject: Sub-projeto +field_hours: Horas +field_activity: Atividade +field_spent_on: Data +field_identifier: Identificador +field_is_filter: Used as a filter +field_issue_to_id: 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_default_value: Padrao + +setting_app_title: Titulo da aplicacao +setting_app_subtitle: Sub-titulo da aplicacao +setting_welcome_text: Texto de boa-vinda +setting_default_language: Lingua padrao +setting_login_required: Autenticacao obrigatoria +setting_self_registration: Registro de si mesmo permitido +setting_attachment_max_size: Tamanho maximo do anexo +setting_issues_export_limit: Limite de exportacao das tarefas +setting_mail_from: Email enviado de +setting_host_name: Servidor +setting_text_formatting: Formato do texto +setting_wiki_compression: Compactacao do historio do Wiki +setting_feeds_limit: Limite do Feed +setting_autofetch_changesets: Autofetch commits +setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio +setting_commit_ref_keywords: Referencing keywords +setting_commit_fix_keywords: Fixing keywords +setting_autologin: Autologin +setting_date_format: Date format +setting_cross_project_issue_relations: Allow cross-project issue relations + +label_user: Usuario +label_user_plural: Usuarios +label_user_new: Novo usuario +label_project: Projeto +label_project_new: Novo projeto +label_project_plural: Projetos +label_project_all: All Projects +label_project_latest: Ultimos projetos +label_issue: Tarefa +label_issue_new: Nova tarefa +label_issue_plural: Tarefas +label_issue_view_all: Ver todas as tarefas +label_document: Documento +label_document_new: Novo documento +label_document_plural: Documentos +label_role: Regra +label_role_plural: Regras +label_role_new: Nova regra +label_role_and_permissions: Regras e permissoes +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: Status da tarefa +label_issue_status_plural: Status das tarefas +label_issue_status_new: Novo status +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 personalizado +label_custom_field_new: Novo campo personalizado +label_enumerations: Enumeracao +label_enumeration_new: Novo valor +label_information: Informacao +label_information_plural: Informacoes +label_please_login: Efetue login +label_register: Registre-se +label_password_lost: Perdi a senha +label_home: Pagina inicial +label_my_page: Minha pagina +label_my_account: Minha conta +label_my_projects: Meus projetos +label_administration: Administracao +label_login: Login +label_logout: Logout +label_help: Ajuda +label_reported_issues: Tarefas reportadas +label_assigned_to_me_issues: Tarefas atribuidas a mim +label_last_login: Utima conexao +label_last_updates: Ultima alteracao +label_last_updates_plural: %d Ultimas alteracoes +label_registered_on: Registrado em +label_activity: Atividade +label_new: Novo +label_logged_as: Logado como +label_environment: Ambiente +label_authentication: Autenticacao +label_auth_source: Modo de autenticacao +label_auth_source_new: Novo modo de autenticacao +label_auth_source_plural: Modos de autenticacao +label_subproject_plural: Sub-projetos +label_min_max_length: Tamanho min-max +label_list: Lista +label_date: Data +label_integer: Inteiro +label_boolean: Boleano +label_string: Texto +label_text: Texto longo +label_attribute: Atributo +label_attribute_plural: Atributos +label_download: %d Download +label_download_plural: %d Downloads +label_no_data: Sem dados para mostrar +label_change_status: Mudar status +label_history: Historico +label_attachment: Arquivo +label_attachment_new: Novo arquivo +label_attachment_delete: Apagar arquivo +label_attachment_plural: Arquivos +label_report: Relatorio +label_report_plural: Relatorio +label_news: Noticias +label_news_new: Adicionar noticias +label_news_plural: Noticias +label_news_latest: Ultimas noticias +label_news_view_all: Ver todas as noticias +label_change_log: Change log +label_settings: Ajustes +label_overview: Visao geral +label_version: Versao +label_version_new: Nova versao +label_version_plural: Versoes +label_confirmation: Confirmacao +label_export_to: Exportar para +label_read: Ler... +label_public_projects: Projetos publicos +label_open_issues: Aberto +label_open_issues_plural: Abertos +label_closed_issues: Fechado +label_closed_issues_plural: Fechados +label_total: Total +label_permissions: Permissoes +label_current_status: Status atual +label_new_statuses_allowed: Novo status permitido +label_all: todos +label_none: nenhum +label_next: Proximo +label_previous: Anterior +label_used_by: Usado por +label_details: Detalhes +label_add_note: Adicionar nota +label_per_page: Por pagina +label_calendar: Calendario +label_months_from: Meses de +label_gantt: Gantt +label_internal: Interno +label_last_changes: utlimas %d mudancas +label_change_view_all: Mostrar todas as mudancas +label_personalize_page: Personalizar esta pagina +label_comment: Comentario +label_comment_plural: Comentarios +label_comment_add: Adicionar comentario +label_comment_added: Comentario adicionado +label_comment_delete: Apagar comentario +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: e +label_not_equals: nao e +label_in_less_than: e maior que +label_in_more_than: e menor que +label_in: em +label_today: hoje +label_this_week: this week +label_less_than_ago: faz menos de +label_more_than_ago: faz mais de +label_ago: dias atras +label_contains: contem +label_not_contains: nao contem +label_day_plural: dias +label_repository: Repository +label_browse: Browse +label_modification: %d change +label_modification_plural: %d changes +label_revision: Revision +label_revision_plural: Revisions +label_added: added +label_modified: modified +label_deleted: deleted +label_latest_revision: Latest revision +label_latest_revision_plural: Latest revisions +label_view_revisions: View revisions +label_max_size: Maximum size +label_on: 'em' +label_sort_highest: Mover para o inicio +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: Due in +label_roadmap_overdue: %s late +label_roadmap_no_issues: Sem tarefas para essa versao +label_search: Busca +label_result_plural: Resultados +label_all_words: Todas as palavras +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: Versao atual +label_preview: Previa +label_feed_plural: Feeds +label_changes_details: Detalhes de todas as mudancas +label_issue_tracking: Tarefas +label_spent_time: Tempo gasto +label_f_hour: %.2f hora +label_f_hour_plural: %.2f horas +label_time_tracking: Tempo trabalhado +label_change_plural: Mudancas +label_statistics: Estatisticas +label_commits_per_month: Commits por mes +label_commits_per_author: Commits por autor +label_view_diff: Ver diferencas +label_diff_inline: inline +label_diff_side_by_side: side by side +label_options: Opcoes +label_copy_workflow_from: Copiar workflow de +label_permissions_report: Relatorio de permissoes +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_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_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: Language based +label_sort_by: Sort by %s +label_send_test_email: Send a test email +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: Modules +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s ago +label_jump_to_a_project: Jump to a project... + +button_login: Login +button_submit: Enviar +button_save: Salvar +button_check_all: Marcar todos +button_uncheck_all: Desmarcar todos +button_delete: Apagar +button_create: Criar +button_test: Testar +button_edit: Editar +button_add: Adicionar +button_change: Mudar +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: Ativar +button_sort: Ordenar +button_log_time: Tempo de trabalho +button_rollback: Voltar para esta versao +button_watch: Watch +button_unwatch: Unwatch +button_reply: Reply +button_archive: Archive +button_unarchive: Unarchive +button_reset: Reset +button_rename: Rename + +status_active: ativo +status_registered: registrado +status_locked: bloqueado + +text_select_mail_notifications: Selecionar acoes para ser enviado uma notificacao por email +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 siginifica sem restricao +text_project_destroy_confirmation: Voce tem certeza que deseja deletar este projeto e todas os dados relacionados? +text_workflow_edit: Selecione uma regra e um tipo de tarefa para editar o workflow +text_are_you_sure: Voce tem certeza ? +text_journal_changed: alterado de %s para %s +text_journal_set_to: setar para %s +text_journal_deleted: apagado +text_tip_task_begin_day: tarefa comeca neste dia +text_tip_task_end_day: tarefa termina neste dia +text_tip_task_begin_end_day: tarefa comeca e termina neste dia +text_project_identifier_info: 'Letras minusculas (a-z), numeros e tracos permitido.
Uma vez salvo, o identificador nao pode ser mudado.' +text_caracters_maximum: %d maximo de caracteres +text_length_between: Tamanho entre %d e %d caracteres. +text_tracker_no_workflow: Sem workflow definido para este tipo. +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: Tarefa %s foi incluída (by %s). +text_issue_updated: Tarefa %s foi alterada (by %s). +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassing issues to this category + +default_role_manager: Analista de Negocio ou Gerente de Projeto +default_role_developper: Desenvolvedor +default_role_reporter: Analista de Suporte +default_tracker_bug: Bug +default_tracker_feature: Implementacao +default_tracker_support: Suporte +default_issue_status_new: Novo +default_issue_status_assigned: Atribuido +default_issue_status_resolved: Resolvido +default_issue_status_feedback: Feedback +default_issue_status_closed: Fechado +default_issue_status_rejected: Rejeitado +default_doc_category_user: Documentacao do usuario +default_doc_category_tech: Documentacao do tecnica +default_priority_low: Baixo +default_priority_normal: Normal +default_priority_high: Alto +default_priority_urgent: Urgente +default_priority_immediate: Imediato +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) +label_file_plural: Files +label_changeset_plural: Changesets +field_column_names: Columns +label_default_columns: Default columns +setting_issue_list_default_columns: Default columns displayed on the issue list +setting_repositories_encodings: Repositories encodings +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +label_bulk_edit_selected_issues: Bulk edit selected issues +label_no_change_option: (No change) +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +label_theme: Theme +label_default: Default +label_search_titles_only: Search titles only +label_nobody: nobody +button_change_password: Change password +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)." +label_user_mail_option_selected: "For any event on the selected projects only..." +label_user_mail_option_all: "For any event on all my projects" +label_user_mail_option_none: "Only for things I watch or I'm involved in" +setting_emails_footer: Emails footer +label_float: Float +button_copy: Copy +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +setting_protocol: Protocol +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +setting_time_format: Time format +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' +label_registration_automatic_activation: automatic account activation +label_registration_manual_activation: manual account activation +notice_account_pending: "Your account was created and is now pending administrator approval." +field_time_zone: Time zone +text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: Searchable +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/pt.yml b/groups/lang/pt.yml new file mode 100644 index 000000000..6f51c8ed2 --- /dev/null +++ b/groups/lang/pt.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro +actionview_datehelper_select_month_names_abbr: Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dia +actionview_datehelper_time_in_words_day_plural: %d dias +actionview_datehelper_time_in_words_hour_about: em torno de uma hora +actionview_datehelper_time_in_words_hour_about_plural: em torno de %d horas +actionview_datehelper_time_in_words_hour_about_single: em torno de uma hora +actionview_datehelper_time_in_words_minute: 1 minuto +actionview_datehelper_time_in_words_minute_half: meio minuto +actionview_datehelper_time_in_words_minute_less_than: menos de um minuto +actionview_datehelper_time_in_words_minute_plural: %d minutos +actionview_datehelper_time_in_words_minute_single: 1 minuto +actionview_datehelper_time_in_words_second_less_than: menos de um segundo +actionview_datehelper_time_in_words_second_less_than_plural: menos de %d segundos +actionview_instancetag_blank_option: Selecione + +activerecord_error_inclusion: não existe na lista +activerecord_error_exclusion: já existe na lista +activerecord_error_invalid: é inválido +activerecord_error_confirmation: não confere com sua confirmação +activerecord_error_accepted: deve ser aceito +activerecord_error_empty: não pode ser vazio +activerecord_error_blank: não pode estar em branco +activerecord_error_too_long: é muito longo +activerecord_error_too_short: é muito curto +activerecord_error_wrong_length: possui o comprimento errado +activerecord_error_taken: já foi usado em outro registro +activerecord_error_not_a_number: não é um número +activerecord_error_not_a_date: não é uma data válida +activerecord_error_greater_than_start_date: deve ser maior que a data inicial +activerecord_error_not_same_project: não pertence ao mesmo projeto +activerecord_error_circular_dependency: Este relaão pode criar uma dependência circular + +general_fmt_age: %d ano +general_fmt_age_plural: %d anos +general_fmt_date: %%d/%%m/%%Y +general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sábado,Domingo +general_first_day_of_week: '1' + +notice_account_updated: Conta foi atualizada com sucesso. +notice_account_invalid_creditentials: Usuário ou senha inválidos. +notice_account_password_updated: Senha foi alterada com sucesso. +notice_account_wrong_password: Senha errada. +notice_account_register_done: Conta foi criada com sucesso. +notice_account_unknown_email: Usuário desconhecido. +notice_can_t_change_password: Esta conta usa autenticação externa. E impossível trocar a senha. +notice_account_lost_email_sent: Um email com as instruções para escolher uma nova senha foi enviado para você. +notice_account_activated: Sua conta foi ativada. Você pode logar agora +notice_successful_create: Criado com sucesso. +notice_successful_update: Alterado com sucesso. +notice_successful_delete: Apagado 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 um outro usuário. +notice_not_authorized: Você não está autorizado a acessar esta página. +notice_email_sent: An email was sent to %s +notice_email_error: An error occurred while sending mail (%s) +notice_feeds_access_key_reseted: Your RSS access key was reseted. + +error_scm_not_found: "A entrada e/ou a revisão não existem no repositório." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Sua senha do %s. +mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:' +mail_subject_register: Ativação de conta do %s. +mail_body_register: 'Para ativar sua conta, clique no link abaixo:' + +gui_validation_error: 1 erro +gui_validation_error_plural: %d erros + +field_name: Nome +field_description: Descrição +field_summary: Sumário +field_is_required: Obrigatório +field_firstname: Primeiro nome +field_lastname: Último nome +field_mail: Email +field_filename: Arquivo +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 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: Status +field_notes: Notas +field_is_closed: Tarefa fechada +field_is_default: Status padrão +field_tracker: Tipo +field_subject: Assunto +field_due_date: Data final +field_assigned_to: Atribuído para +field_priority: Prioridade +field_fixed_version: Target version +field_user: Usuário +field_role: Regra +field_homepage: Página inicial +field_is_public: Público +field_parent: Sub-projeto de +field_is_in_chlog: Tarefas mostradas no changelog +field_is_in_roadmap: Tarefas mostradas no roadmap +field_login: Login +field_mail_notification: Notificações por email +field_admin: Administrador +field_last_login_on: Última conexão +field_language: Língua +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: Base DN +field_attr_login: Atributo login +field_attr_firstname: Atributo primeiro nome +field_attr_lastname: Atributo último nome +field_attr_mail: Atributo email +field_onthefly: Criação de usuário sob-demanda +field_start_date: Início +field_done_ratio: %% Terminado +field_auth_source: Modo de autenticação +field_hide_mail: Esconda meu email +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: Usado como filtro +field_issue_to_id: Tarefa relacionada +field_delay: Atraso +field_assignable: Issues can be assigned to this role +field_redirect_existing_links: Redirect existing links +field_estimated_hours: Estimated time +field_default_value: Padrão + +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: Linguagem padrão +setting_login_required: Autenticação obrigatória +setting_self_registration: Registro permitido +setting_attachment_max_size: Tamanho máximo do anexo +setting_issues_export_limit: Limite de exportação das tarefas +setting_mail_from: Email enviado de +setting_host_name: Servidor +setting_text_formatting: Formato do texto +setting_wiki_compression: Compactação do histórico do Wiki +setting_feeds_limit: Limite do Feed +setting_autofetch_changesets: Buscar automaticamente commits +setting_sys_api_enabled: Ativa WS para gerenciamento do repositório +setting_commit_ref_keywords: Palavras-chave de referôncia +setting_commit_fix_keywords: Palavras-chave fixas +setting_autologin: Autologin +setting_date_format: Date format +setting_cross_project_issue_relations: Allow cross-project issue relations + +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_project_all: All Projects +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_document: Documento +label_document_new: Novo documento +label_document_plural: Documentos +label_role: Regra +label_role_plural: Regras +label_role_new: Nova regra +label_role_and_permissions: Regras 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: Status da tarefa +label_issue_status_plural: Status das tarefas +label_issue_status_new: Novo status +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: Enumeração +label_enumeration_new: Novo valor +label_information: Informação +label_information_plural: Informações +label_please_login: Efetue login +label_register: Registre-se +label_password_lost: Perdi a 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: Login +label_logout: Logout +label_help: Ajuda +label_reported_issues: Tarefas reportadas +label_assigned_to_me_issues: Tarefas atribuídas à mim +label_last_login: Útima conexão +label_last_updates: Última alteração +label_last_updates_plural: %d Últimas alterações +label_registered_on: Registrado em +label_activity: Atividade +label_new: Novo +label_logged_as: Logado 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_min_max_length: Tamanho min-max +label_list: Lista +label_date: Data +label_integer: Inteiro +label_boolean: Booleano +label_string: Texto +label_text: Texto longo +label_attribute: Atributo +label_attribute_plural: Atributos +label_download: %d Download +label_download_plural: %d Downloads +label_no_data: Sem dados para mostrar +label_change_status: Mudar status +label_history: Histórico +label_attachment: Arquivo +label_attachment_new: Novo arquivo +label_attachment_delete: Apagar arquivo +label_attachment_plural: Arquivos +label_report: Relatório +label_report_plural: Relatório +label_news: Notícias +label_news_new: Adicionar notícias +label_news_plural: Notícias +label_news_latest: Últimas notícias +label_news_view_all: Ver todas as notícias +label_change_log: Log de mudanças +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: Aberto +label_open_issues_plural: Abertos +label_closed_issues: Fechado +label_closed_issues_plural: Fechados +label_total: Total +label_permissions: Permissões +label_current_status: Status atual +label_new_statuses_allowed: Novo status permitido +label_all: todos +label_none: nenhum +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 %d mudanças +label_change_view_all: Mostrar todas as mudanças +label_personalize_page: Personalizar esta página +label_comment: Comentário +label_comment_plural: Comentários +label_comment_add: Adicionar comentário +label_comment_added: Comentário adicionado +label_comment_delete: Apagar 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: é +label_not_equals: não e +label_in_less_than: é maior que +label_in_more_than: é menor que +label_in: em +label_today: hoje +label_this_week: this week +label_less_than_ago: faz menos de +label_more_than_ago: faz 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_browse: Procurar +label_modification: %d mudança +label_modification_plural: %d mudanças +label_revision: Revisão +label_revision_plural: Revisões +label_added: adicionado +label_modified: modificado +label_deleted: deletado +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_on: em +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 +label_roadmap_overdue: %s late +label_roadmap_no_issues: Sem tarefas para essa versão +label_search: Busca +label_result_plural: Resultados +label_all_words: Todas as palavras +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: Versão atual +label_preview: Prévia +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: %.2f hora +label_f_hour_plural: %.2f horas +label_time_tracking: Tempo trabalhado +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: Status aplicado +label_loading: Carregando... +label_relation_new: Nova relação +label_relation_delete: Deletar relação +label_relates_to: relacionado à +label_duplicates: duplicadas +label_blocks: bloqueios +label_blocked_by: bloqueado por +label_precedes: procede +label_follows: segue +label_end_to_start: fim ao início +label_end_to_end: fim ao fim +label_start_to_start: ínícia ao inícia +label_start_to_end: inícia ao fim +label_stay_logged_in: Rester connecté +label_disabled: désactivé +label_show_completed_versions: Voire les versions passées +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_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: Language based +label_sort_by: Sort by %s +label_send_test_email: Send a test email +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: Modules +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s ago +label_jump_to_a_project: Jump to a project... + +button_login: Login +button_submit: Enviar +button_save: Salvar +button_check_all: Marcar todos +button_uncheck_all: Desmarcar todos +button_delete: Apagar +button_create: Criar +button_test: Testar +button_edit: Editar +button_add: Adicionar +button_change: Mudar +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: Ativar +button_sort: Ordenar +button_log_time: Tempo de trabalho +button_rollback: Voltar para esta versão +button_watch: Observar +button_unwatch: Não observar +button_reply: Reply +button_archive: Archive +button_unarchive: Unarchive +button_reset: Reset +button_rename: Rename + +status_active: ativo +status_registered: registrado +status_locked: bloqueado + +text_select_mail_notifications: Selecionar ações para ser enviada uma notificação por email +text_regexp_info: ex. ^[A-Z0-9]+$ +text_min_max_length_info: 0 siginifica sem restrição +text_project_destroy_confirmation: Você tem certeza que deseja deletar este projeto e todos os dados relacionados? +text_workflow_edit: Selecione uma regra e um tipo de tarefa para editar o workflow +text_are_you_sure: Você tem certeza ? +text_journal_changed: alterado de %s para %s +text_journal_set_to: alterar para %s +text_journal_deleted: apagado +text_tip_task_begin_day: tarefa começa neste dia +text_tip_task_end_day: tarefa termina neste dia +text_tip_task_begin_end_day: tarefa começa e termina neste dia +text_project_identifier_info: 'Letras minúsculas (a-z), números e traços permitido.
Uma vez salvo, o identificador nao pode ser mudado.' +text_caracters_maximum: %d móximo de caracteres +text_length_between: Tamanho entre %d e %d caracteres. +text_tracker_no_workflow: Sem workflow definido para este tipo. +text_unallowed_characters: Caracteres não permitidos +text_comma_separated: Permitido múltiplos valores (separados por vírgula). +text_issues_ref_in_commit_messages: Referenciando e arrumando tarefas nas mensagens de commit +text_issue_added: Tarefa %s foi incluída (by %s). +text_issue_updated: Tarefa %s foi alterada (by %s). +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassing issues to this category + +default_role_manager: Analista de Negócio ou Gerente de Projeto +default_role_developper: Desenvolvedor +default_role_reporter: Analista de Suporte +default_tracker_bug: Bug +default_tracker_feature: Implementaçõo +default_tracker_support: Suporte +default_issue_status_new: Novo +default_issue_status_assigned: Atribuído +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 do usuário +default_doc_category_tech: Documentação técnica +default_priority_low: Baixo +default_priority_normal: Normal +default_priority_high: Alto +default_priority_urgent: Urgente +default_priority_immediate: Imediato +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) +label_file_plural: Files +label_changeset_plural: Changesets +field_column_names: Columns +label_default_columns: Default columns +setting_issue_list_default_columns: Default columns displayed on the issue list +setting_repositories_encodings: Repositories encodings +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +label_bulk_edit_selected_issues: Bulk edit selected issues +label_no_change_option: (No change) +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +label_theme: Theme +label_default: Default +label_search_titles_only: Search titles only +label_nobody: nobody +button_change_password: Change password +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)." +label_user_mail_option_selected: "For any event on the selected projects only..." +label_user_mail_option_all: "For any event on all my projects" +label_user_mail_option_none: "Only for things I watch or I'm involved in" +setting_emails_footer: Emails footer +label_float: Float +button_copy: Copy +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +setting_protocol: Protocol +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +setting_time_format: Time format +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' +label_registration_automatic_activation: automatic account activation +label_registration_manual_activation: manual account activation +notice_account_pending: "Your account was created and is now pending administrator approval." +field_time_zone: Time zone +text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: Searchable +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/ro.yml b/groups/lang/ro.yml new file mode 100644 index 000000000..59edfeb70 --- /dev/null +++ b/groups/lang/ro.yml @@ -0,0 +1,620 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Ianuarie,Februarie,Martie,Aprilie,Mai,Iunie,Iulie,August,Septembrie,Octombrie,Noiembrie,Decembrie +actionview_datehelper_select_month_names_abbr: Ian,Feb,Mar,Apr,Mai,Jun,Jul,Aug,Sep,Oct,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 zi +actionview_datehelper_time_in_words_day_plural: %d zile +actionview_datehelper_time_in_words_hour_about: aproximativ o ora +actionview_datehelper_time_in_words_hour_about_plural: aproximativ %d ore +actionview_datehelper_time_in_words_hour_about_single: aproximativ o ora +actionview_datehelper_time_in_words_minute: 1 minut +actionview_datehelper_time_in_words_minute_half: 30 de secunde +actionview_datehelper_time_in_words_minute_less_than: mai putin de un minut +actionview_datehelper_time_in_words_minute_plural: %d minute +actionview_datehelper_time_in_words_minute_single: 1 minut +actionview_datehelper_time_in_words_second_less_than: mai putin de o secunda +actionview_datehelper_time_in_words_second_less_than_plural: mai putin de %d secunde +actionview_instancetag_blank_option: Va rog selectati + +activerecord_error_inclusion: nu este inclus in lista +activerecord_error_exclusion: este rezervat +activerecord_error_invalid: este invalid +activerecord_error_confirmation: nu corespunde confirmarii +activerecord_error_accepted: trebuie acceptat +activerecord_error_empty: nu poate fi gol +activerecord_error_blank: nu poate fi gol +activerecord_error_too_long: este prea lung +activerecord_error_too_short: este prea scurt +activerecord_error_wrong_length: are lungimea eronata +activerecord_error_taken: deja a fost luat/rezervat +activerecord_error_not_a_number: nu este un numar +activerecord_error_not_a_date: nu este o data valida +activerecord_error_greater_than_start_date: trebuie sa fie mai mare ca data de start +activerecord_error_not_same_project: nu apartine projectului respectiv +activerecord_error_circular_dependency: Aceasta relatie ar crea dependenta circulara + +general_fmt_age: %d an +general_fmt_age_plural: %d ani +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Luni,Marti,Miercuri,Joi,Vineri,Sambata,Duminica +general_first_day_of_week: '7' + +notice_account_updated: Contul a fost creat cu succes. +notice_account_invalid_creditentials: Numele utilizator sau parola este invalida. +notice_account_password_updated: Parola a fost modificata cu succes. +notice_account_wrong_password: Parola gresita +notice_account_register_done: Contul a fost creat cu succes. Pentru activarea contului folositi linkul primit in e-mailul de confirmare. +notice_account_unknown_email: Utilizator inexistent. +notice_can_t_change_password: Acest cont foloseste un sistem de autenticare externa. Parola nu poate fi schimbata. +notice_account_lost_email_sent: Un e-mail cu instructiuni de a seta noua parola a fost trimisa. +notice_account_activated: Contul a fost activat. Acum puteti intra in cont. +notice_successful_create: Creat cu succes. +notice_successful_update: Modificare cu succes. +notice_successful_delete: Stergere cu succes. +notice_successful_connection: Conectare cu succes. +notice_file_not_found: Pagina dorita nu exista sau nu mai este valabila. +notice_locking_conflict: Informatiile au fost modificate de un alt utilizator. +notice_not_authorized: Nu aveti autorizatia sa accesati aceasta pagina. +notice_email_sent: Un e-mail a fost trimis la adresa %s +notice_email_error: Eroare in trimiterea e-mailului (%s) +notice_feeds_access_key_reseted: Parola de acces RSS a fost resetat. + +error_scm_not_found: "Articolul sau reviziunea nu exista in stoc (Repository)." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Your %s password +mail_body_lost_password: 'To change your password, click on the following link:' +mail_subject_register: Your %s account activation +mail_body_register: 'To activate your account, click on the following link:' + +gui_validation_error: 1 eroare +gui_validation_error_plural: %d erori + +field_name: Nume +field_description: Descriere +field_summary: Sumar +field_is_required: Obligatoriu +field_firstname: Nume +field_lastname: Prenume +field_mail: Email +field_filename: Fisier +field_filesize: Marimea fisierului +field_downloads: Download +field_author: Autor +field_created_on: Creat +field_updated_on: Modificat +field_field_format: Format +field_is_for_all: Pentru toate proiectele +field_possible_values: Valori posibile +field_regexp: Expresie regulara +field_min_length: Lungime minima +field_max_length: Lungime maxima +field_value: Valoare +field_category: Categorie +field_title: Titlu +field_project: Proiect +field_issue: Tichet +field_status: Statut +field_notes: Note +field_is_closed: Tichet rezolvat +field_is_default: Statut de baza +field_tracker: Tip tichet +field_subject: Subiect +field_due_date: Data finalizarii +field_assigned_to: Atribuit pentru +field_priority: Prioritate +field_fixed_version: Target version +field_user: Utilizator +field_role: Rol +field_homepage: Pagina principala +field_is_public: Public +field_parent: Subproiect al +field_is_in_chlog: Tichetele sunt vizibile in changelog +field_is_in_roadmap: Tichetele sunt vizibile in roadmap +field_login: Autentificare +field_mail_notification: Notificari prin e-mail +field_admin: Administrator +field_last_login_on: Ultima conectare +field_language: Limba +field_effective_date: Data +field_password: Parola +field_new_password: Parola noua +field_password_confirmation: Confirmare +field_version: Versiune +field_type: Tip +field_host: Host +field_port: Port +field_account: Cont +field_base_dn: Base DN +field_attr_login: Atribut autentificare +field_attr_firstname: Atribut nume +field_attr_lastname: Atribut prenume +field_attr_mail: Atribut e-mail +field_onthefly: Creare utilizator on-the-fly (rapid) +field_start_date: Start +field_done_ratio: %% rezolvat +field_auth_source: Mod de autentificare +field_hide_mail: Ascunde adresa de e-mail +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: Folosit ca un filtru +field_issue_to_id: Articole similare +field_delay: Intarziere +field_assignable: La acest rol se poate atribui tichete +field_redirect_existing_links: Redirectare linkuri existente +field_estimated_hours: Timpul estimat +field_default_value: Default value + +setting_app_title: Titlul aplicatiei +setting_app_subtitle: Subtitlul aplicatiei +setting_welcome_text: Textul de intampinare +setting_default_language: Limbajul +setting_login_required: Autentificare obligatorie +setting_self_registration: Inregistrarea utilizatorilor pe cont propriu este permisa +setting_attachment_max_size: Lungimea maxima al attachmentului +setting_issues_export_limit: Limita de exportare a tichetelor +setting_mail_from: Adresa de e-mail al emitatorului +setting_host_name: Numele hostului +setting_text_formatting: Formatarea textului +setting_wiki_compression: Compresie istoric wiki +setting_feeds_limit: Limita continut feed +setting_autofetch_changesets: Autofetch commits +setting_sys_api_enabled: Setare WS pentru managementul stocului (repository) +setting_commit_ref_keywords: Cuvinte cheie de referinta +setting_commit_fix_keywords: Cuvinte cheie de rezolvare +setting_autologin: Autentificare automata +setting_date_format: Formatul datelor +setting_cross_project_issue_relations: Tichetele pot avea relatii intre diferite proiecte + +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_project_all: Toate proiectele +label_project_latest: Ultimele proiecte +label_issue: Tichet +label_issue_new: Tichet nou +label_issue_plural: Tichete +label_issue_view_all: Vizualizare toate tichetele +label_document: Document +label_document_new: Document nou +label_document_plural: Documente +label_role: Rol +label_role_plural: Roluri +label_role_new: Rol nou +label_role_and_permissions: Roluri si permisiuni +label_member: Membru +label_member_new: Membru nou +label_member_plural: Membrii +label_tracker: Tip tichet +label_tracker_plural: Tipuri de tichete +label_tracker_new: Tip tichet nou +label_workflow: Workflow +label_issue_status: Statut tichet +label_issue_status_plural: Statut tichete +label_issue_status_new: Statut nou +label_issue_category: Categorie tichet +label_issue_category_plural: Categorii tichete +label_issue_category_new: Categorie noua +label_custom_field: Camp personalizat +label_custom_field_plural: Campuri personalizate +label_custom_field_new: Camp personalizat nou +label_enumerations: Enumeratii +label_enumeration_new: Valoare noua +label_information: Informatie +label_information_plural: Informatii +label_please_login: Va rugam sa va autentificati +label_register: Inregistrare +label_password_lost: Parola pierduta +label_home: Prima pagina +label_my_page: Pagina mea +label_my_account: Contul meu +label_my_projects: Proiectele mele +label_administration: Administrare +label_login: Autentificare +label_logout: Iesire din cont +label_help: Ajutor +label_reported_issues: Tichete raportate +label_assigned_to_me_issues: Tichete atribuite pentru mine +label_last_login: Ultima conectare +label_last_updates: Ultima modificare +label_last_updates_plural: ultimele %d modificari +label_registered_on: Inregistrat la +label_activity: Activitate +label_new: Nou +label_logged_as: Inregistrat ca +label_environment: Mediu +label_authentication: Autentificare +label_auth_source: Modul de autentificare +label_auth_source_new: Mod de autentificare noua +label_auth_source_plural: Moduri de autentificare +label_subproject_plural: Subproiecte +label_min_max_length: Lungime min-max +label_list: Lista +label_date: Data +label_integer: Numar intreg +label_boolean: Variabila logica +label_string: Text +label_text: text lung +label_attribute: Atribut +label_attribute_plural: Attribute +label_download: %d Download +label_download_plural: %d Downloads +label_no_data: Nu exista date de vizualizat +label_change_status: Schimbare statut +label_history: Istoric +label_attachment: Fisier +label_attachment_new: Fisier nou +label_attachment_delete: Stergere fisier +label_attachment_plural: Fisiere +label_report: Raport +label_report_plural: Rapoarte +label_news: Stiri +label_news_new: Adauga stiri +label_news_plural: Stiri +label_news_latest: Ultimele noutati +label_news_view_all: Vizualizare stiri +label_change_log: Change log +label_settings: Setari +label_overview: Sumar +label_version: Versiune +label_version_new: Versiune noua +label_version_plural: Versiuni +label_confirmation: Confirmare +label_export_to: Exportare in +label_read: Citire... +label_public_projects: Proiecte publice +label_open_issues: deschis +label_open_issues_plural: deschise +label_closed_issues: rezolvat +label_closed_issues_plural: rezolvate +label_total: Total +label_permissions: Permisiuni +label_current_status: Statut curent +label_new_statuses_allowed: Drepturi de a schimba statutul in +label_all: toate +label_none: n/a +label_next: Urmator +label_previous: Anterior +label_used_by: Folosit de +label_details: Detalii +label_add_note: Adauga o nota +label_per_page: Per pagina +label_calendar: Calendar +label_months_from: luni incepand cu +label_gantt: Gantt +label_internal: Internal +label_last_changes: ultimele %d modificari +label_change_view_all: Vizualizare toate modificarile +label_personalize_page: Personalizeaza aceasta pagina +label_comment: Comentariu +label_comment_plural: Comentarii +label_comment_add: Adauga un comentariu +label_comment_added: Comentariu adaugat +label_comment_delete: Stergere comentarii +label_query: Raport personalizat +label_query_plural: Rapoarte personalizate +label_query_new: Raport nou +label_filter_add: Adauga filtru +label_filter_plural: Filtre +label_equals: egal cu +label_not_equals: nu este egal cu +label_in_less_than: este mai putin decat +label_in_more_than: este mai mult ca +label_in: in +label_today: azi +label_this_week: saptamana curenta +label_less_than_ago: recent +label_more_than_ago: mai multe zile +label_ago: in ultimele zile +label_contains: contine +label_not_contains: nu contine +label_day_plural: zile +label_repository: Stoc (Repository) +label_browse: Navigare +label_modification: %d modificare +label_modification_plural: %d modificari +label_revision: Revizie +label_revision_plural: Revizii +label_added: adaugat +label_modified: modificat +label_deleted: sters +label_latest_revision: Ultima revizie +label_latest_revision_plural: Ultimele revizii +label_view_revisions: Vizualizare revizii +label_max_size: Marime maxima +label_on: 'din' +label_sort_highest: Muta prima +label_sort_higher: Muta sus +label_sort_lower: Mota jos +label_sort_lowest: Mota ultima +label_roadmap: Harta activitatiilor +label_roadmap_due_in: Rezolvat in +label_roadmap_overdue: %s intarziere +label_roadmap_no_issues: Nu sunt tichete pentru aceasta reviziune +label_search: Cauta +label_result_plural: Rezultate +label_all_words: Toate cuvintele +label_wiki: Wiki +label_wiki_edit: Editare wiki +label_wiki_edit_plural: Editari wiki +label_wiki_page: Pagina wiki +label_wiki_page_plural: Pagini wiki +label_current_version: Versiunea curenta +label_preview: Pre-vizualizare +label_feed_plural: Feeduri +label_changes_details: Detaliile modificarilor +label_issue_tracking: Urmarire tichete +label_spent_time: Timp consumat +label_f_hour: %.2f ora +label_f_hour_plural: %.2f ore +label_time_tracking: Urmarire timp +label_change_plural: Schimbari +label_statistics: Statistici +label_commits_per_month: Rezolvari lunare +label_commits_per_author: Rezolvari +label_view_diff: Vizualizare diferente +label_diff_inline: inline +label_diff_side_by_side: side by side +label_options: Optiuni +label_copy_workflow_from: Copiaza workflow de la +label_permissions_report: Raportul permisiunilor +label_watched_issues: Tichete urmarite +label_related_issues: Tichete similare +label_applied_status: Statut aplicat +label_loading: Incarcare... +label_relation_new: Relatie noua +label_relation_delete: Stergere relatie +label_relates_to: relatat la +label_duplicates: duplicate +label_blocks: blocuri +label_blocked_by: blocat de +label_precedes: precedes +label_follows: follows +label_end_to_start: de la sfarsit la capat +label_end_to_end: de la sfarsit la sfarsit +label_start_to_start: de la capat la capat +label_start_to_end: de la sfarsit la capat +label_stay_logged_in: Ramane autenticat +label_disabled: dezactivata +label_show_completed_versions: Vizualizare verziuni completate +label_me: mine +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_reply_plural: Raspunsuri +label_send_information: Trimite informatii despre cont pentru utilizator +label_year: An +label_month: Luna +label_week: Saptamana +label_date_from: De la +label_date_to: Pentru +label_language_based: Bazat pe limbaj +label_sort_by: Sortare dupa %s +label_send_test_email: trimite un e-mail de test +label_feeds_access_key_created_on: Parola de acces RSS creat cu %s mai devreme +label_module_plural: Module +label_added_time_by: Adaugat de %s %s mai devreme +label_updated_time: Modificat %s mai devreme +label_jump_to_a_project: Alege un proiect ... + +button_login: Autentificare +button_submit: Trimite +button_save: Salveaza +button_check_all: Bifeaza toate +button_uncheck_all: Reseteaza toate +button_delete: Sterge +button_create: Creare +button_test: Test +button_edit: Editare +button_add: Adauga +button_change: Modificare +button_apply: Aplicare +button_clear: Resetare +button_lock: Inchide +button_unlock: Deschide +button_download: Download +button_list: Listare +button_view: Vizualizare +button_move: Mutare +button_back: Inapoi +button_cancel: Anulare +button_activate: Activare +button_sort: Sortare +button_log_time: Log time +button_rollback: Inapoi la aceasta versiune +button_watch: Urmarie +button_unwatch: Terminare urmarire +button_reply: Raspuns +button_archive: Arhivare +button_unarchive: Dezarhivare +button_reset: Reset +button_rename: Redenumire + +status_active: activ +status_registered: inregistrat +status_locked: inchis + +text_select_mail_notifications: Selectare actiuni pentru care se va trimite notificari prin e-mail. +text_regexp_info: de exemplu ^[A-Z0-9]+$ +text_min_max_length_info: 0 inseamna fara restrictii +text_project_destroy_confirmation: Sunteti sigur ca vreti sa stergeti acest proiect si toate datele aferente ? +text_workflow_edit: Selecteaza un rol si un tip tichet pentru a edita acest workflow +text_are_you_sure: Sunteti sigur ? +text_journal_changed: modificat de la %s la %s +text_journal_set_to: setat la %s +text_journal_deleted: sters +text_tip_task_begin_day: activitate care incepe azi +text_tip_task_end_day: activitate care se termina azi +text_tip_task_begin_end_day: activitate care incepe si se termina azi +text_project_identifier_info: 'Se poate folosi caracterele a-z si cifrele.
Odata salvat identificatorul nu poate fi modificat.' +text_caracters_maximum: maximum %d caractere. +text_length_between: Lungimea intre %d si %d caractere. +text_tracker_no_workflow: Nu este definit nici un workflow pentru acest tip de tichet +text_unallowed_characters: Caractere nepermise +text_comma_separated: Se poate folosi valori multiple (separate de virgula). +text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages +text_issue_added: Tichetul %s a fost raportat (by %s). +text_issue_updated: tichetul %s a fost modificat (by %s). +text_wiki_destroy_confirmation: Sunteti sigur ca vreti sa stergeti acest wiki si continutul ei ? +text_issue_category_destroy_question: Cateva tichete (%d) apartin acestei categorii. Cum vreti sa procedati ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassing issues to this category + +default_role_manager: Manager +default_role_developper: Programator +default_role_reporter: Creator rapoarte +default_tracker_bug: Defect +default_tracker_feature: Functionalitate +default_tracker_support: Suport +default_issue_status_new: Nou +default_issue_status_assigned: Atribuit +default_issue_status_resolved: Rezolvat +default_issue_status_feedback: Feedback +default_issue_status_closed: Rezolvat +default_issue_status_rejected: Respins +default_doc_category_user: Documentatie +default_doc_category_tech: Documentatie tehnica +default_priority_low: Redusa +default_priority_normal: Normala +default_priority_high: Ridicata +default_priority_urgent: Urgenta +default_priority_immediate: Imediata +default_activity_design: Design +default_activity_development: Programare + +enumeration_issue_priorities: Prioritati tichet +enumeration_doc_categories: Categorii documente +enumeration_activities: Activitati (urmarite in timp) +label_index_by_date: Index by date +label_index_by_title: Index by title +label_file_plural: Files +label_changeset_plural: Changesets +field_column_names: Columns +label_default_columns: Default columns +setting_issue_list_default_columns: Default columns displayed on the issue list +setting_repositories_encodings: Repositories encodings +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +label_bulk_edit_selected_issues: Bulk edit selected issues +label_no_change_option: (No change) +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +label_theme: Theme +label_default: Default +label_search_titles_only: Search titles only +label_nobody: nobody +button_change_password: Change password +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)." +label_user_mail_option_selected: "For any event on the selected projects only..." +label_user_mail_option_all: "For any event on all my projects" +label_user_mail_option_none: "Only for things I watch or I'm involved in" +setting_emails_footer: Emails footer +label_float: Float +button_copy: Copy +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +setting_protocol: Protocol +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +setting_time_format: Time format +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' +label_registration_automatic_activation: automatic account activation +label_registration_manual_activation: manual account activation +notice_account_pending: "Your account was created and is now pending administrator approval." +field_time_zone: Time zone +text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: Searchable +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/ru.yml b/groups/lang/ru.yml new file mode 100644 index 000000000..f69009847 --- /dev/null +++ b/groups/lang/ru.yml @@ -0,0 +1,624 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Январь,Февраль,Март,Ðпрель,Май,Июнь,Июль,ÐвгуÑÑ‚,СентÑбрь,ОктÑбрь,ÐоÑбрь,Декабрь +actionview_datehelper_select_month_names_abbr: Янв,Фев,Мар,Ðпр,Май,Июн,Июл,Ðвг,Сен,Окт,ÐоÑб,Дек +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 день +actionview_datehelper_time_in_words_day_plural: %d дней(Ñ) +actionview_datehelper_time_in_words_hour_about: около чаÑа +actionview_datehelper_time_in_words_hour_about_plural: около %d чаÑов +actionview_datehelper_time_in_words_hour_about_single: около чаÑа +actionview_datehelper_time_in_words_minute: 1 минута +actionview_datehelper_time_in_words_minute_half: полминуты +actionview_datehelper_time_in_words_minute_less_than: менее минуты +actionview_datehelper_time_in_words_minute_plural: %d минут(Ñ‹) +actionview_datehelper_time_in_words_minute_single: 1 минута +actionview_datehelper_time_in_words_second_less_than: менее Ñекунды +actionview_datehelper_time_in_words_second_less_than_plural: менее %d Ñекунд +actionview_instancetag_blank_option: Выберите + +activerecord_error_inclusion: нет в ÑпиÑке +activerecord_error_exclusion: зарезервировано +activerecord_error_invalid: неверное значение +activerecord_error_confirmation: ошибка в подтверждении +activerecord_error_accepted: необходимо принÑть +activerecord_error_empty: необходимо заполнить +activerecord_error_blank: необходимо заполнить +activerecord_error_too_long: Ñлишком длинное значение +activerecord_error_too_short: Ñлишком короткое значение +activerecord_error_wrong_length: не ÑоответÑтвует длине +activerecord_error_taken: уже иÑпользуетÑÑ +activerecord_error_not_a_number: не ÑвлÑетÑÑ Ñ‡Ð¸Ñлом +activerecord_error_not_a_date: дата недейÑтвительна +activerecord_error_greater_than_start_date: должна быть позднее даты начала +activerecord_error_not_same_project: не отноÑÑÑ‚ÑÑ Ðº одному проекту +activerecord_error_circular_dependency: Ð¢Ð°ÐºÐ°Ñ ÑвÑзь приведет к цикличеÑкой завиÑимоÑти + +general_fmt_age: %d г. +general_fmt_age_plural: %d гг. +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'Ðет' +general_text_Yes: 'Да' +general_text_no: 'Ðет' +general_text_yes: 'Да' +general_lang_name: 'Russian (РуÑÑкий)' +general_csv_separator: ',' +general_csv_encoding: UTF-8 +general_pdf_encoding: UTF-8 +general_day_names: Понедельник,Вторник,Среда,Четверг,ПÑтница,Суббота,ВоÑкреÑенье +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: Отправлено пиÑьмо %s +notice_email_error: Во Ð²Ñ€ÐµÐ¼Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸ пиÑьма произошла ошибка (%s) +notice_feeds_access_key_reseted: Ваш ключ доÑтупа RSS был перезапущен. +notice_failed_to_save_issues: "Ðе удалоÑÑŒ Ñохранить %d пункт(ов)из %d выбранных: %s." +notice_no_issue_selected: "Ðе выбрано ни одной задачи! ПожалуйÑта, отметьте задачи, которые вы хотите отредактировать." +notice_account_pending: "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ уже Ñоздан и ожидает Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратора." +notice_default_data_loaded: Была загружена ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¿Ð¾-умолчанию. + +error_scm_not_found: Хранилилище не Ñодержит запиÑи и/или иÑправлениÑ. +error_scm_command_failed: "Ошибка доÑтупа к хранилищу: %s" +error_can_t_load_default_data: "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð¿Ð¾ умолчанию не была загружена: %s" + +mail_subject_lost_password: Ваш %s пароль +mail_body_lost_password: 'Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ, зайдите по Ñледующей ÑÑылке:' +mail_subject_register: ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ð¾Ð¹ запиÑи %s +mail_body_register: 'Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ учетной запиÑи, зайдите по Ñледующей ÑÑылке:' +mail_body_account_information_external: Ð’Ñ‹ можете иÑпользовать вашу "%s" учетную запиÑÑŒ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ð°. +mail_body_account_information: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Вашей учетной запиÑи +mail_subject_account_activation_request: Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° активацию Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð² ÑиÑтеме %s +mail_body_account_activation_request: 'Ðовый пользователь (%s) зарегиÑтрирован. Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ ожидает вашего утверждениÑ:' + + +gui_validation_error: 1 ошибка +gui_validation_error_plural: %d ошибки(ок) + +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: Базовое отличительное Ð¸Ð¼Ñ +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_id: СвÑзанные задачи +field_delay: Отложить +field_assignable: Задача может быть назначена Ñтой роли +field_redirect_existing_links: Перенаправить ÑущеÑтвующие ÑÑылки +field_estimated_hours: Оцененное Ð²Ñ€ÐµÐ¼Ñ +field_column_names: Колонки +field_default_value: Значение по умолчанию +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_host_name: Ð˜Ð¼Ñ ÐºÐ¾Ð¼Ð¿ÑŒÑŽÑ‚ÐµÑ€Ð° +setting_text_formatting: Форматирование текÑта +setting_wiki_compression: Сжатие иÑтории Wiki +setting_feeds_limit: Ограничение кол-ва заголовков Ð´Ð»Ñ RSS потока +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: ПодÑтрочные Ð¿Ñ€Ð¸Ð¼ÐµÑ‡Ð°Ð½Ð¸Ñ Emailов +setting_protocol: Протокол +setting_bcc_recipients: ИÑпользовать Ñкрытые ÑпиÑки (bcc) +setting_per_page_options: Кол-во Ñтрок на Ñтраницу +setting_user_format: Формат Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ + +label_user: Пользователь +label_user_plural: Пользователи +label_user_new: Ðовый пользователь +label_project: Проект +label_project_new: Ðовый проект +label_project_plural: Проекты +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_last_updates: ПоÑледнее обновление +label_last_updates_plural: %d поÑледние Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ +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: %d Загружено +label_download_plural: %d Загрузок +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_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: менее %d изменений +label_change_view_all: ПроÑмотреть вÑе Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ +label_personalize_page: ПерÑонализировать данную Ñтраницу +label_comment: Комментировать +label_comment_plural: Комментарии +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: %d изменение +label_modification_plural: %d изменений +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_on: 'из' +label_sort_highest: Ð’ начало +label_sort_higher: Вверх +label_sort_lower: Вниз +label_sort_lowest: Ð’ конец +label_roadmap: Оперативный план +label_roadmap_due_in: Ð’Ð¾Ð²Ñ€ÐµÐ¼Ñ +label_roadmap_overdue: %s опоздание +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: %.2f Ñ‡Ð°Ñ +label_f_hour_plural: %.2f чаÑов(а) +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: Сортировать по %s +label_send_test_email: ПоÑлать email Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸ +label_feeds_access_key_created_on: Ключ доÑтупа RSS Ñоздан %s назад +label_module_plural: Модули +label_added_time_by: Добавлен %s %s назад +label_updated_time: Обновлен %s назад +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: Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ñ‹Ñ… запиÑей по email +label_registration_automatic_activation: автоматичеÑÐºÐ°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡Ñ‚ÐµÐ½Ñ‹Ñ… запиÑей +label_registration_manual_activation: активировать учетные запиÑи вручную +label_age: ВозраÑÑ‚ +label_change_properties: Изменить ÑвойÑтва +label_general: Общее +label_repository_plural: Хранилища +label_associated_revisions: СвÑзанные редакции +label_issues_by: Сортировать по %s +label_display_per_page: 'Ðа Ñтраницу: %s' + +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: Обновить + +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_journal_changed: параметр изменилÑÑ Ñ %s на %s +text_journal_set_to: параметр изменилÑÑ Ð½Ð° %s +text_journal_deleted: удалено +text_tip_task_begin_day: дата начала задачи +text_tip_task_end_day: дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ +text_tip_task_begin_end_day: начало задачи и окончание ее в Ñтот день +text_project_identifier_info: 'Строчные буквы (a-z), допуÑтимы цифры и дефиÑ.
Сохраненный идентификатор не может быть изменен.' +text_caracters_maximum: %d Ñимволов(а) макÑимум. +text_length_between: Длина между %d и %d Ñимволов. +text_tracker_no_workflow: Ð”Ð»Ñ Ñтого трекера поÑледовательноÑть дейÑтвий не определена +text_unallowed_characters: Запрещенные Ñимволы +text_comma_separated: ДопуÑтимы неÑколько значений (разделенные запÑтой). +text_issues_ref_in_commit_messages: СопоÑтавление и изменение ÑтатуÑа задач иÑÑ…Ð¾Ð´Ñ Ð¸Ð· текÑта Ñообщений +text_issue_added: По задаче %s был Ñоздан отчет (%s). +text_issue_updated: Задача %s была обновлена (%s). +text_wiki_destroy_confirmation: Ð’Ñ‹ уверены, что хотите удалить данную вики и вÑе Ñодержимое? +text_issue_category_destroy_question: ÐеÑколько задач (%d) назначено в данную категорию. Что вы хотите предпринÑть? +text_issue_category_destroy_assignments: Удалить Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ð¸ +text_issue_category_reassign_to: Переназначить задачи Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ категории +text_user_mail_option: "Ð”Ð»Ñ Ð½ÐµÐ²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ñ‹Ñ… проектов, вы будете получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ о том что проÑматриваете или в чем учаÑтвуете (например, вопроÑÑ‹ автором которых вы ÑвлÑетеÑÑŒ или которые вам назначены)." +text_caracters_minimum: Должно быть не менее %d знаков. +text_load_default_configuration: Загрузить конфигурацию по-умолчанию +text_no_configuration_data: "Роли, трекеры, ÑтатуÑÑ‹ задач и оперативный план не были Ñконфигурированы.\nÐаÑтоÑтельно рекомендуетÑÑ Ð·Ð°Ð³Ñ€ÑƒÐ·Ð¸Ñ‚ÑŒ конфигурацию по-умолчанию. Ð’Ñ‹ Ñможете её изменить потом." + +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_assigned: Ðазначен +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: Реализовано в %s редакции. +label_more: Больше +text_issues_destroy_confirmation: 'Ð’Ñ‹ уверены, что хотите удалить выбранные задачи?' +label_scm: 'Тип хранилища' +text_select_project_modules: 'Выберите модули, которые будут иÑпользованы в проекте:' +label_issue_added: Задача добавлена +label_issue_updated: Задача обновлена +label_document_added: Документ добавлен +label_message_posted: Сообщение добавлено +label_file_added: Файл добавлен +label_news_added: ÐовоÑть добавлена +label_calendar_filter: Ð’ÐºÐ»ÑŽÑ‡Ð°Ñ +label_calendar_no_assigned: не мои +label_timelog_today: РаÑход времени за ÑÐµÐ³Ð¾Ð´Ð½Ñ +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: Скачек +label_this_month: Ñтот меÑÑц +label_last_n_days: поÑледние %d дней +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: Ð’Ñ‹ ÑобираетеÑÑŒ удалить %.02f чаÑа(ов) прикрепленных за Ñтой задачей. +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): %s will be also deleted.' diff --git a/groups/lang/sr.yml b/groups/lang/sr.yml new file mode 100644 index 000000000..d9869c362 --- /dev/null +++ b/groups/lang/sr.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Januar,Februar,Mart,April,Maj,Jun,Jul,Avgust,Septembar,Oktobar,Novembar,Decembar +actionview_datehelper_select_month_names_abbr: Jan,Feb,Mar,Apr,Maj,Jun,Jul,Avg,Sep,Okt,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dan +actionview_datehelper_time_in_words_day_plural: %d dana +actionview_datehelper_time_in_words_hour_about: oko sat vremena +actionview_datehelper_time_in_words_hour_about_plural: oko %d sati +actionview_datehelper_time_in_words_hour_about_single: oko sat vremena +actionview_datehelper_time_in_words_minute: 1 minut +actionview_datehelper_time_in_words_minute_half: pola minuta +actionview_datehelper_time_in_words_minute_less_than: manje od minut +actionview_datehelper_time_in_words_minute_plural: %d minuta +actionview_datehelper_time_in_words_minute_single: 1 minut +actionview_datehelper_time_in_words_second_less_than: manje od sekunde +actionview_datehelper_time_in_words_second_less_than_plural: manje od %d sekundi +actionview_instancetag_blank_option: Molim izaberite + +activerecord_error_inclusion: nije ukljuÄen u listu +activerecord_error_exclusion: je rezervisan +activerecord_error_invalid: je pogreÅ¡an +activerecord_error_confirmation: Ne slaže se sa potvrdom +activerecord_error_accepted: mora biti prihvaćen +activerecord_error_empty: ne sme biti prazan +activerecord_error_blank: ne sme biti prazno +activerecord_error_too_long: je suvise dugaÄko +activerecord_error_too_short: je suvise kratko +activerecord_error_wrong_length: je pogreÅ¡ne dužine +activerecord_error_taken: je već zauzeto +activerecord_error_not_a_number: nije broj +activerecord_error_not_a_date: nije datum +activerecord_error_greater_than_start_date: mora biti veći od poÄetnog datuma +activerecord_error_not_same_project: ne pripada istom projektu +activerecord_error_circular_dependency: Ova relacija bi kreirala kružnu zavisnost + +general_fmt_age: %d g +general_fmt_age_plural: %d god. +general_fmt_date: %%m/%%d/%%G +general_fmt_datetime: %%m/%%d/%%G %%H:%%M %%p +general_fmt_datetime_short: %%b %%d, %%H:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: Ponedeljak, Utorak, Sreda, Äetvrtak, Petak, Subota, Nedelja +general_first_day_of_week: '1' + +notice_account_updated: Nalog je uspeÅ¡no izmenjen. +notice_account_invalid_creditentials: PogreÅ¡an korisnik ili lozinka +notice_account_password_updated: Lozinka je uspeÅ¡no izmenjena. +notice_account_wrong_password: PogreÅ¡na lozinka +notice_account_register_done: Nalog je uspeÅ¡no kreiran. 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 izmena. +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 izmenjeni od strane drugog korisnika. +notice_not_authorized: Niste ovlašćeni da pristupite ovoj stranici. +notice_email_sent: Email je poslat %s +notice_email_error: DoÅ¡lo je do greÅ¡ke pri slanju maila (%s) +notice_feeds_access_key_reseted: VaÅ¡ RSS pristup je resetovan. +notice_failed_to_save_issues: "NeuspeÅ¡no snimanje %d kartica na %d izabrano: %s." +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: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: VaÅ¡a %s lozinka +mail_body_lost_password: 'Da biste izmenili vaÅ¡u lozinku, kliknite na sledeći link:' +mail_subject_register: Aktivacija %s naloga +mail_body_register: 'Da biste aktivirali vaÅ¡ nalog, kliknite na sledeći link:' +mail_body_account_information_external: Mozete koristiti vas "%s" nalog da bi ste se prikljucili. +mail_body_account_information: Informacije o vasem nalogu + +gui_validation_error: 1 greÅ¡ka +gui_validation_error_plural: %d 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: File +field_filesize: VeliÄina +field_downloads: Downloads +field_author: Autor +field_created_on: Kreirano +field_updated_on: Izmenjeno +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: GreÅ¡ka zatvorena +field_is_default: Podrazumevana vrednost +field_tracker: Tracker +field_subject: Subjekat +field_due_date: Do datuma +field_assigned_to: Dodeljeno +field_priority: Prioritet +field_fixed_version: Target version +field_user: Korisnik +field_role: Uloga +field_homepage: Homepage +field_is_public: Javni +field_parent: Podprojekat od +field_is_in_chlog: Kartice se prikazuju u changelog-u +field_is_in_roadmap: Kartice se prikazuju u roadmap-u +field_login: Login +field_mail_notification: ObaveÅ¡tavanje putem mail-a +field_admin: Administrator +field_last_login_on: Poslednja 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: 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: Start +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: Podprojekat +field_hours: Sati +field_activity: Aktivnost +field_spent_on: Datum +field_identifier: Identifikator +field_is_filter: Korišćen kao filter +field_issue_to_id: 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 obaveyno +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: Autologin +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_user: Korisnik +label_user_plural: Korisnici +label_user_new: Novi korisnik +label_project: Projekat +label_project_new: Novi projekat +label_project_plural: Projekti +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: Dokumenat +label_document_new: Novi dokumenat +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: Tracker +label_tracker_plural: Trackers +label_tracker_new: Novi tracker +label_workflow: Tok rada +label_issue_status: Status kartice +label_issue_status_plural: Statusi kartica +label_issue_status_new: Novi status +label_issue_category: Kategorij 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: Enumeracije +label_enumeration_new: Nova vrednost +label_information: Informacija +label_information_plural: Informacije +label_please_login: Molim ulogujte se +label_register: Registracija +label_password_lost: Izgubljena lozinka +label_home: Home +label_my_page: Moja Stranica +label_my_account: Moj nalog +label_my_projects: Moji projekti +label_administration: Administracija +label_login: Login +label_logout: Logout +label_help: Pomoć +label_reported_issues: Prijavljene kartice +label_assigned_to_me_issues: Kartice meni dodeljene +label_last_login: Poslednja konekcija +label_last_updates: Poslednje izmene +label_last_updates_plural: %d poslednje izmenjene +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: Podprojekti +label_min_max_length: Min - Max velicina +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: %d Download +label_download_plural: %d Downloads +label_no_data: Nema podataka za prikaz +label_change_status: Izmena 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 novosti +label_news_plural: Novosti +label_news_latest: Poslednje novosti +label_news_view_all: Pregled svih novosti +label_change_log: Change log +label_settings: PodeÅ¡avanja +label_overview: Overview +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: Otvoreni +label_closed_issues: Zatvoreni +label_closed_issues_plural: Zatvoreni +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 %d izmena +label_change_view_all: Prikaz svih izmena +label_personalize_page: Personalizuj ovu stranicu +label_comment: Komentar +label_comment_plural: Komentari +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: je manji od +label_in_more_than: je veci od +label_in: u +label_today: danas +label_this_week: ove nedelje +label_less_than_ago: manje nego dana +label_more_than_ago: viÅ¡e nego dana +label_ago: pre dana +label_contains: Sadrži +label_not_contains: ne sadrži +label_day_plural: dana +label_repository: SpremiÅ¡te +label_browse: Pregled +label_modification: %d izmena +label_modification_plural: %d izmena +label_revision: Revizija +label_revision_plural: Revizije +label_added: dodato +label_modified: modifikovano +label_deleted: izmenjeno +label_latest_revision: Poslednja revizija +label_latest_revision_plural: Poslednje revizije +label_view_revisions: Pregled revizija +label_max_size: Maksimalna veliÄina +label_on: 'ukljuÄeno' +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: Roadmap +label_roadmap_due_in: ZavrÅ¡ava se za +label_roadmap_overdue: %s 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 izmena +label_wiki_edit_plural: Wiki izmene +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 izmena +label_issue_tracking: Praćenje kartica +label_spent_time: PotroÅ¡eno vremena +label_f_hour: %.2f Äasa +label_f_hour_plural: %.2f Äasova +label_time_tracking: Praćenje vremena +label_change_plural: Izmene +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: Kartice u vezi +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: Sortiraj po %s +label_send_test_email: PoÅ¡alji probni email +label_feeds_access_key_created_on: RSS kljuÄ za pristup je kreiran pre %s +label_module_plural: Modulovi +label_added_time_by: Dodato pre %s %s +label_updated_time: Izmenjeno pre %s +label_jump_to_a_project: Prebaci se na projekat... +label_file_plural: Fajlovi +label_changeset_plural: Skupovi izmena +label_default_columns: Podrazumevane kolone +label_no_change_option: (Bez izmena) +label_bulk_edit_selected_issues: ZajedniÄka izmena 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: Login +button_submit: PoÅ¡alji +button_save: Snimi +button_check_all: OznaÄi sve +button_uncheck_all: IskljuÄi sve +button_delete: BriÅ¡i +button_create: Kreiraj +button_test: Testiraj +button_edit: Izmene +button_add: Dodavanje +button_change: Izmena +button_apply: Primena +button_clear: Brisanje +button_lock: ZakljuÄavanje +button_unlock: OdkljuÄavanje +button_download: Download +button_list: Lista +button_view: Pregled +button_move: PremeÅ¡tanje +button_back: Nazad +button_cancel: Odustajanje +button_activate: Aktiviraj +button_sort: Sortiranje +button_log_time: Log time +button_rollback: IzvrÅ¡i rollback na ovu verziju +button_watch: Praćenje +button_unwatch: Prekid praćenja +button_reply: Odgovor +button_archive: Arhiviranje +button_unarchive: Dearhiviranje +button_reset: Reset +button_rename: Promena imena +button_change_password: Izmena lozinke + +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_journal_changed: izmenjen iz %s u %s +text_journal_set_to: postavi na %s +text_journal_deleted: izbrisano +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.
Jednom snimljen identifikator se ne može menjati' +text_caracters_maximum: %d karaktera maksimalno. +text_length_between: Dužina izmedu %d i %d 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 %s je prijavljena (by %s). +text_issue_updated: Kartica %s je izmenjena (by %s). +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 (%d) 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 kreirali 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_assigned: Dodeljeno +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: Normalan +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: Copy +setting_protocol: Protocol +label_user_mail_no_self_notified: "Ne želim da budem obaveÅ¡tavan o izmenama koje sam pravim" +setting_time_format: Format vremena +label_registration_activation_by_email: aktivacija naloga putem email-a +mail_subject_account_activation_request: %s zahtev za aktivacijom naloga +mail_body_account_activation_request: 'Novi korisnik (%s) 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 kreiran i Äeka odobrenje administratora." +field_time_zone: Vremenska zona +text_caracters_minimum: Mora biti minimum %d karaktera dugaÄka. +setting_bcc_recipients: '"Blind carbon copy" primaoci (bcc)' +button_annotate: Annotate +label_issues_by: Kartice od %s +field_searchable: Searchable +label_display_per_page: 'Po stranici: %s' +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/sv.yml b/groups/lang/sv.yml new file mode 100644 index 000000000..c0f691230 --- /dev/null +++ b/groups/lang/sv.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Januari,Februari,Mars,April,Maj,Juni,Juli,Augusti,September,Oktober,November,December +actionview_datehelper_select_month_names_abbr: Jan,Feb,Mar,Apr,Maj,Jun,Jul,Aug,Sep,Okt,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 dag +actionview_datehelper_time_in_words_day_plural: %d dagar +actionview_datehelper_time_in_words_hour_about: cirka en timme +actionview_datehelper_time_in_words_hour_about_plural: cirka %d timmar +actionview_datehelper_time_in_words_hour_about_single: cirka en timme +actionview_datehelper_time_in_words_minute: 1 minut +actionview_datehelper_time_in_words_minute_half: en halv minute +actionview_datehelper_time_in_words_minute_less_than: mindre än en minut +actionview_datehelper_time_in_words_minute_plural: %d minuter +actionview_datehelper_time_in_words_minute_single: 1 minut +actionview_datehelper_time_in_words_second_less_than: mindre än en sekund +actionview_datehelper_time_in_words_second_less_than_plural: mindre än %d sekunder +actionview_instancetag_blank_option: Var god välj + +activerecord_error_inclusion: finns inte i listan +activerecord_error_exclusion: är reserverad +activerecord_error_invalid: är ogiltig +activerecord_error_confirmation: överränsstämmer inte med bekräftelsen +activerecord_error_accepted: mÃ¥ste accepteras +activerecord_error_empty: fÃ¥r inte vara tom +activerecord_error_blank: fÃ¥r inte vara tom +activerecord_error_too_long: är för lÃ¥ng +activerecord_error_too_short: är för kort +activerecord_error_wrong_length: har fel längd +activerecord_error_taken: har redan blivit tagen +activerecord_error_not_a_number: är inte ett nummer +activerecord_error_not_a_date: är inte ett korrekt datum +activerecord_error_greater_than_start_date: mÃ¥ste vara senare än startdatumet +activerecord_error_not_same_project: doesn't belong to the same project +activerecord_error_circular_dependency: This relation would create a circular dependency + +general_fmt_age: %d Ã¥r +general_fmt_age_plural: %d Ã¥r +general_fmt_date: %%Y-%%m-%%d +general_fmt_datetime: %%Y-%%m-%%d %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +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_encoding: ISO-8859-1 +general_pdf_encoding: ISO-8859-1 +general_day_names: MÃ¥ndag,Tisdag,Onsdag,Torsdag,Fredag,Lördag,Söndag +general_first_day_of_week: '7' + +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. +notice_account_unknown_email: Okäns användare. +notice_can_t_change_password: Detta konto använder en extern authentikeringskälla. Det gÃ¥r inte att byta lösenord. +notice_account_lost_email_sent: Ett email 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: Lyckat skapande. +notice_successful_update: Lyckad uppdatering. +notice_successful_delete: Lyckad borttagning. +notice_successful_connection: Lyckad uppkoppling. +notice_file_not_found: Sidan du försökte komma Ã¥t existerar inte eller har blivit borttagen. +notice_locking_conflict: Data har uppdaterats av en annan användare. +notice_not_authorized: You are not authorized to access this page. +notice_email_sent: An email was sent to %s +notice_email_error: An error occurred while sending mail (%s) +notice_feeds_access_key_reseted: Your RSS access key was reseted. + +error_scm_not_found: "Inlägg och/eller revision finns inte i repositoriet." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + +mail_subject_lost_password: Ditt %s lösenord +mail_body_lost_password: 'För att ändra lösenord, följ denna länk:' +mail_subject_register: Ditt %s kontoaktivering +mail_body_register: 'För att aktivera ditt konto, använd följande länk.' + +gui_validation_error: 1 fel +gui_validation_error_plural: %d fel + +field_name: Namn +field_description: Beskrivning +field_summary: Sammanfattning +field_is_required: Obligatorisk +field_firstname: Förnamn +field_lastname: Efternamn +field_mail: Email +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: Regular expression +field_min_length: Minimilängd +field_max_length: Maximumlängd +field_value: Värde +field_category: Kategori +field_title: Titel +field_project: Projekt +field_issue: Brist +field_status: Status +field_notes: Anteckningar +field_is_closed: Brist stängd +field_is_default: Defaultstatus +field_tracker: Tracker +field_subject: Rubrik +field_due_date: Färdigdatum +field_assigned_to: Tilldelad +field_priority: Prioritet +field_fixed_version: Target version +field_user: Användare +field_role: Roll +field_homepage: Hemsida +field_is_public: Offentlig +field_parent: Delprojekt av +field_is_in_chlog: Brister visade i ändringslogg +field_is_in_roadmap: Bsiter visade i roadmap +field_login: Inloggning +field_mail_notification: Emailnotifieringar +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 +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örnamnattribut +field_attr_lastname: Efternamnattribut +field_attr_mail: Emailattribut +field_onthefly: On-the-fly användarskapning +field_start_date: Start +field_done_ratio: %% Done +field_auth_source: Authentikeringsläge +field_hide_mail: Dölj min emailadress +field_comment: Kommentar +field_url: URL +field_start_page: Startsida +field_subproject: Delprojekt +field_hours: Timmar +field_activity: Aktivitet +field_spent_on: Datum +field_identifier: Identifierare +field_is_filter: Used as a filter +field_issue_to_id: 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_default_value: Default value + +setting_app_title: Applikationstitel +setting_app_subtitle: Applicationsunderrubrik +setting_welcome_text: Välkommentext +setting_default_language: Default sprÃ¥k +setting_login_required: Authent. obligatoriskt +setting_self_registration: Självregistrering pÃ¥slaget +setting_attachment_max_size: Bifogad maxstorlek +setting_issues_export_limit: Brist exportgräns +setting_mail_from: Emailavsändare +setting_host_name: Värddatornamn +setting_text_formatting: Textformattering +setting_wiki_compression: Wiki historiekomprimering +setting_feeds_limit: Feed innehÃ¥llsgräns +setting_autofetch_changesets: Automatisk hämtning av commits +setting_sys_api_enabled: Aktivera WS för repository management +setting_commit_ref_keywords: Referencing keywords +setting_commit_fix_keywords: Fixing keywords +setting_autologin: Autologin +setting_date_format: Date format +setting_cross_project_issue_relations: Allow cross-project issue relations + +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_project_all: All Projects +label_project_latest: Senaste projekt +label_issue: Brist +label_issue_new: Ny brist +label_issue_plural: Brister +label_issue_view_all: Visa alla brister +label_document: Dokument +label_document_new: Nytt dokument +label_document_plural: Dokument +label_role: Roll +label_role_plural: Roller +label_role_new: Ny roll +label_role_and_permissions: Roller och rättigheter +label_member: Medlem +label_member_new: Ny medlem +label_member_plural: Medlemmar +label_tracker: Tracker +label_tracker_plural: Trackers +label_tracker_new: Ny tracker +label_workflow: Workflow +label_issue_status: Briststatus +label_issue_status_plural: Briststatusar +label_issue_status_new: Ny status +label_issue_category: Bristkategori +label_issue_category_plural: Bristkategorier +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_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 brister +label_assigned_to_me_issues: Brister tilldelade mig +label_last_login: Senaste inloggning +label_last_updates: Senast uppdaterad +label_last_updates_plural: %d senaste uppdateringarna +label_registered_on: Registrerad +label_activity: Aktivitet +label_new: Ny +label_logged_as: Loggad som +label_environment: Miljö +label_authentication: Authentikering +label_auth_source: Authentikeringsläge +label_auth_source_new: Nytt authentikeringsläge +label_auth_source_plural: Authentikeringslägen +label_subproject_plural: Delprojekt +label_min_max_length: Min - Max längd +label_list: Lista +label_date: Datum +label_integer: Heltal +label_boolean: Boolean +label_string: Text +label_text: Long text +label_attribute: Attribut +label_attribute_plural: Attribut +label_download: %d Nerladdning +label_download_plural: %d 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_report: Rapport +label_report_plural: Rapporter +label_news: Nyhet +label_news_new: Lägg till nyhet +label_news_plural: Nyheter +label_news_latest: Senaste neheten +label_news_view_all: Visa alla nyheter +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: Offentligt projekt +label_open_issues: öppen +label_open_issues_plural: öppna +label_closed_issues: stängd +label_closed_issues_plural: stängda +label_total: Total +label_permissions: Rättigheter +label_current_status: Nuvarande status +label_new_statuses_allowed: Nya statusar tillÃ¥tna +label_all: alla +label_none: inga +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 %d ändringar +label_change_view_all: Visa alla ändringar +label_personalize_page: Anpassa denna sida +label_comment: Kommentar +label_comment_plural: 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: i mindre än +label_in_more_than: i mer än +label_in: i +label_today: idag +label_this_week: this week +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: Repositorie +label_browse: Bläddra +label_modification: %d ändring +label_modification_plural: %d ändringar +label_revision: Revision +label_revision_plural: Revisioner +label_added: tillagd +label_modified: modifierad +label_deleted: borttagen +label_latest_revision: Senaste revisionen +label_latest_revision_plural: Senaste revisionerna +label_view_revisions: Visa revisioner +label_max_size: Maximumstorlek +label_on: 'pÃ¥' +label_sort_highest: Flytta till top +label_sort_higher: Flytta up +label_sort_lower: Flytta ner +label_sort_lowest: Flytta till botten +label_roadmap: Roadmap +label_roadmap_due_in: Färdig om +label_roadmap_overdue: %s late +label_roadmap_no_issues: Inga brister för denna version +label_search: Sök +label_result_plural: Resultat +label_all_words: Alla ord +label_wiki: Wiki +label_wiki_edit: Wiki editera +label_wiki_edit_plural: Wiki editeringar +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: Nuvarande version +label_preview: Preview +label_feed_plural: Feeder +label_changes_details: Detaljer om alla ändringar +label_issue_tracking: BristspÃ¥rning +label_spent_time: Spenderad tid +label_f_hour: %.2f timmar +label_f_hour_plural: %.2f timmar +label_time_tracking: TidsspÃ¥rning +label_change_plural: Ändringar +label_statistics: Statistik +label_commits_per_month: Commit per mÃ¥nad +label_commits_per_author: Commit per författare +label_view_diff: Visa skillnader +label_diff_inline: inline +label_diff_side_by_side: sida vid sida +label_options: Inställningar +label_copy_workflow_from: Kopiera workflow frÃ¥n +label_permissions_report: Rättighetsrapport +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_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_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: Language based +label_sort_by: Sort by %s +label_send_test_email: Send a test email +label_feeds_access_key_created_on: RSS access key created %s ago +label_module_plural: Modules +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s ago +label_jump_to_a_project: Jump to a project... + +button_login: Logga in +button_submit: Skicka +button_save: Spara +button_check_all: Markera alla +button_uncheck_all: Avmarkera alla +button_delete: Ta bort +button_create: Skapa +button_test: Testa +button_edit: Editera +button_add: Lägg till +button_change: Ändra +button_apply: Värkställ +button_clear: Rensa +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: Rulla tillbaka till denna version +button_watch: Watch +button_unwatch: Unwatch +button_reply: Reply +button_archive: Archive +button_unarchive: Unarchive +button_reset: Reset +button_rename: Rename + +status_active: activ +status_registered: registrerad +status_locked: lÃ¥st + +text_select_mail_notifications: Väl action för vilka email 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_workflow_edit: Väl en roll och en tracker för att editera workflow. +text_are_you_sure: Är du säker? +text_journal_changed: ändrad frÃ¥n %s till %s +text_journal_set_to: satt till %s +text_journal_deleted: borttagen +text_tip_task_begin_day: arbetsuppgift börjar denna dag +text_tip_task_end_day: arbetsuppgift 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.
När den är sparad kan identifieraren inte ändras.' +text_caracters_maximum: %d tecken maximum. +text_length_between: Längd mellan %d och %d tecken. +text_tracker_no_workflow: Inget workflow definerat för denna 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: Brist %s har rapporterats (by %s). +text_issue_updated: Brist %s har uppdaterats (by %s). +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassing issues to this category + +default_role_manager: Förvaltare +default_role_developper: Utvecklare +default_role_reporter: Rapporterare +default_tracker_bug: Bugg +default_tracker_feature: Finess +default_tracker_support: Support +default_issue_status_new: Ny +default_issue_status_assigned: Tilldelad +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Ã¥ttom +default_priority_immediate: Omedelbar +default_activity_design: Design +default_activity_development: Utveckling + +enumeration_issue_priorities: Bristprioriteringar +enumeration_doc_categories: Dokumentkategorier +enumeration_activities: Aktiviteter (tidsspÃ¥rning) +field_comments: Comment +label_file_plural: Files +label_changeset_plural: Changesets +field_column_names: Columns +label_default_columns: Default columns +setting_issue_list_default_columns: Default columns displayed on the issue list +setting_repositories_encodings: Repositories encodings +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +label_bulk_edit_selected_issues: Bulk edit selected issues +label_no_change_option: (No change) +notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +label_theme: Theme +label_default: Default +label_search_titles_only: Search titles only +label_nobody: nobody +button_change_password: Change password +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)." +label_user_mail_option_selected: "For any event on the selected projects only..." +label_user_mail_option_all: "For any event on all my projects" +label_user_mail_option_none: "Only for things I watch or I'm involved in" +setting_emails_footer: Emails footer +label_float: Float +button_copy: Copy +mail_body_account_information_external: You can use your "%s" account to log in. +mail_body_account_information: Your account information +setting_protocol: Protocol +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +setting_time_format: Time format +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: %s account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' +label_registration_automatic_activation: automatic account activation +label_registration_manual_activation: manual account activation +notice_account_pending: "Your account was created and is now pending administrator approval." +field_time_zone: Time zone +text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate +label_issues_by: Issues by %s +field_searchable: Searchable +label_display_per_page: 'Per page: %s' +setting_per_page_options: Objects per page options +label_age: Age +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: %s" +button_update: Update +label_change_properties: Change properties +label_general: General +label_repository_plural: Repositories +label_associated_revisions: Associated revisions +setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. +label_more: More +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: Issue added +label_issue_updated: Issue updated +label_document_added: Document added +label_message_posted: Message added +label_file_added: File added +label_news_added: News added +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/uk.yml b/groups/lang/uk.yml new file mode 100644 index 000000000..a52a05603 --- /dev/null +++ b/groups/lang/uk.yml @@ -0,0 +1,622 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Січень,Лютий,Березень,Квітень,Травень,Червень,Липень,Серпень,ВереÑень,Жовтень,ЛиÑтопад,Грудень +actionview_datehelper_select_month_names_abbr: Січ,Лют,Бер,Квіт,Трав,Чер,Лип,Серп,Вер,Жов,ЛиÑ,Груд +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 день +actionview_datehelper_time_in_words_day_plural: %d днів(Ñ) +actionview_datehelper_time_in_words_hour_about: близько години +actionview_datehelper_time_in_words_hour_about_plural: близько %d годин(и) +actionview_datehelper_time_in_words_hour_about_single: близько години +actionview_datehelper_time_in_words_minute: 1 хвилина +actionview_datehelper_time_in_words_minute_half: півхвилини +actionview_datehelper_time_in_words_minute_less_than: менше хвилини +actionview_datehelper_time_in_words_minute_plural: %d хвилин(и) +actionview_datehelper_time_in_words_minute_single: 1 хвилина +actionview_datehelper_time_in_words_second_less_than: менше Ñекунди +actionview_datehelper_time_in_words_second_less_than_plural: менше %d Ñекунд(и) +actionview_instancetag_blank_option: Оберіть + +activerecord_error_inclusion: немає в ÑпиÑку +activerecord_error_exclusion: зарезервовано +activerecord_error_invalid: невірне +activerecord_error_confirmation: не збігаєтьÑÑ Ð· підтвердженнÑм +activerecord_error_accepted: необхідно прийнÑти +activerecord_error_empty: не може бути порожнім +activerecord_error_blank: не може бути незаповненим +activerecord_error_too_long: дуже довге +activerecord_error_too_short: дуже коротке +activerecord_error_wrong_length: не відповідає довжині +activerecord_error_taken: вже викориÑтовуєтьÑÑ +activerecord_error_not_a_number: не Ñ” чиÑлом +activerecord_error_not_a_date: Ñ” недійÑною датою +activerecord_error_greater_than_start_date: повинна бути пізніша за дату початку +activerecord_error_not_same_project: не відноÑÑтьÑÑ Ð´Ð¾ одного проекту +activerecord_error_circular_dependency: Такий зв'Ñзок приведе до циклічної залежноÑті + +general_fmt_age: %d Ñ€. +general_fmt_age_plural: %d рр. +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'ÐÑ–' +general_text_Yes: 'Так' +general_text_no: 'ÐÑ–' +general_text_yes: 'Так' +general_lang_name: 'Ukrainian (УкраїнÑька)' +general_csv_separator: ',' +general_csv_encoding: UTF-8 +general_pdf_encoding: UTF-8 +general_day_names: Понеділок,Вівторок,Середа,Четвер,П'ÑтницÑ,Субота,ÐÐµÐ´Ñ–Ð»Ñ +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: Відправлено лиÑта %s +notice_email_error: Під Ñ‡Ð°Ñ Ð²Ñ–Ð´Ð¿Ñ€Ð°Ð²ÐºÐ¸ лиÑта відбулаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° (%s) +notice_feeds_access_key_reseted: Ваш ключ доÑтупу RSS було Ñкинуто. +notice_failed_to_save_issues: "Ðе вдалоÑÑ Ð·Ð±ÐµÑ€ÐµÐ³Ñ‚Ð¸ %d пункт(ів) з %d вибраних: %s." +notice_no_issue_selected: "Ðе вибрано жодної задачі! Будь лаÑка, відзначте задачу, Ñку ви хочете відредагувати." +notice_account_pending: "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ñтворено Ñ– він чекає на Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð°Ð´Ð¼Ñ–Ð½Ñ–Ñтратором." + +mail_subject_lost_password: Ваш %s пароль +mail_body_lost_password: 'Ð”Ð»Ñ Ð·Ð¼Ñ–Ð½Ð¸ паролÑ, зайдіть за наÑтупним поÑиланнÑм:' +mail_subject_register: ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу %s +mail_body_register: 'Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— облікового запиÑу, зайдіть за наÑтупним поÑиланнÑм:' +mail_body_account_information_external: Ви можете викориÑтовувати ваш "%s" обліковий Ð·Ð°Ð¿Ð¸Ñ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ. +mail_body_account_information: Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ð¾ Вашому обліковому запиÑу +mail_subject_account_activation_request: Запит на активацію облікового запиÑу %s +mail_body_account_activation_request: 'Ðовий кориÑтувач (%s) зареєÑтрувавÑÑ. Його обліковий Ð·Ð°Ð¿Ð¸Ñ Ñ‡ÐµÐºÐ°Ñ” на ваше підтвердженнÑ:' + +gui_validation_error: 1 помилка +gui_validation_error_plural: %d помилки(ок) + +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_id: Зв'Ñзані задачі +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_project_all: УÑÑ– проекти +label_project_latest: ОÑтанні проекти +label_issue: ÐŸÐ¸Ñ‚Ð°Ð½Ð½Ñ +label_issue_new: Ðові Ð¿Ð¸Ñ‚Ð°Ð½Ð½Ñ +label_issue_plural: ÐŸÐ¸Ñ‚Ð°Ð½Ð½Ñ +label_issue_view_all: ПроглÑнути вÑÑ– Ð¿Ð¸Ñ‚Ð°Ð½Ð½Ñ +label_issues_by: ÐŸÐ¸Ñ‚Ð°Ð½Ð½Ñ Ð·Ð° %s +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_last_updates: ОÑтаннє Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ +label_last_updates_plural: %d оÑтанні Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ +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: %d Завантажено +label_download_plural: %d Завантажень +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_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: оÑтанні %d змін +label_change_view_all: ПроглÑнути вÑÑ– зміни +label_personalize_page: ПерÑоналізувати цю Ñторінку +label_comment: Коментувати +label_comment_plural: Коментарі +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: %d зміна +label_modification_plural: %d змін +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_on: 'з' +label_sort_highest: У початок +label_sort_higher: Вгору +label_sort_lower: Вниз +label_sort_lowest: У кінець +label_roadmap: Оперативний план +label_roadmap_due_in: Строк +label_roadmap_overdue: %s Ð·Ð°Ð¿Ñ–Ð·Ð½ÐµÐ½Ð½Ñ +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: %.2f година +label_f_hour_plural: %.2f годин(и) +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: Сортувати за %s +label_send_test_email: ПоÑлати email Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ +label_feeds_access_key_created_on: Ключ доÑтупу RSS Ñтворений %s назад +label_module_plural: Модулі +label_added_time_by: Доданий %s %s назад +label_updated_time: Оновлений %s назад +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_journal_changed: змінивÑÑ Ð· %s на %s +text_journal_set_to: параметр змінивÑÑ Ð½Ð° %s +text_journal_deleted: видалено +text_tip_task_begin_day: день початку задачі +text_tip_task_end_day: день Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡Ñ– +text_tip_task_begin_end_day: початок задачі Ñ– Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ð´Ð½Ñ +text_project_identifier_info: 'РÑдкові букви (a-z), допуÑтимі цифри Ñ– дефіÑ.
Збережений ідентифікатор не може бути змінений.' +text_caracters_maximum: %d Ñимволів(а) макÑимум. +text_caracters_minimum: Повинно мати Ñкнайменше %d Ñимволів(а) у довжину. +text_length_between: Довжина між %d Ñ– %d Ñимволів. +text_tracker_no_workflow: Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ координатора поÑлідовніÑть дій не визначена +text_unallowed_characters: Заборонені Ñимволи +text_comma_separated: ДопуÑтимі декілька значень (розділені комою). +text_issues_ref_in_commit_messages: ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ñ‚Ð° зміна питань у повідомленнÑÑ… до подавань +text_issue_added: ÐŸÐ¸Ñ‚Ð°Ð½Ð½Ñ %s Ñтворено. +text_issue_updated: ÐŸÐ¸Ñ‚Ð°Ð½Ð½Ñ %s оновлено. +text_wiki_destroy_confirmation: Ви впевнені, що хочете видалити цю wiki Ñ– веÑÑŒ зміÑÑ‚? +text_issue_category_destroy_question: Декілька питань (%d) призначено в цю категорію. Що ви хочете зробити? +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_assigned: Призначено +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 %s. +label_display_per_page: 'Per page: %s' +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: %s" +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: %s" +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 %d 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: %.02f 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): %s will be also deleted.' diff --git a/groups/lang/zh-tw.yml b/groups/lang/zh-tw.yml new file mode 100644 index 000000000..a0c7fafb3 --- /dev/null +++ b/groups/lang/zh-tw.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: 一月,二月,三月,四月,五月,六月,七月,八月,乿œˆ,åæœˆ,å一月,å二月 +actionview_datehelper_select_month_names_abbr: 一月,二月,三月,四月,五月,六月,七月,八月,乿œˆ,åæœˆ,å一月,å二月 +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 天 +actionview_datehelper_time_in_words_day_plural: %d 天 +actionview_datehelper_time_in_words_hour_about: ç´„ 1 å°æ™‚ +actionview_datehelper_time_in_words_hour_about_plural: ç´„ %d å°æ™‚ +actionview_datehelper_time_in_words_hour_about_single: ç´„ 1 å°æ™‚ +actionview_datehelper_time_in_words_minute: 1 åˆ†é˜ +actionview_datehelper_time_in_words_minute_half: åŠåˆ†é˜ +actionview_datehelper_time_in_words_minute_less_than: å°æ–¼ 1 åˆ†é˜ +actionview_datehelper_time_in_words_minute_plural: %d åˆ†é˜ +actionview_datehelper_time_in_words_minute_single: 1 åˆ†é˜ +actionview_datehelper_time_in_words_second_less_than: å°æ–¼ 1 ç§’ +actionview_datehelper_time_in_words_second_less_than_plural: å°æ–¼ %d ç§’ +actionview_instancetag_blank_option: è«‹é¸æ“‡ + +activerecord_error_inclusion: å¿…é ˆè¢«åŒ…å« +activerecord_error_exclusion: 必須被排除 +activerecord_error_invalid: 䏿­£ç¢º +activerecord_error_confirmation: èˆ‡ç¢ºèªæ¬„ä½ä¸ç›¸ç¬¦ +activerecord_error_accepted: å¿…é ˆè¢«æŽ¥å— +activerecord_error_empty: ä¸å¯ç‚ºç©ºå€¼ +activerecord_error_blank: ä¸å¯ç‚ºç©ºç™½ +activerecord_error_too_long: 長度éŽé•· +activerecord_error_too_short: 長度太短 +activerecord_error_wrong_length: é•·åº¦ä¸æ­£ç¢º +activerecord_error_taken: 已經被使用 +activerecord_error_not_a_number: 䏿˜¯ä¸€å€‹æ•¸å­— +activerecord_error_not_a_date: 日期格å¼ä¸æ­£ç¢º +activerecord_error_greater_than_start_date: 必須在起始日期之後 +activerecord_error_not_same_project: ä¸å±¬æ–¼åŒä¸€å€‹å°ˆæ¡ˆ +activerecord_error_circular_dependency: é€™å€‹é—œè¯æœƒå°Žè‡´ç’°ç‹€ç›¸ä¾ + +general_fmt_age: %d å¹´ +general_fmt_age_plural: %d å¹´ +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'å¦' +general_text_Yes: '是' +general_text_no: 'å¦' +general_text_yes: '是' +general_lang_name: 'Traditional Chinese (ç¹é«”中文)' +general_csv_separator: ',' +general_csv_encoding: Big5 +general_pdf_encoding: Big5 +general_day_names: 星期一,星期二,星期三,星期四,星期五,星期六,星期日 +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: 郵件已經æˆåŠŸå¯„é€è‡³ä»¥ä¸‹æ”¶ä»¶è€…: %s +notice_email_error: 寄é€éƒµä»¶çš„éŽç¨‹ä¸­ç™¼ç”ŸéŒ¯èª¤ (%s) +notice_feeds_access_key_reseted: 您的 RSS å­˜å–éµå·²è¢«é‡æ–°è¨­å®šã€‚ +notice_failed_to_save_issues: " %d 個項目儲存失敗 (總共é¸å– %d 個項目): %s." +notice_no_issue_selected: "æœªé¸æ“‡ä»»ä½•é …ç›®ï¼è«‹å‹¾é¸æ‚¨æƒ³è¦ç·¨è¼¯çš„項目。" +notice_account_pending: "您的帳號已經建立,正在等待管ç†å“¡çš„審核。" +notice_default_data_loaded: é è¨­çµ„態已載入æˆåŠŸã€‚ + +error_can_t_load_default_data: "無法載入é è¨­çµ„態: %s" +error_scm_not_found: SCM 儲存庫中找ä¸åˆ°é€™å€‹å°ˆæ¡ˆæˆ–版本。 +error_scm_command_failed: "嘗試存å–儲存庫時發生錯誤: %s" +error_scm_annotate: "SCM 儲存庫中無此項目或此項目無法被加註。" +error_issue_not_found_in_project: '該項目ä¸å­˜åœ¨æˆ–ä¸å±¬æ–¼æ­¤å°ˆæ¡ˆ' + +mail_subject_lost_password: 您的 Redmine 網站密碼 +mail_body_lost_password: '欲變更您的 Redmine 網站密碼, 請點é¸ä»¥ä¸‹éˆçµ:' +mail_subject_register: 啟用您的 Redmine 帳號 +mail_body_register: '欲啟用您的 Redmine 帳號, 請點é¸ä»¥ä¸‹éˆçµ:' +mail_body_account_information_external: 您å¯ä»¥ä½¿ç”¨ "%s" 帳號登入 Redmine 網站。 +mail_body_account_information: 您的 Redmine 帳號資訊 +mail_subject_account_activation_request: Redmine 帳號啟用需求通知 +mail_body_account_activation_request: 'æœ‰ä½æ–°ç”¨æˆ¶ (%s) 已經完æˆè¨»å†Šï¼Œæ­£ç­‰å€™æ‚¨çš„審核:' + +gui_validation_error: 1 個錯誤 +gui_validation_error_plural: %d 個錯誤 + +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: 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: Lastname attribute +field_attr_mail: Email attribute +field_onthefly: On-the-fly user creation +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: Used as a filter +field_issue_to_id: Related issue +field_delay: 逾期 +field_assignable: é …ç›®å¯è¢«åˆ†æ´¾è‡³æ­¤è§’色 +field_redirect_existing_links: Redirect existing links +field_estimated_hours: é ä¼°å·¥æ™‚ +field_column_names: Columns +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 content limit +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_emails_footer: é›»å­éƒµä»¶é™„帶說明 +setting_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_project_all: 全部的專案 +label_project_latest: 最近的專案 +label_issue: é …ç›® +label_issue_new: 建立新的項目 +label_issue_plural: 項目清單 +label_issue_view_all: 檢視所有項目 +label_issues_by: 項目按 %s 分組顯示 +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_last_updates: 最近更新 +label_last_updates_plural: %d 個最近更新 +label_registered_on: 註冊於 +label_activity: 活動 +label_overall_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: %d 個下載 +label_download_plural: %d 個下載 +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: Read... +label_public_projects: 公開專案 +label_open_issues: 進行中 +label_open_issues_plural: 進行中 +label_closed_issues: å·²çµæŸ +label_closed_issues_plural: å·²çµæŸ +label_total: 總計 +label_permissions: æ¬Šé™ +label_current_status: ç›®å‰ç‹€æ…‹ +label_new_statuses_allowed: å¯è®Šæ›´è‡³ä»¥ä¸‹ç‹€æ…‹ +label_all: 全部 +label_none: 空值 +label_nobody: 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: Internal +label_last_changes: 最近 %d 個變更 +label_change_view_all: 檢視所有變更 +label_personalize_page: è‡ªè¨‚ç‰ˆé¢ +label_comment: 註解 +label_comment_plural: 註解 +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: all time +label_yesterday: 昨天 +label_this_week: 本週 +label_last_week: 上週 +label_last_n_days: éŽåŽ» %d 天 +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: %d 變更 +label_modification_plural: %d 變更 +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_on: 總共 +label_sort_highest: 移動至開頭 +label_sort_higher: 往上移動 +label_sort_lower: 往下移動 +label_sort_lowest: 移動至çµå°¾ +label_roadmap: 版本è—圖 +label_roadmap_due_in: 倒數天數: +label_roadmap_overdue: %s 逾期 +label_roadmap_no_issues: 此版本尚未包å«ä»»ä½•é …ç›® +label_search: æœå°‹ +label_result_plural: çµæžœ +label_all_words: 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: %.2f å°æ™‚ +label_f_hour_plural: %.2f å°æ™‚ +label_time_tracking: 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_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: 按 %s æŽ’åº +label_send_test_email: 坄逿¸¬è©¦éƒµä»¶ +label_feeds_access_key_created_on: RSS å­˜å–éµå»ºç«‹æ–¼ %s ä¹‹å‰ +label_module_plural: 模組 +label_added_time_by: 是由 %s æ–¼ %s å‰åŠ å…¥ +label_updated_time: æ–¼ %s 剿›´æ–° +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: 'æ¯é é¡¯ç¤º: %s 個' +label_age: 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: 計劃表 + +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: List +button_view: 檢視 +button_move: 移動 +button_back: 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: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 代表「ä¸é™åˆ¶ã€ +text_project_destroy_confirmation: 您確定è¦åˆªé™¤é€™å€‹å°ˆæ¡ˆå’Œå…¶ä»–相關資料? +text_workflow_edit: 鏿“‡è§’色與追蹤標籤以設定其工作æµç¨‹ +text_are_you_sure: 確定執行? +text_journal_changed: 從 %s 變更為 %s +text_journal_set_to: 設定為 %s +text_journal_deleted: 已刪除 +text_tip_task_begin_day: 今天起始的工作 +text_tip_task_end_day: 今天截止的的工作 +text_tip_task_begin_end_day: 今天起始與截止的工作 +text_project_identifier_info: 'åªå…許å°å¯«è‹±æ–‡å­—æ¯ï¼ˆa-z)ã€é˜¿æ‹‰ä¼¯æ•¸å­—與連字符號(-)。
儲存後,代碼ä¸å¯å†è¢«æ›´æ”¹ã€‚' +text_caracters_maximum: 最多 %d 個字元. +text_caracters_minimum: 長度必須大於 %d 個字元. +text_length_between: 長度必須介於 %d 至 %d 個字元之間. +text_tracker_no_workflow: 此追蹤標籤尚未定義工作æµç¨‹ +text_unallowed_characters: ä¸å…許的字元 +text_comma_separated: å¯è¼¸å…¥å¤šå€‹å€¼ (以逗號分隔). +text_issues_ref_in_commit_messages: é€äº¤è¨Šæ¯ä¸­åƒç…§(或修正)項目之關éµå­— +text_issue_added: é …ç›® %s 已被 %s 通報。 +text_issue_updated: é …ç›® %s 已被 %s 更新。 +text_wiki_destroy_confirmation: 您確定è¦åˆªé™¤é€™å€‹ wiki 和其中的所有內容? +text_issue_category_destroy_question: 有 (%d) 個項目被指派到此分類. è«‹é¸æ“‡æ‚¨æƒ³è¦çš„動作? +text_issue_category_destroy_assignments: 移除這些項目的分類 +text_issue_category_reassign_to: 釿–°æŒ‡æ´¾é€™äº›é …目至其它分類 +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: 載入é è¨­çµ„æ…‹ +text_status_changed_by_changeset: 已套用至變更集 %s. +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: 您å³å°‡åˆªé™¤çš„項目已報工 %.02f å°æ™‚. æ‚¨çš„é¸æ“‡æ˜¯ï¼Ÿ +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_assigned: 已指派 +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_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lang/zh.yml b/groups/lang/zh.yml new file mode 100644 index 000000000..12fb8cb3e --- /dev/null +++ b/groups/lang/zh.yml @@ -0,0 +1,621 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: 一月,二月,三月,四月,五月,六月,七月,八月,乿œˆ,åæœˆ,å一月,å二月 +actionview_datehelper_select_month_names_abbr: 一,二,三,å››,五,å…­,七,å…«,ä¹,å,å一,å二 +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 天 +actionview_datehelper_time_in_words_day_plural: %d 天 +actionview_datehelper_time_in_words_hour_about: 约 1 å°æ—¶ +actionview_datehelper_time_in_words_hour_about_plural: 约 %d å°æ—¶ +actionview_datehelper_time_in_words_hour_about_single: 约 1 å°æ—¶ +actionview_datehelper_time_in_words_minute: 1 分钟 +actionview_datehelper_time_in_words_minute_half: åŠåˆ†é’Ÿ +actionview_datehelper_time_in_words_minute_less_than: 1 分钟以内 +actionview_datehelper_time_in_words_minute_plural: %d 分钟 +actionview_datehelper_time_in_words_minute_single: 1 分钟 +actionview_datehelper_time_in_words_second_less_than: 1 秒以内 +actionview_datehelper_time_in_words_second_less_than_plural: %d 秒以内 +actionview_instancetag_blank_option: 请选择 + +activerecord_error_inclusion: 未被包å«åœ¨åˆ—表中 +activerecord_error_exclusion: 是ä¿ç•™å­— +activerecord_error_invalid: 是无效的 +activerecord_error_confirmation: 与确认æ ä¸ç¬¦ +activerecord_error_accepted: å¿…é¡»è¢«æŽ¥å— +activerecord_error_empty: ä¸å¯ä¸ºç©º +activerecord_error_blank: ä¸å¯ä¸ºç©ºç™½ +activerecord_error_too_long: 过长 +activerecord_error_too_short: 过短 +activerecord_error_wrong_length: é•¿åº¦ä¸æ­£ç¡® +activerecord_error_taken: 已被使用 +activerecord_error_not_a_number: 䏿˜¯æ•°å­— +activerecord_error_not_a_date: 䏿˜¯æœ‰æ•ˆçš„æ—¥æœŸ +activerecord_error_greater_than_start_date: å¿…é¡»åœ¨èµ·å§‹æ—¥æœŸä¹‹åŽ +activerecord_error_not_same_project: ä¸å±žäºŽåŒä¸€ä¸ªé¡¹ç›® +activerecord_error_circular_dependency: 此关è”将导致循环ä¾èµ– + +general_fmt_age: %d å¹´ +general_fmt_age_plural: %d å¹´ +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'å¦' +general_text_Yes: '是' +general_text_no: 'å¦' +general_text_yes: '是' +general_lang_name: 'Simplified Chinese (简体中文)' +general_csv_separator: ',' +general_csv_encoding: gb2312 +general_pdf_encoding: gb2312 +general_day_names: 星期一,星期二,星期三,星期四,星期五,星期六,星期日 +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: 邮件已æˆåŠŸå‘é€åˆ° %s +notice_email_error: å‘é€é‚®ä»¶æ—¶å‘生错误 (%s) +notice_feeds_access_key_reseted: 您的RSSå­˜å–键已被é‡ç½®ã€‚ +notice_failed_to_save_issues: "%d 个问题ä¿å­˜å¤±è´¥ï¼ˆå…±é€‰æ‹© %d 个问题):%s." +notice_no_issue_selected: "未选择任何问题ï¼è¯·é€‰æ‹©æ‚¨è¦ç¼–辑的问题。" +notice_account_pending: "您的å¸å·å·²è¢«æˆåŠŸåˆ›å»ºï¼Œæ­£åœ¨ç­‰å¾…ç®¡ç†å‘˜çš„审核。" +notice_default_data_loaded: æˆåŠŸè½½å…¥é»˜è®¤è®¾ç½®ã€‚ + +error_can_t_load_default_data: "无法载入默认设置:%s" +error_scm_not_found: "版本库中ä¸å­˜åœ¨è¯¥æ¡ç›®å’Œï¼ˆæˆ–)其修订版本。" +error_scm_command_failed: "访问版本库时å‘生错误:%s" +error_scm_annotate: "该æ¡ç›®ä¸å­˜åœ¨æˆ–无法追溯。" +error_issue_not_found_in_project: '问题ä¸å­˜åœ¨æˆ–ä¸å±žäºŽæ­¤é¡¹ç›®' + +mail_subject_lost_password: 您的 %s å¯†ç  +mail_body_lost_password: '请点击以下链接æ¥ä¿®æ”¹æ‚¨çš„密ç ï¼š' +mail_subject_register: %så¸å·æ¿€æ´» +mail_body_register: 'è¯·ç‚¹å‡»ä»¥ä¸‹é“¾æŽ¥æ¥æ¿€æ´»æ‚¨çš„å¸å·ï¼š' +mail_body_account_information_external: 您å¯ä»¥ä½¿ç”¨æ‚¨çš„ "%s" å¸å·æ¥ç™»å½•。 +mail_body_account_information: 您的å¸å·ä¿¡æ¯ +mail_subject_account_activation_request: %så¸å·æ¿€æ´»è¯·æ±‚ +mail_body_account_activation_request: '新用户(%sï¼‰å·²å®Œæˆæ³¨å†Œï¼Œæ­£åœ¨ç­‰å€™æ‚¨çš„审核:' + +gui_validation_error: 1 个错误 +gui_validation_error_plural: %d 个错误 + +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_id: 相关问题 +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: 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_emails_footer: 邮件签å +setting_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_project_all: 所有的项目 +label_project_latest: 最近更新的项目 +label_issue: 问题 +label_issue_new: 新建问题 +label_issue_plural: 问题 +label_issue_view_all: 查看所有问题 +label_issues_by: 按 %s 分组显示问题 +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_last_updates: æœ€åŽæ›´æ–° +label_last_updates_plural: %d æœ€åŽæ›´æ–° +label_registered_on: 注册于 +label_activity: 活动 +label_overall_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: %d 次下载 +label_download_plural: %d 次下载 +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_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: 最近的 %d æ¬¡å˜æ›´ +label_change_view_all: æŸ¥çœ‹æ‰€æœ‰å˜æ›´ +label_personalize_page: 个性化定制本页 +label_comment: 评论 +label_comment_plural: 评论 +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: æœ€åŽ %d 天 +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: %d 个更新 +label_modification_plural: %d 个更新 +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_on: 'on' +label_sort_highest: 置顶 +label_sort_higher: 上移 +label_sort_lower: 下移 +label_sort_lowest: 置底 +label_roadmap: 路线图 +label_roadmap_due_in: 截止日期到 +label_roadmap_overdue: %s 延期 +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: %.2f å°æ—¶ +label_f_hour_plural: %.2f å°æ—¶ +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_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: æ ¹æ® %s æŽ’åº +label_send_test_email: å‘逿µ‹è¯•邮件 +label_feeds_access_key_created_on: RSS å­˜å–键是在 %s 之å‰å»ºç«‹çš„ +label_module_plural: æ¨¡å— +label_added_time_by: ç”± %s 在 %s 之剿·»åŠ  +label_updated_time: 更新于 %s å‰ +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: 'æ¯é¡µæ˜¾ç¤ºï¼š%s' +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: 计划 + +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: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 表示没有é™åˆ¶ +text_project_destroy_confirmation: 您确信è¦åˆ é™¤è¿™ä¸ªé¡¹ç›®ä»¥åŠæ‰€æœ‰ç›¸å…³çš„æ•°æ®å—? +text_workflow_edit: 选择角色和跟踪标签æ¥ç¼–辑工作æµç¨‹ +text_are_you_sure: 您确定? +text_journal_changed: 从 %s å˜æ›´ä¸º %s +text_journal_set_to: 设置为 %s +text_journal_deleted: 已删除 +text_tip_task_begin_day: 今天开始的任务 +text_tip_task_end_day: 今天结æŸçš„任务 +text_tip_task_begin_end_day: 今天开始并结æŸçš„任务 +text_project_identifier_info: 'åªå…许使用å°å†™å­—æ¯ï¼ˆa-z),数字和连字符(-)。
请注æ„,标识符ä¿å­˜åŽå°†ä¸å¯ä¿®æ”¹ã€‚' +text_caracters_maximum: 最多 %d 个字符。 +text_caracters_minimum: è‡³å°‘éœ€è¦ %d 个字符。 +text_length_between: 长度必须在 %d 到 %d 个字符之间。 +text_tracker_no_workflow: 此跟踪标签未定义工作æµç¨‹ +text_unallowed_characters: éžæ³•字符 +text_comma_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆç”¨é€—å·,分开)。 +text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸­å¼•用和修订问题 +text_issue_added: 问题 %s 已由 %s æäº¤ã€‚ +text_issue_updated: 问题 %s 已由 %s 更新。 +text_wiki_destroy_confirmation: 您确定è¦åˆ é™¤è¿™ä¸ª wiki åŠå…¶æ‰€æœ‰å†…容å—? +text_issue_category_destroy_question: 有一些问题(%d ä¸ªï¼‰å±žäºŽæ­¤ç±»åˆ«ã€‚æ‚¨æƒ³è¿›è¡Œå“ªç§æ“作? +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: å·²åº”ç”¨åˆ°å˜æ›´åˆ—表 %s. +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: 您è¦åˆ é™¤çš„问题已ç»ä¸ŠæŠ¥äº† %.02f å°æ—¶çš„工作é‡ã€‚æ‚¨æƒ³è¿›è¡Œé‚£ç§æ“作? +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_assigned: 已指派 +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_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/groups/lib/SVG/GPL.txt b/groups/lib/SVG/GPL.txt new file mode 100644 index 000000000..5b6e7c66c --- /dev/null +++ b/groups/lib/SVG/GPL.txt @@ -0,0 +1,340 @@ + 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. + + 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. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You 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. + + , 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. diff --git a/groups/lib/SVG/Graph/Bar.rb b/groups/lib/SVG/Graph/Bar.rb new file mode 100644 index 000000000..c394f17fe --- /dev/null +++ b/groups/lib/SVG/Graph/Bar.rb @@ -0,0 +1,137 @@ +require 'rexml/document' +require 'SVG/Graph/Graph' +require 'SVG/Graph/BarBase' + +module SVG + module Graph + # === Create presentation quality SVG bar graphs easily + # + # = Synopsis + # + # require 'SVG/Graph/Bar' + # + # fields = %w(Jan Feb Mar); + # data_sales_02 = [12, 45, 21] + # + # graph = SVG::Graph::Bar.new( + # :height => 500, + # :width => 300, + # :fields => fields + # ) + # + # graph.add_data( + # :data => data_sales_02, + # :title => 'Sales 2002' + # ) + # + # 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[http://www.w3c.org/tr/svg bar 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. + # + # = Notes + # + # The default stylesheet handles upto 12 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 12 data sets as they will have no style and + # be in black. + # + # = Examples + # + # * http://germane-software.com/repositories/public/SVG/test/test.rb + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + class Bar < BarBase + include REXML + + # See Graph::initialize and BarBase::set_defaults + def set_defaults + super + self.top_align = self.top_font = 1 + end + + protected + + def get_x_labels + @config[:fields] + 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 x_label_offset( width ) + width / 2.0 + end + + def draw_data + fieldwidth = field_width + maxvalue = max_value + minvalue = min_value + + fieldheight = (@graph_height.to_f - font_size*2*top_font) / + (get_y_labels.max - get_y_labels.min) + bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0 + + subbar_width = fieldwidth - bargap + subbar_width /= @data.length if stack == :side + x_mod = (@graph_width-bargap)/2 - (stack==:side ? subbar_width/2 : 0) + # Y1 + p2 = @graph_height + # to X2 + field_count = 0 + @config[:fields].each_index { |i| + dataset_count = 0 + for dataset in @data + # X1 + p1 = (fieldwidth * field_count) + # to Y2 + p3 = @graph_height - ((dataset[:data][i] - minvalue) * fieldheight) + p1 += subbar_width * dataset_count if stack == :side + @graph.add_element( "path", { + "class" => "fill#{dataset_count+1}", + "d" => "M#{p1} #{p2} V#{p3} h#{subbar_width} V#{p2} Z" + }) + make_datapoint_text( + p1 + subbar_width/2.0, + p3 - 6, + dataset[:data][i].to_s) + dataset_count += 1 + end + field_count += 1 + } + end + end + end +end diff --git a/groups/lib/SVG/Graph/BarBase.rb b/groups/lib/SVG/Graph/BarBase.rb new file mode 100644 index 000000000..b7d1e9055 --- /dev/null +++ b/groups/lib/SVG/Graph/BarBase.rb @@ -0,0 +1,140 @@ +require 'rexml/document' +require 'SVG/Graph/Graph' + +module SVG + module Graph + # = Synopsis + # + # A superclass for bar-style graphs. Do not attempt to instantiate + # directly; use one of the subclasses instead. + # + # = Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class BarBase < SVG::Graph::Graph + # Ensures that :fields are provided in the configuration. + 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 + # [bar_gap] true + # [stack] :overlap + def set_defaults + init_with( :bar_gap => true, :stack => :overlap ) + end + + # Whether to have a gap between the bars or not, default + # is true, set to false if you don't want gaps. + attr_accessor :bar_gap + # How to stack data sets. :overlap overlaps bars with + # transparent colors, :top stacks bars on top of one another, + # :side stacks the bars side-by-side. Defaults to :overlap. + attr_accessor :stack + + + protected + + def max_value + return @data.collect{|x| x[:data].max}.max + end + + def min_value + min = 0 + + if (min_scale_value.nil? == false) then + min = min_scale_value + else + min = @data.collect{|x| x[:data].min}.min + end + + return min + end + + def get_css + return < 500, + # :width => 300, + # :fields => fields, + # }) + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002', + # }) + # + # 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 horitonzal bar 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://germane-software.com/repositories/public/SVG/test/test.rb + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class BarHorizontal < BarBase + # In addition to the defaults set in BarBase::set_defaults, sets + # [rotate_y_labels] true + # [show_x_guidelines] true + # [show_y_guidelines] false + def set_defaults + super + init_with( + :rotate_y_labels => true, + :show_x_guidelines => true, + :show_y_guidelines => false + ) + self.right_align = self.right_font = 1 + end + + protected + + def get_x_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 get_y_labels + @config[:fields] + end + + def y_label_offset( height ) + height / -2.0 + end + + def draw_data + minvalue = min_value + fieldheight = field_height + fieldwidth = (@graph_width.to_f - font_size*2*right_font ) / + (get_x_labels.max - get_x_labels.min ) + bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0 + + subbar_height = fieldheight - bargap + subbar_height /= @data.length if stack == :side + + field_count = 1 + y_mod = (subbar_height / 2) + (font_size / 2) + @config[:fields].each_index { |i| + dataset_count = 0 + for dataset in @data + y = @graph_height - (fieldheight * field_count) + y += (subbar_height * dataset_count) if stack == :side + x = (dataset[:data][i] - minvalue) * fieldwidth + + @graph.add_element( "path", { + "d" => "M0 #{y} H#{x} v#{subbar_height} H0 Z", + "class" => "fill#{dataset_count+1}" + }) + make_datapoint_text( + x+5, y+y_mod, dataset[:data][i], "text-anchor: start; " + ) + dataset_count += 1 + end + field_count += 1 + } + end + end + end +end diff --git a/groups/lib/SVG/Graph/Graph.rb b/groups/lib/SVG/Graph/Graph.rb new file mode 100644 index 000000000..403a0202b --- /dev/null +++ b/groups/lib/SVG/Graph/Graph.rb @@ -0,0 +1,977 @@ +begin + require 'zlib' + @@__have_zlib = true +rescue + @@__have_zlib = false +end + +require 'rexml/document' + +module SVG + module Graph + VERSION = '@ANT_VERSION@' + + # === Base object for generating SVG Graphs + # + # == Synopsis + # + # This class is only used as a superclass of specialized charts. Do not + # attempt to use this class directly, unless creating a new chart type. + # + # For examples of how to subclass this class, see the existing specific + # subclasses, such as SVG::Graph::Pie. + # + # == Examples + # + # For examples of how to use this package, see either the test files, or + # the documentation for the specific class you want to use. + # + # * file:test/plot.rb + # * file:test/single.rb + # * file:test/test.rb + # * file:test/timeseries.rb + # + # == Description + # + # This package should be used as a base for creating SVG graphs. + # + # == Acknowledgements + # + # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby + # port is based on. + # + # Stephen Morgan for creating the TT template and SVG. + # + # == See + # + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Graph + include REXML + + # Initialize the graph object with the graph settings. You won't + # instantiate this class directly; see the subclass for options. + # [width] 500 + # [height] 300 + # [show_x_guidelines] false + # [show_y_guidelines] true + # [show_data_values] true + # [min_scale_value] 0 + # [show_x_labels] true + # [stagger_x_labels] false + # [rotate_x_labels] false + # [step_x_labels] 1 + # [step_include_first_x_label] true + # [show_y_labels] true + # [rotate_y_labels] false + # [scale_integers] false + # [show_x_title] false + # [x_title] 'X Field names' + # [show_y_title] false + # [y_title_text_direction] :bt + # [y_title] 'Y Scale' + # [show_graph_title] false + # [graph_title] 'Graph Title' + # [show_graph_subtitle] false + # [graph_subtitle] 'Graph Sub Title' + # [key] true, + # [key_position] :right, # bottom or righ + # [font_size] 12 + # [title_font_size] 16 + # [subtitle_font_size] 14 + # [x_label_font_size] 12 + # [x_title_font_size] 14 + # [y_label_font_size] 12 + # [y_title_font_size] 14 + # [key_font_size] 10 + # [no_css] false + # [add_popups] false + def initialize( config ) + @config = config + + self.top_align = self.top_font = self.right_align = self.right_font = 0 + + init_with({ + :width => 500, + :height => 300, + :show_x_guidelines => false, + :show_y_guidelines => true, + :show_data_values => true, + + :min_scale_value => 0, + + :show_x_labels => true, + :stagger_x_labels => false, + :rotate_x_labels => false, + :step_x_labels => 1, + :step_include_first_x_label => true, + + :show_y_labels => true, + :rotate_y_labels => false, + :stagger_y_labels => false, + :scale_integers => false, + + :show_x_title => false, + :x_title => 'X Field names', + + :show_y_title => false, + :y_title_text_direction => :bt, + :y_title => 'Y Scale', + + :show_graph_title => false, + :graph_title => 'Graph Title', + :show_graph_subtitle => false, + :graph_subtitle => 'Graph Sub Title', + :key => true, + :key_position => :right, # bottom or right + + :font_size =>10, + :title_font_size =>12, + :subtitle_font_size =>14, + :x_label_font_size =>11, + :x_title_font_size =>14, + :y_label_font_size =>11, + :y_title_font_size =>14, + :key_font_size => 9, + + :no_css =>false, + :add_popups =>false, + }) + + set_defaults if methods.include? "set_defaults" + + init_with config + end + + + # This method allows you do add data to the graph object. + # It can be called several times to add more data sets in. + # + # data_sales_02 = [12, 45, 21]; + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002' + # }) + def add_data conf + @data = [] unless defined? @data + + if conf[:data] and conf[:data].kind_of? Array + @data << conf + else + raise "No data provided by #{conf.inspect}" + end + end + + + # This method removes all data from the object so that you can + # reuse it to create a new graph but with the same config options. + # + # graph.clear_data + def clear_data + @data = [] + end + + + # This method processes the template with the data and + # config which has been set and returns the resulting SVG. + # + # This method will croak unless at least one data set has + # been added to the graph object. + # + # print graph.burn + def burn + raise "No data available" unless @data.size > 0 + + calculations if methods.include? 'calculations' + + start_svg + calculate_graph_dimensions + @foreground = Element.new( "g" ) + draw_graph + draw_titles + draw_legend + draw_data + @graph.add_element( @foreground ) + style + + data = "" + @doc.write( data, 0 ) + + if @config[:compress] + if @@__have_zlib + inp, out = IO.pipe + gz = Zlib::GzipWriter.new( out ) + gz.write data + gz.close + data = inp.read + else + data << ""; + end + end + + return data + end + + + # Set the height of the graph box, this is the total height + # of the SVG box created - not the graph it self which auto + # scales to fix the space. + attr_accessor :height + # Set the width of the graph box, this is the total width + # of the SVG box created - not the graph it self which auto + # scales to fix the space. + attr_accessor :width + # Set the path to an external stylesheet, set to '' if + # you want to revert back to using the defaut internal version. + # + # To create an external stylesheet create a graph using the + # default internal version and copy the stylesheet section to + # an external file and edit from there. + attr_accessor :style_sheet + # (Bool) Show the value of each element of data on the graph + attr_accessor :show_data_values + # The point at which the Y axis starts, defaults to '0', + # if set to nil it will default to the minimum data value. + attr_accessor :min_scale_value + # Whether to show labels on the X axis or not, defaults + # to true, set to false if you want to turn them off. + attr_accessor :show_x_labels + # This puts the X labels at alternative levels so if they + # are long field names they will not overlap so easily. + # Default it false, to turn on set to true. + attr_accessor :stagger_x_labels + # This puts the Y labels at alternative levels so if they + # are long field names they will not overlap so easily. + # Default it false, to turn on set to true. + attr_accessor :stagger_y_labels + # This turns the X axis labels by 90 degrees. + # Default it false, to turn on set to true. + attr_accessor :rotate_x_labels + # This turns the Y axis labels by 90 degrees. + # Default it false, to turn on set to true. + attr_accessor :rotate_y_labels + # How many "steps" to use between displayed X axis labels, + # a step of one means display every label, a step of two results + # in every other label being displayed (label label label), + # a step of three results in every third label being displayed + # (label label label) and so on. + attr_accessor :step_x_labels + # Whether to (when taking "steps" between X axis labels) step from + # the first label (i.e. always include the first label) or step from + # the X axis origin (i.e. start with a gap if step_x_labels is greater + # than one). + attr_accessor :step_include_first_x_label + # Whether to show labels on the Y axis or not, defaults + # to true, set to false if you want to turn them off. + attr_accessor :show_y_labels + # Ensures only whole numbers are used as the scale divisions. + # Default it false, to turn on set to true. This has no effect if + # scale divisions are less than 1. + attr_accessor :scale_integers + # This defines the gap between markers on the Y axis, + # default is a 10th of the max_value, e.g. you will have + # 10 markers on the Y axis. NOTE: do not set this too + # low - you are limited to 999 markers, after that the + # graph won't generate. + attr_accessor :scale_divisions + # Whether to show the title under the X axis labels, + # default is false, set to true to show. + attr_accessor :show_x_title + # What the title under X axis should be, e.g. 'Months'. + attr_accessor :x_title + # Whether to show the title under the Y axis labels, + # default is false, set to true to show. + attr_accessor :show_y_title + # Aligns writing mode for Y axis label. + # Defaults to :bt (Bottom to Top). + # Change to :tb (Top to Bottom) to reverse. + attr_accessor :y_title_text_direction + # What the title under Y axis should be, e.g. 'Sales in thousands'. + attr_accessor :y_title + # Whether to show a title on the graph, defaults + # to false, set to true to show. + attr_accessor :show_graph_title + # What the title on the graph should be. + attr_accessor :graph_title + # Whether to show a subtitle on the graph, defaults + # to false, set to true to show. + attr_accessor :show_graph_subtitle + # What the subtitle on the graph should be. + attr_accessor :graph_subtitle + # Whether to show a key, defaults to false, set to + # true if you want to show it. + attr_accessor :key + # Where the key should be positioned, defaults to + # :right, set to :bottom if you want to move it. + attr_accessor :key_position + # Set the font size (in points) of the data point labels + attr_accessor :font_size + # Set the font size of the X axis labels + attr_accessor :x_label_font_size + # Set the font size of the X axis title + attr_accessor :x_title_font_size + # Set the font size of the Y axis labels + attr_accessor :y_label_font_size + # Set the font size of the Y axis title + attr_accessor :y_title_font_size + # Set the title font size + attr_accessor :title_font_size + # Set the subtitle font size + attr_accessor :subtitle_font_size + # Set the key font size + attr_accessor :key_font_size + # Show guidelines for the X axis + attr_accessor :show_x_guidelines + # Show guidelines for the Y axis + attr_accessor :show_y_guidelines + # Do not use CSS if set to true. Many SVG viewers do not support CSS, but + # not using CSS can result in larger SVGs as well as making it impossible to + # change colors after the chart is generated. Defaults to false. + attr_accessor :no_css + # Add popups for the data points on some graphs + attr_accessor :add_popups + + + protected + + def sort( *arrys ) + sort_multiple( arrys ) + end + + # Overwrite configuration options with supplied options. Used + # by subclasses. + def init_with config + config.each { |key, value| + self.send( key.to_s+"=", value ) if methods.include? key.to_s + } + end + + attr_accessor :top_align, :top_font, :right_align, :right_font + + KEY_BOX_SIZE = 12 + + # Override this (and call super) to change the margin to the left + # of the plot area. Results in @border_left being set. + def calculate_left_margin + @border_left = 7 + # Check for Y labels + max_y_label_height_px = rotate_y_labels ? + y_label_font_size : + get_y_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * y_label_font_size * 0.6 + @border_left += max_y_label_height_px if show_y_labels + @border_left += max_y_label_height_px + 10 if stagger_y_labels + @border_left += y_title_font_size + 5 if show_y_title + end + + + # Calculates the width of the widest Y label. This will be the + # character height if the Y labels are rotated + def max_y_label_width_px + return font_size if rotate_y_labels + end + + + # Override this (and call super) to change the margin to the right + # of the plot area. Results in @border_right being set. + def calculate_right_margin + @border_right = 7 + if key and key_position == :right + val = keys.max { |a,b| a.length <=> b.length } + @border_right += val.length * key_font_size * 0.7 + @border_right += KEY_BOX_SIZE + @border_right += 10 # Some padding around the box + end + end + + + # Override this (and call super) to change the margin to the top + # of the plot area. Results in @border_top being set. + def calculate_top_margin + @border_top = 5 + @border_top += title_font_size if show_graph_title + @border_top += 5 + @border_top += subtitle_font_size if show_graph_subtitle + end + + + # Adds pop-up point information to a graph. + def add_popup( x, y, label ) + txt_width = label.length * font_size * 0.6 + 10 + tx = (x+txt_width > width ? x-5 : x+5) + t = @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => (y - font_size).to_s, + "visibility" => "hidden", + }) + t.attributes["style"] = "fill: #000; "+ + (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") + t.text = label.to_s + t.attributes["id"] = t.id.to_s + + @foreground.add_element( "circle", { + "cx" => x.to_s, + "cy" => y.to_s, + "r" => "10", + "style" => "opacity: 0", + "onmouseover" => + "document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )", + "onmouseout" => + "document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )", + }) + + end + + + # Override this (and call super) to change the margin to the bottom + # of the plot area. Results in @border_bottom being set. + def calculate_bottom_margin + @border_bottom = 7 + if key and key_position == :bottom + @border_bottom += @data.size * (font_size + 5) + @border_bottom += 10 + end + if show_x_labels + max_x_label_height_px = rotate_x_labels ? + get_x_labels.max{|a,b| + a.length<=>b.length + }.length * x_label_font_size * 0.6 : + x_label_font_size + @border_bottom += max_x_label_height_px + @border_bottom += max_x_label_height_px + 10 if stagger_x_labels + end + @border_bottom += x_title_font_size + 5 if show_x_title + end + + + # Draws the background, axis, and labels. + def draw_graph + @graph = @root.add_element( "g", { + "transform" => "translate( #@border_left #@border_top )" + }) + + # Background + @graph.add_element( "rect", { + "x" => "0", + "y" => "0", + "width" => @graph_width.to_s, + "height" => @graph_height.to_s, + "class" => "graphBackground" + }) + + # Axis + @graph.add_element( "path", { + "d" => "M 0 0 v#@graph_height", + "class" => "axis", + "id" => "xAxis" + }) + @graph.add_element( "path", { + "d" => "M 0 #@graph_height h#@graph_width", + "class" => "axis", + "id" => "yAxis" + }) + + draw_x_labels + draw_y_labels + end + + + # Where in the X area the label is drawn + # Centered in the field, should be width/2. Start, 0. + def x_label_offset( width ) + 0 + end + + def make_datapoint_text( x, y, value, style="" ) + if show_data_values + @foreground.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "dataPointLabel", + "style" => "#{style} stroke: #fff; stroke-width: 2;" + }).text = value.to_s + text = @foreground.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "dataPointLabel" + }) + text.text = value.to_s + text.attributes["style"] = style if style.length > 0 + end + end + + + # Draws the X axis labels + def draw_x_labels + stagger = x_label_font_size + 5 + if show_x_labels + label_width = field_width + + count = 0 + for label in get_x_labels + if step_include_first_x_label == true then + step = count % step_x_labels + else + step = (count + 1) % step_x_labels + end + + if step == 0 then + text = @graph.add_element( "text" ) + text.attributes["class"] = "xAxisLabels" + text.text = label.to_s + + x = count * label_width + x_label_offset( label_width ) + y = @graph_height + x_label_font_size + 3 + t = 0 - (font_size / 2) + + if stagger_x_labels and count % 2 == 1 + y += stagger + @graph.add_element( "path", { + "d" => "M#{x} #@graph_height v#{stagger}", + "class" => "staggerGuideLine" + }) + end + + text.attributes["x"] = x.to_s + text.attributes["y"] = y.to_s + if rotate_x_labels + text.attributes["transform"] = + "rotate( 90 #{x} #{y-x_label_font_size} )"+ + " translate( 0 -#{x_label_font_size/4} )" + text.attributes["style"] = "text-anchor: start" + else + text.attributes["style"] = "text-anchor: middle" + end + end + + draw_x_guidelines( label_width, count ) if show_x_guidelines + count += 1 + end + end + end + + + # Where in the Y area the label is drawn + # Centered in the field, should be width/2. Start, 0. + def y_label_offset( height ) + 0 + end + + + def field_width + (@graph_width.to_f - font_size*2*right_font) / + (get_x_labels.length - right_align) + end + + + def field_height + (@graph_height.to_f - font_size*2*top_font) / + (get_y_labels.length - top_align) + end + + + # Draws the Y axis labels + def draw_y_labels + stagger = y_label_font_size + 5 + if show_y_labels + label_height = field_height + + count = 0 + y_offset = @graph_height + y_label_offset( label_height ) + y_offset += font_size/1.2 unless rotate_y_labels + for label in get_y_labels + y = y_offset - (label_height * count) + x = rotate_y_labels ? 0 : -3 + + if stagger_y_labels and count % 2 == 1 + x -= stagger + @graph.add_element( "path", { + "d" => "M#{x} #{y} h#{stagger}", + "class" => "staggerGuideLine" + }) + end + + text = @graph.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "yAxisLabels" + }) + text.text = label.to_s + if rotate_y_labels + text.attributes["transform"] = "translate( -#{font_size} 0 ) "+ + "rotate( 90 #{x} #{y} ) " + text.attributes["style"] = "text-anchor: middle" + else + text.attributes["y"] = (y - (y_label_font_size/2)).to_s + text.attributes["style"] = "text-anchor: end" + end + draw_y_guidelines( label_height, count ) if show_y_guidelines + count += 1 + end + end + end + + + # Draws the X axis guidelines + def draw_x_guidelines( label_height, count ) + if count != 0 + @graph.add_element( "path", { + "d" => "M#{label_height*count} 0 v#@graph_height", + "class" => "guideLines" + }) + end + end + + + # Draws the Y axis guidelines + def draw_y_guidelines( label_height, count ) + if count != 0 + @graph.add_element( "path", { + "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width", + "class" => "guideLines" + }) + end + end + + + # Draws the graph title and subtitle + def draw_titles + if show_graph_title + @root.add_element( "text", { + "x" => (width / 2).to_s, + "y" => (title_font_size).to_s, + "class" => "mainTitle" + }).text = graph_title.to_s + end + + if show_graph_subtitle + y_subtitle = show_graph_title ? + title_font_size + 10 : + subtitle_font_size + @root.add_element("text", { + "x" => (width / 2).to_s, + "y" => (y_subtitle).to_s, + "class" => "subTitle" + }).text = graph_subtitle.to_s + end + + if show_x_title + y = @graph_height + @border_top + x_title_font_size + if show_x_labels + y += x_label_font_size + 5 if stagger_x_labels + y += x_label_font_size + 5 + end + x = width / 2 + + @root.add_element("text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "xAxisTitle", + }).text = x_title.to_s + end + + if show_y_title + x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) + y = height / 2 + + text = @root.add_element("text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "yAxisTitle", + }) + text.text = y_title.to_s + if y_title_text_direction == :bt + text.attributes["transform"] = "rotate( -90, #{x}, #{y} )" + else + text.attributes["transform"] = "rotate( 90, #{x}, #{y} )" + end + end + end + + def keys + return @data.collect{ |d| d[:title] } + end + + # Draws the legend on the graph + def draw_legend + if key + group = @root.add_element( "g" ) + + key_count = 0 + for key_name in keys + y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5) + group.add_element( "rect", { + "x" => 0.to_s, + "y" => y_offset.to_s, + "width" => KEY_BOX_SIZE.to_s, + "height" => KEY_BOX_SIZE.to_s, + "class" => "key#{key_count+1}" + }) + group.add_element( "text", { + "x" => (KEY_BOX_SIZE + 5).to_s, + "y" => (y_offset + KEY_BOX_SIZE - 2).to_s, + "class" => "keyText" + }).text = key_name.to_s + key_count += 1 + end + + case key_position + when :right + x_offset = @graph_width + @border_left + 10 + y_offset = @border_top + 20 + when :bottom + x_offset = @border_left + 20 + y_offset = @border_top + @graph_height + 5 + if show_x_labels + max_x_label_height_px = rotate_x_labels ? + get_x_labels.max{|a,b| + a.length<=>b.length + }.length * x_label_font_size : + x_label_font_size + y_offset += max_x_label_height_px + y_offset += max_x_label_height_px + 5 if stagger_x_labels + end + y_offset += x_title_font_size + 5 if show_x_title + end + group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" + end + end + + + private + + def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) + if lo < hi + p = partition(arrys,lo,hi) + sort_multiple(arrys, lo, p-1) + sort_multiple(arrys, p+1, hi) + end + arrys + end + + def partition( arrys, lo, hi ) + p = arrys[0][lo] + l = lo + z = lo+1 + while z <= hi + if arrys[0][z] < p + l += 1 + arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } + end + z += 1 + end + arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } + l + end + + def style + if no_css + styles = parse_css + @root.elements.each("//*[@class]") { |el| + cl = el.attributes["class"] + style = styles[cl] + style += el.attributes["style"] if el.attributes["style"] + el.attributes["style"] = style + } + end + end + + def parse_css + css = get_style + rv = {} + while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m + names_orig = names = $1 + css = $' + css =~ /([^}]+)\}/m + content = $1 + css = $' + + nms = [] + while names =~ /^\s*,?\s*\.(\w+)/ + nms << $1 + names = $' + end + + content = content.tr( "\n\t", " ") + for name in nms + current = rv[name] + current = current ? current+"; "+content : content + rv[name] = current.strip.squeeze(" ") + end + end + return rv + end + + + # Override and place code to add defs here + def add_defs defs + end + + + def start_svg + # Base document + @doc = Document.new + @doc << XMLDecl.new + @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + + %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} ) + if style_sheet && style_sheet != '' + @doc << ProcessingInstruction.new( "xml-stylesheet", + %Q{href="#{style_sheet}" type="text/css"} ) + end + @root = @doc.add_element( "svg", { + "width" => width.to_s, + "height" => height.to_s, + "viewBox" => "0 0 #{width} #{height}", + "xmlns" => "http://www.w3.org/2000/svg", + "xmlns:xlink" => "http://www.w3.org/1999/xlink", + "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", + "a3:scriptImplementation" => "Adobe" + }) + @root << Comment.new( " "+"\\"*66 ) + @root << Comment.new( " Created with SVG::Graph " ) + @root << Comment.new( " SVG::Graph by Sean E. Russell " ) + @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ + " Leo Lapworth & Stephan Morgan " ) + @root << Comment.new( " "+"/"*66 ) + + defs = @root.add_element( "defs" ) + add_defs defs + if not(style_sheet && style_sheet != '') and !no_css + @root << Comment.new(" include default stylesheet if none specified ") + style = defs.add_element( "style", {"type"=>"text/css"} ) + style << CData.new( get_style ) + end + + @root << Comment.new( "SVG Background" ) + @root.add_element( "rect", { + "width" => width.to_s, + "height" => height.to_s, + "x" => "0", + "y" => "0", + "class" => "svgBackground" + }) + end + + + def calculate_graph_dimensions + calculate_left_margin + calculate_right_margin + calculate_bottom_margin + calculate_top_margin + @graph_width = width - @border_left - @border_right + @graph_height = height - @border_top - @border_bottom + end + + def get_style + return < 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 + # + # 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 < 500, + # :width => 300, + # :fields => fields, + # }) + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002', + # }) + # + # 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 pie 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, display percent on pie chart, + # title, subtitle etc. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/single.rb + # + # == See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Pie < Graph + # Defaults are those set by Graph::initialize, and + # [show_shadow] true + # [shadow_offset] 10 + # [show_data_labels] false + # [show_actual_values] false + # [show_percent] true + # [show_key_data_labels] true + # [show_key_actual_values] true + # [show_key_percent] false + # [expanded] false + # [expand_greatest] false + # [expand_gap] 10 + # [show_x_labels] false + # [show_y_labels] false + # [datapoint_font_size] 12 + def set_defaults + init_with( + :show_shadow => true, + :shadow_offset => 10, + + :show_data_labels => false, + :show_actual_values => false, + :show_percent => true, + + :show_key_data_labels => true, + :show_key_actual_values => true, + :show_key_percent => false, + + :expanded => false, + :expand_greatest => false, + :expand_gap => 10, + + :show_x_labels => false, + :show_y_labels => false, + :datapoint_font_size => 12 + ) + @data = [] + end + + # Adds a data set to the graph. + # + # graph.add_data( { :data => [1,2,3,4] } ) + # + # Note that the :title is not necessary. If multiple + # data sets are added to the graph, the pie chart will + # display the +sums+ of the data. EG: + # + # graph.add_data( { :data => [1,2,3,4] } ) + # graph.add_data( { :data => [2,3,5,9] } ) + # + # is the same as: + # + # graph.add_data( { :data => [3,5,8,13] } ) + def add_data arg + arg[:data].each_index {|idx| + @data[idx] = 0 unless @data[idx] + @data[idx] += arg[:data][idx] + } + end + + # If true, displays a drop shadow for the chart + attr_accessor :show_shadow + # Sets the offset of the shadow from the pie chart + attr_accessor :shadow_offset + # If true, display the data labels on the chart + attr_accessor :show_data_labels + # If true, display the actual field values in the data labels + attr_accessor :show_actual_values + # If true, display the percentage value of each pie wedge in the data + # labels + attr_accessor :show_percent + # If true, display the labels in the key + attr_accessor :show_key_data_labels + # If true, display the actual value of the field in the key + attr_accessor :show_key_actual_values + # If true, display the percentage value of the wedges in the key + attr_accessor :show_key_percent + # If true, "explode" the pie (put space between the wedges) + attr_accessor :expanded + # If true, expand the largest pie wedge + attr_accessor :expand_greatest + # The amount of space between expanded wedges + attr_accessor :expand_gap + # The font size of the data point labels + attr_accessor :datapoint_font_size + + + protected + + def add_defs defs + gradient = defs.add_element( "filter", { + "id"=>"dropshadow", + "width" => "1.2", + "height" => "1.2", + } ) + gradient.add_element( "feGaussianBlur", { + "stdDeviation" => "4", + "result" => "blur" + }) + end + + # We don't need the graph + def draw_graph + end + + def get_y_labels + [""] + end + + def get_x_labels + [""] + end + + def keys + total = 0 + max_value = 0 + @data.each {|x| total += x } + percent_scale = 100.0 / total + count = -1 + a = @config[:fields].collect{ |x| + count += 1 + v = @data[count] + perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" + x + " [" + v.to_s + "]" + perc + } + end + + RADIANS = Math::PI/180 + + def draw_data + @graph = @root.add_element( "g" ) + background = @graph.add_element("g") + midground = @graph.add_element("g") + + diameter = @graph_height > @graph_width ? @graph_width : @graph_height + diameter -= expand_gap if expanded or expand_greatest + diameter -= datapoint_font_size if show_data_labels + diameter -= 10 if show_shadow + radius = diameter / 2.0 + + xoff = (width - diameter) / 2 + yoff = (height - @border_bottom - diameter) + yoff -= 10 if show_shadow + @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" + + wedge_text_pad = 5 + wedge_text_pad = 20 if show_percent and show_data_labels + + total = 0 + max_value = 0 + @data.each {|x| + max_value = max_value < x ? x : max_value + total += x + } + percent_scale = 100.0 / total + + prev_percent = 0 + rad_mult = 3.6 * RADIANS + @config[:fields].each_index { |count| + value = @data[count] + percent = percent_scale * value + + radians = prev_percent * rad_mult + x_start = radius+(Math.sin(radians) * radius) + y_start = radius-(Math.cos(radians) * radius) + radians = (prev_percent+percent) * rad_mult + x_end = radius+(Math.sin(radians) * radius) + y_end = radius-(Math.cos(radians) * radius) + path = "M#{radius},#{radius} L#{x_start},#{y_start} "+ + "A#{radius},#{radius} "+ + "0, #{percent >= 50 ? '1' : '0'},1, "+ + "#{x_end} #{y_end} Z" + + + wedge = @foreground.add_element( "path", { + "d" => path, + "class" => "fill#{count+1}" + }) + + translate = nil + tx = 0 + ty = 0 + half_percent = prev_percent + percent / 2 + radians = half_percent * rad_mult + + if show_shadow + shadow = background.add_element( "path", { + "d" => path, + "filter" => "url(#dropshadow)", + "style" => "fill: #ccc; stroke: none;" + }) + clear = midground.add_element( "path", { + "d" => path, + "style" => "fill: #fff; stroke: none;" + }) + end + + if expanded or (expand_greatest && value == max_value) + tx = (Math.sin(radians) * expand_gap) + ty = -(Math.cos(radians) * expand_gap) + translate = "translate( #{tx} #{ty} )" + wedge.attributes["transform"] = translate + clear.attributes["transform"] = translate + end + + if show_shadow + shadow.attributes["transform"] = + "translate( #{tx+shadow_offset} #{ty+shadow_offset} )" + end + + if show_data_labels and value != 0 + label = "" + label += @config[:fields][count] if show_key_data_labels + label += " ["+value.to_s+"]" if show_actual_values + label += " "+percent.round.to_s+"%" if show_percent + + msr = Math.sin(radians) + mcr = Math.cos(radians) + tx = radius + (msr * radius) + ty = radius -(mcr * radius) + + if expanded or (expand_greatest && value == max_value) + tx += (msr * expand_gap) + ty -= (mcr * expand_gap) + end + @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => ty.to_s, + "class" => "dataPointLabel", + "style" => "stroke: #fff; stroke-width: 2;" + }).text = label.to_s + @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => ty.to_s, + "class" => "dataPointLabel", + }).text = label.to_s + end + + prev_percent += percent + } + end + + + def round val, to + up = 10**to.to_f + (val * up).to_i / up + end + + + def get_css + return < 500, + # :width => 300, + # :key => true, + # :scale_x_integers => true, + # :scale_y_integerrs => true, + # }) + # + # graph.add_data({ + # :data => projection + # :title => 'Projected', + # }) + # + # graph.add_data({ + # :data => actual, + # :title => 'Actual', + # }) + # + # print graph.burn() + # + # = Description + # + # Produces a graph of scalar data. + # + # This object aims to allow you to easily create high quality + # SVG[http://www.w3c.org/tr/svg] scalar plots. 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/plot.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. + # + # Unlike the other types of charts, data sets must contain x,y pairs: + # + # [ 1, 2 ] # A data set with 1 point: (1,2) + # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Plot < Graph + + # In addition to the defaults set by Graph::initialize, sets + # [show_data_points] true + # [area_fill] false + # [stacked] false + def set_defaults + init_with( + :show_data_points => true, + :area_fill => false, + :stacked => false + ) + self.top_align = self.right_align = self.top_font = self.right_font = 1 + end + + # Determines the scaling for the X axis divisions. + # + # graph.scale_x_divisions = 2 + # + # would cause the graph to attempt to generate labels stepped by 2; EG: + # 0,2,4,6,8... + attr_accessor :scale_x_divisions + # Determines the scaling for the Y axis divisions. + # + # graph.scale_y_divisions = 0.5 + # + # would cause the graph to attempt to generate labels stepped by 0.5; EG: + # 0, 0.5, 1, 1.5, 2, ... + attr_accessor :scale_y_divisions + # Make the X axis labels integers + attr_accessor :scale_x_integers + # Make the Y axis labels integers + attr_accessor :scale_y_integers + # Fill the area under the line + attr_accessor :area_fill + # Show a small circle on the graph where the line + # goes from one point to the next. + attr_accessor :show_data_points + # Set the minimum value of the X axis + attr_accessor :min_x_value + # Set the minimum value of the Y axis + attr_accessor :min_y_value + + + # Adds data to the plot. The data must be in X,Y pairs; EG + # [ 1, 2 ] # A data set with 1 point: (1,2) + # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) + 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 x,y pairs! "+ + "The data provided contained an odd set of "+ + "data points" unless data[:data].length % 2 == 0 + return if data[:data].length == 0 + + x = [] + y = [] + data[:data].each_index {|i| + (i%2 == 0 ? x : y) << data[:data][i] + } + sort( x, y ) + data[:data] = [x,y] + @data << data + end + + + protected + + def keys + @data.collect{ |x| x[:title] } + end + + def calculate_left_margin + super + label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6 + @border_left = label_left if label_left > @border_left + end + + def calculate_right_margin + super + label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6 + @border_right = label_right if label_right > @border_right + end + + + X = 0 + Y = 1 + def x_range + max_value = @data.collect{|x| x[:data][X][-1] }.max + min_value = @data.collect{|x| x[:data][X][0] }.min + min_value = min_value "M#{x_start} #@graph_height #{lpath} V#@graph_height Z", + "class" => "fill#{line}" + }) + end + + @graph.add_element( "path", { + "d" => "M#{x_start} #{y_start} #{lpath}", + "class" => "line#{line}" + }) + + if show_data_points || show_data_values + x_points.each_index { |idx| + x = (x_points[idx] - x_min) * x_step + y = @graph_height - (y_points[idx] - y_min) * y_step + if show_data_points + @graph.add_element( "circle", { + "cx" => x.to_s, + "cy" => y.to_s, + "r" => "2.5", + "class" => "dataPoint#{line}" + }) + add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups + end + make_datapoint_text( x, y-6, y_points[idx] ) + } + end + line += 1 + end + end + + def format x, y + "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})" + end + + def get_css + return < 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 + # + # 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 < 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 diff --git a/groups/lib/SVG/Graph/TimeSeries.rb b/groups/lib/SVG/Graph/TimeSeries.rb new file mode 100644 index 000000000..7ab3e7476 --- /dev/null +++ b/groups/lib/SVG/Graph/TimeSeries.rb @@ -0,0 +1,241 @@ +require 'SVG/Graph/Plot' +require 'parsedate' + +module SVG + module Graph + # === For creating SVG plots of scalar temporal data + # + # = Synopsis + # + # require 'SVG/Graph/TimeSeriess' + # + # # Data sets are x,y pairs + # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, + # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13] + # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, + # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, + # "5/1/84", 17, "10/1/80", 12] + # + # graph = SVG::Graph::TimeSeries.new( { + # :width => 640, + # :height => 480, + # :graph_title => title, + # :show_graph_title => true, + # :no_css => true, + # :key => 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", + # :show_y_title => true, + # :y_title => "Ice Cream Cones", + # :y_title_text_direction => :bt, + # :stagger_x_labels => true, + # :x_label_format => "%m/%d/%y", + # }) + # + # graph.add_data({ + # :data => projection + # :title => 'Projected', + # }) + # + # graph.add_data({ + # :data => actual, + # :title => 'Actual', + # }) + # + # print graph.burn() + # + # = Description + # + # Produces a graph of temporal scalar data. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/timeseries.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. + # + # Unlike the other types of charts, data sets must contain x,y pairs: + # + # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) + # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and + # # ("14:20",6) + # + # 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 + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class TimeSeries < Plot + # 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 + super + init_with( + #:max_time_span => '', + :x_label_format => '%Y-%m-%d %H:%M:%S', + :popup_format => '%Y-%m-%d %H:%M:%S' + ) + 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 + + # Add data to the plot. + # + # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) + # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and + # # ("14:20",6) + # graph.add_data( + # :data => d1, + # :title => 'One' + # ) + # graph.add_data( + # :data => d2, + # :title => 'Two' + # ) + # + # Note that the data must be in time,value pairs, and that the date format + # may be any date that is parseable by ParseDate. + 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 x,y pairs! "+ + "The data provided contained an odd set of "+ + "data points" unless data[:data].length % 2 == 0 + return if data[:data].length == 0 + + + x = [] + y = [] + data[:data].each_index {|i| + if i%2 == 0 + arr = ParseDate.parsedate( data[:data][i] ) + t = Time.local( *arr[0,6].compact ) + x << t.to_i + else + y << data[:data][i] + end + } + sort( x, y ) + data[:data] = [x,y] + @data << data + 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 + get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } + end + + private + 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 diff --git a/groups/lib/SVG/LICENSE.txt b/groups/lib/SVG/LICENSE.txt new file mode 100644 index 000000000..2b945d276 --- /dev/null +++ b/groups/lib/SVG/LICENSE.txt @@ -0,0 +1,57 @@ +SVG::Graph is copyrighted free software by Sean Russell . +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. + diff --git a/groups/lib/ar_condition.rb b/groups/lib/ar_condition.rb new file mode 100644 index 000000000..30c5572ed --- /dev/null +++ b/groups/lib/ar_condition.rb @@ -0,0 +1,41 @@ +# 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 diff --git a/groups/lib/diff.rb b/groups/lib/diff.rb new file mode 100644 index 000000000..646f91bae --- /dev/null +++ b/groups/lib/diff.rb @@ -0,0 +1,280 @@ +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 + +module Diffable + def diff(b) + 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 diff --git a/groups/lib/redcloth.rb b/groups/lib/redcloth.rb new file mode 100644 index 000000000..7e0c71839 --- /dev/null +++ b/groups/lib/redcloth.rb @@ -0,0 +1,1140 @@ +# 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: +# +#
This is a link +# +# == 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: +# +# Textist +# +# With a link: +# +# !/common/textist.gif(Textist)!:http://textism.com +# +# Will become: +# +# Textist +# +# == 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: +# +# ACLU +# +# == 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 RedCloth < 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 fell!" + # + 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 bold man", [:filter_html] ) + # r.to_html + # #=>"

A <b>bold</b> man

" + # + 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 fell!" + # + 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] + 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 + 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+?))? + ([^\s\-].*?[^\s\-]|\w) + #{rcq} + (?=[[:punct:]]|\s|$)/x + else + /(#{rcq}) + (#{C}) + (?::(\S+))? + ([^\s\-].*?[^\s\-]|\w) + #{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 + [ //, '>' ], # 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(?:[(]([^)]*)[)])/, '\1' ], # 3+ uppercase acronym + [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^\2\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 ) + str.gsub!( '&', '&' ) + str.gsub!( '"', '"' ) if mode != :NoQuotes + str.gsub!( "'", ''' ) if mode == :Quotes + str.gsub!( '<', '<') + str.gsub!( '>', '>') + 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 + 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 << "#{ $1 };" if not filter_styles and + text.sub!( /\{([^}]*)\}/, '' ) + + 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. + split( /\|$/m ). + delete_if { |x| x.empty? }. + each do |row| + + ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m + + cells = [] + #row.split( /\(?!\[\[[^\]])|(?![^\[]\]\])/ ).each do |cell| + row.split( /\|(?![^\[\|]*\]\])/ ).each do |cell| + ctyp = 'd' + ctyp = 'h' if cell =~ /^_/ + + catts = '' + catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/ + + unless cell.strip.empty? + catts = shelve( catts ) if catts + cells << "\t\t\t#{ cell }" + end + end + ratts = shelve( ratts ) if ratts + rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" + end + "\t\n#{ rows.join( "\n" ) }\n\t\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] << "\n\t\n\t" + depth.pop + end + end + if depth.last and depth.last.length == tl.length + lines[line_id - 1] << '' + 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
  • #{ content }" + else + lines[line_id] = "\t\t
  • #{ 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] << "
  • \n\t" + end + end + end + lines.join( "\n" ) + 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 }#{ after }" ) + end + end + + def lT( text ) + text =~ /\#$/ ? 'o' : 'u' + end + + def hard_break( text ) + text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) 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
    #{ blk }
    " + else + blk = "\t

    #{ blk }

    " + 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\n\t\t#{ content }

    \n\t" + end + + def textile_p( tag, atts, cite, content ) + atts = shelve( atts ) if atts + "\t<#{ tag }#{ atts }>#{ content }" + 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 }\"" + content = "#{ num } #{ content }" + atts = shelve( atts ) if atts + "\t#{ content }

    " + 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 }", $' + 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 }\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" ) + "
    \n#{ blk }\n
    \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| + "
    " + 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 }" + + end + end + end + + LINK_RE = / + ([\s\[{(]|[#{PUNCT}])? # $pre + " # start + (#{C}) # $atts + ([^"]+?) # $text + \s? + (?:\(([^)]+?)\)(?="))? # $title + ": + (\S+?) # $url + (\/)? # $slash + ([^\w\/;]*?) # $post + (?=<|\s|$) + /x + + def inline_textile_link( text ) + text.gsub!( LINK_RE ) do |m| + pre,atts,text,title,url,slash,post = $~[1..7] + + url, url_title = check_refs( url ) + title ||= url_title + + atts = pba( atts ) + atts = " href=\"#{ url }#{ slash }\"#{ atts }" + atts << " title=\"#{ title }\"" if title + atts = shelve( atts ) if atts + + external = (url =~ /^https?:\/\//) ? ' class="external"' : '' + + "#{ pre }#{ text }#{ post }" + 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 ) + + "#{ text }" + 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 ) + + "#{ text }" + end + end + + TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/ + MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+?(?:\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 = / + (

    |.|^) # 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] + 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 << "" if href + out << "" + out << "#{ href_a1 }#{ href_a2 }" if href + + if algn + algn = h_align( algn ) + if stln == "

    " + out = "

    #{ out }" + else + out = "#{ stln }

    #{ out }
    " + 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\2\3' ) + text.gsub!( /^ *==([^=]+.*?)==/m, + '\1\2\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)?/, + '\1\2' ) + end + + OFFTAGS = /(code|pre|kbd|notextile)/ + OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\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 ,
     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 
     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 ) unless used_offtags['notextile']
    +                        @pre_list.last << line
    +                        line = ""
    +                    else
    +                        htmlesc( aftertag, :NoQuotes ) if aftertag and not used_offtags['notextile']
    +                        line = ""
    +                        @pre_list << "#{ $3 }#{ aftertag }"
    +                    end
    +                elsif $1 and codepre > 0
    +                    if codepre - used_offtags.length > 0
    +                        htmlesc( line, :NoQuotes ) unless used_offtags['notextile']
    +                        @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 
     content
    +            text.gsub!( // ) { @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 )
    +        ' ' + name + '
    ' + 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!( /]*)>/ ) 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) + + def escape_html_tags(text) + text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' if $3}" } + end +end + diff --git a/groups/lib/redmine.rb b/groups/lib/redmine.rb new file mode 100644 index 000000000..2697e8f5f --- /dev/null +++ b/groups/lib/redmine.rb @@ -0,0 +1,133 @@ +require 'redmine/access_control' +require 'redmine/menu_manager' +require 'redmine/mime_type' +require 'redmine/core_ext' +require 'redmine/themes' +require 'redmine/plugin' + +begin + require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick) +rescue LoadError + # RMagick is not available +end + +REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git ) + +# 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 :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]}, :require => :member + map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :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}, :public => true + map.permission :add_issues, {:issues => :new} + map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]} + map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} + map.permission :add_issue_notes, {:issues => :edit} + 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, :projects => :gantt + map.permission :view_calendar, :projects => :calendar + 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 + 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, :destroy_attachment]}, :require => :loggedin + map.permission :view_documents, :documents => [:index, :show, :download] + end + + map.project_module :files do |map| + map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin + map.permission :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, :history, :diff, :annotate, :special] + map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment] + end + + map.project_module :repository do |map| + map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member + map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph] + map.permission :view_changesets, :repositories => [:show, :revisions, :revision] + 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]} + map.permission :edit_messages, {:messages => :edit}, :require => :member + map.permission :delete_messages, {:messages => :destroy}, :require => :member + end +end + +Redmine::MenuManager.map :top_menu do |menu| + menu.push :home, :home_url, :html => { :class => 'home' } + menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? } + menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' } + menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? } + menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' } +end + +Redmine::MenuManager.map :account_menu do |menu| + menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? } + menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? } + menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? } + menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? } +end + +Redmine::MenuManager.map :application_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' } +end diff --git a/groups/lib/redmine/access_control.rb b/groups/lib/redmine/access_control.rb new file mode 100644 index 000000000..f5b25f277 --- /dev/null +++ b/groups/lib/redmine/access_control.rb @@ -0,0 +1,112 @@ +# 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 + + def allowed_actions(permission_name) + perm = @permissions.detect {|p| p.name == 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 + end + + def public? + @public + end + + def require_member? + @require && @require == :member + end + + def require_loggedin? + @require && (@require == :member || @require == :loggedin) + end + end + end +end diff --git a/groups/lib/redmine/access_keys.rb b/groups/lib/redmine/access_keys.rb new file mode 100644 index 000000000..96029a6fc --- /dev/null +++ b/groups/lib/redmine/access_keys.rb @@ -0,0 +1,31 @@ +# 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 diff --git a/groups/lib/redmine/core_ext.rb b/groups/lib/redmine/core_ext.rb new file mode 100644 index 000000000..573313e74 --- /dev/null +++ b/groups/lib/redmine/core_ext.rb @@ -0,0 +1 @@ +Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) } diff --git a/groups/lib/redmine/core_ext/string.rb b/groups/lib/redmine/core_ext/string.rb new file mode 100644 index 000000000..ce2646fb9 --- /dev/null +++ b/groups/lib/redmine/core_ext/string.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/string/conversions' + +class String #:nodoc: + include Redmine::CoreExtensions::String::Conversions +end diff --git a/groups/lib/redmine/core_ext/string/conversions.rb b/groups/lib/redmine/core_ext/string/conversions.rb new file mode 100644 index 000000000..7444445b0 --- /dev/null +++ b/groups/lib/redmine/core_ext/string/conversions.rb @@ -0,0 +1,40 @@ +# 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! + unless s =~ %r{^[\d\.,]+$} + # 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!(',', '.') + s.to_f + end + end + end + end +end diff --git a/groups/lib/redmine/default_data/loader.rb b/groups/lib/redmine/default_data/loader.rb new file mode 100644 index 000000000..11bd2a0b4 --- /dev/null +++ b/groups/lib/redmine/default_data/loader.rb @@ -0,0 +1,169 @@ +# 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 GLoc + + 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, + :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, + :edit_wiki_pages, + :delete_wiki_pages, + :add_messages, + :view_files, + :manage_files, + :browse_repository, + :view_changesets] + + reporter = Role.create! :name => l(:default_role_reporter), + :position => 3, + :permissions => [:add_issues, + :add_issue_notes, + :save_queries, + :view_gantt, + :view_calendar, + :log_time, + :view_time_entries, + :comment_news, + :view_documents, + :view_wiki_pages, + :add_messages, + :view_files, + :browse_repository, + :view_changesets] + + Role.non_member.update_attribute :permissions, [:add_issues, + :add_issue_notes, + :save_queries, + :view_gantt, + :view_calendar, + :view_time_entries, + :comment_news, + :view_documents, + :view_wiki_pages, + :add_messages, + :view_files, + :browse_repository, + :view_changesets] + + Role.anonymous.update_attribute :permissions, [:view_gantt, + :view_calendar, + :view_time_entries, + :view_documents, + :view_wiki_pages, + :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) + assigned = IssueStatus.create!(:name => l(:default_issue_status_assigned), :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, assigned, resolved, feedback].each { |os| + [assigned, 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, assigned, 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 + Enumeration.create!(:opt => "DCAT", :name => l(:default_doc_category_user), :position => 1) + Enumeration.create!(:opt => "DCAT", :name => l(:default_doc_category_tech), :position => 2) + + Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_low), :position => 1) + Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_normal), :position => 2, :is_default => true) + Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_high), :position => 3) + Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_urgent), :position => 4) + Enumeration.create!(:opt => "IPRI", :name => l(:default_priority_immediate), :position => 5) + + Enumeration.create!(:opt => "ACTI", :name => l(:default_activity_design), :position => 1) + Enumeration.create!(:opt => "ACTI", :name => l(:default_activity_development), :position => 2) + end + true + end + end + end + end +end diff --git a/groups/lib/redmine/helpers/calendar.rb b/groups/lib/redmine/helpers/calendar.rb new file mode 100644 index 000000000..347f1c5b5 --- /dev/null +++ b/groups/lib/redmine/helpers/calendar.rb @@ -0,0 +1,76 @@ +# 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 GLoc + 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 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 diff --git a/groups/lib/redmine/info.rb b/groups/lib/redmine/info.rb new file mode 100644 index 000000000..0e00e8b85 --- /dev/null +++ b/groups/lib/redmine/info.rb @@ -0,0 +1,10 @@ +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 + end + end +end diff --git a/groups/lib/redmine/menu_manager.rb b/groups/lib/redmine/menu_manager.rb new file mode 100644 index 000000000..af54b41fe --- /dev/null +++ b/groups/lib/redmine/menu_manager.rb @@ -0,0 +1,146 @@ +# 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 'gloc' + +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 + menu_items[controller_name.to_sym][:actions][action_name.to_sym] || + menu_items[controller_name.to_sym][:default] + 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 = [] + 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 + #url = (project && item.url.is_a?(Hash)) ? {item.param => project}.merge(item.url) : (item.url.is_a?(Symbol) ? send(item.url) : item.url) + links << content_tag('li', + link_to(l(item.caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options))) + end + end + links.empty? ? nil : content_tag('ul', links.join("\n")) + end + end + + class << self + def map(menu_name) + mapper = Mapper.new + yield mapper + @items ||= {} + @items[menu_name.to_sym] ||= [] + @items[menu_name.to_sym] += mapper.items + 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 + # 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: the localized string key that is used as the item label + # * html_options: a hash of html options that are passed to link_to + def push(name, url, options={}) + items << MenuItem.new(name, url, options) + end + + def items + @items ||= [] + end + end + + class MenuItem + include GLoc + attr_reader :name, :url, :param, :condition, :html_options + + 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_key = options[:caption] + @html_options = options[:html] || {} + end + + def caption + # check if localized string exists on first render (after GLoc strings are loaded) + @caption ||= (@caption_key || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize)) + end + end + end +end diff --git a/groups/lib/redmine/mime_type.rb b/groups/lib/redmine/mime_type.rb new file mode 100644 index 000000000..dfdfff407 --- /dev/null +++ b/groups/lib/redmine/mime_type.rb @@ -0,0 +1,70 @@ +# 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-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', + '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', + }.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 + + def self.main_mimetype_of(name) + mimetype = of(name) + mimetype.split('/').first if mimetype + end + + # return true if mime-type for name is type/* + # otherwise false + def self.is_type?(type, name) + main_mimetype = main_mimetype_of(name) + type.to_s == main_mimetype + end + end +end diff --git a/groups/lib/redmine/plugin.rb b/groups/lib/redmine/plugin.rb new file mode 100644 index 000000000..36632c13e --- /dev/null +++ b/groups/lib/redmine/plugin.rb @@ -0,0 +1,125 @@ +# 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: + + # Base class for Redmine plugins. + # Plugins are registered using the register class method that acts as the public constructor. + # + # Redmine::Plugin.register :example do + # name 'Example plugin' + # author 'John Smith' + # description 'This is an example plugin for Redmine' + # version '0.0.1' + # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings' + # end + # + # === Plugin attributes + # + # +settings+ is an optional attribute that let the plugin be configurable. + # It must be a hash with the following keys: + # * :default: default value for the plugin settings + # * :partial: path of the configuration partial view, relative to the plugin app/views directory + # Example: + # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings' + # In this example, the settings partial will be found here in the plugin directory: app/views/settings/_settings.rhtml. + # + # When rendered, the plugin settings value is available as the local variable +settings+ + class Plugin + @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, :author, :version, :settings + + # Plugin constructor + def self.register(name, &block) + p = new + p.instance_eval(&block) + Plugin.registered_plugins[name] = p + 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(name, item, url, options={}) + Redmine::MenuManager.map(name) {|menu| menu.push item, url, options} + 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 + + # Returns +true+ if the plugin can be configured. + def configurable? + settings && settings.is_a?(Hash) && !settings[:partial].blank? + end + end +end diff --git a/groups/lib/redmine/scm/adapters/abstract_adapter.rb b/groups/lib/redmine/scm/adapters/abstract_adapter.rb new file mode 100644 index 000000000..2c254d48d --- /dev/null +++ b/groups/lib/redmine/scm/adapters/abstract_adapter.rb @@ -0,0 +1,395 @@ +# 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: + 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 revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + return nil + end + + def diff(path, identifier_from, identifier_to=nil, type="inline") + return nil + end + + def cat(path, identifier=nil) + return nil + end + + def with_leading_slash(path) + path ||= '' + (path[0,1]!="/") ? "/#{path}" : path + end + + def shell_quote(str) + if RUBY_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 + RAILS_DEFAULT_LOGGER + end + + def shellout(cmd, &block) + logger.debug "Shelling out: #{cmd}" if logger && logger.debug? + begin + IO.popen(cmd, "r+") do |io| + io.close_write + block.call(io) if block_given? + end + rescue Errno::ENOENT => e + # The command failed, log it and re-raise + logger.error("SCM command failed: #{cmd}\n with: #{e.message}") + raise CommandFailed.new(e.message) + end + end + end + + class Entries < Array + def sort_by_name + sort {|x,y| + if x.kind == y.kind + x.name <=> y.name + 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 + + 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 + + class DiffTableList < Array + def initialize (diff, type="inline") + diff_table = DiffTable.new type + diff.each do |line| + if line =~ /^(---|\+\+\+) (.*)$/ + self << diff_table if diff_table.length > 1 + diff_table = DiffTable.new type + end + a = diff_table.add_line line + end + self << diff_table unless diff_table.empty? + self + end + end + + # Class for create a 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 + def add_line(line) + unless @parsing + if line =~ /^(---|\+\+\+) (.*)$/ + @file_name = $2 + return false + elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ + @line_num_l = $5.to_i + @line_num_r = $2.to_i + @parsing = true + end + else + if line =~ /^[^\+\-\s@\\]/ + self.delete(self.keys.sort.last) + @parsing = false + return false + elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ + @line_num_l = $5.to_i + @line_num_r = $2.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_left = escapeHTML line[1..-1] + diff.nb_line_left = @line_num_l + diff.type_diff_left = 'diff_in' + @line_num_l += 1 + true + elsif line[0, 1] == "-" + diff = sbs? type, 'remove' + @before = 'remove' + diff.line_right = escapeHTML line[1..-1] + diff.nb_line_right = @line_num_r + diff.type_diff_right = 'diff_out' + @line_num_r += 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 + + 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 diff --git a/groups/lib/redmine/scm/adapters/bazaar_adapter.rb b/groups/lib/redmine/scm/adapters/bazaar_adapter.rb new file mode 100644 index 000000000..2225a627c --- /dev/null +++ b/groups/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -0,0 +1,185 @@ +# 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" + cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 + 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+)$/ + 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, type="inline") + 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 + DiffTableList.new diff, type + 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 diff --git a/groups/lib/redmine/scm/adapters/cvs_adapter.rb b/groups/lib/redmine/scm/adapters/cvs_adapter.rb new file mode 100644 index 000000000..37920b599 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/cvs_adapter.rb @@ -0,0 +1,354 @@ +# 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 " 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 " entries '#{path}' with identifier '#{identifier}'" + path_with_project="#{url}#{with_leading_slash(path)}" + entries = Entries.new + cmd = "#{CVS_BIN} -d #{root_url} rls -ed" + cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier + cmd << " #{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 " 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 << " #{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 + # gsub(/^:.*@[^:]+:\d*/, '') is here to remove :pserver:anonymous@foo.bar: string if present in the url + if /^RCS file: #{Regexp.escape(root_url.gsub(/^:.*@[^:]+:\d*/, ''))}\/#{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, type="inline") + logger.debug " 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} #{path_with_project}" + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + return nil if $? && $?.exitstatus != 0 + DiffTableList.new diff, type + end + + def cat(path, identifier=nil) + identifier = (identifier) ? identifier : "HEAD" + logger.debug " cat path:'#{path}',identifier #{identifier}" + path_with_project="#{url}#{with_leading_slash(path)}" + cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{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 " annotate path:'#{path}',identifier #{identifier}" + path_with_project="#{url}#{with_leading_slash(path)}" + cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{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 + + # 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 diff --git a/groups/lib/redmine/scm/adapters/darcs_adapter.rb b/groups/lib/redmine/scm/adapters/darcs_adapter.rb new file mode 100644 index 000000000..a1d1867b1 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/darcs_adapter.rb @@ -0,0 +1,156 @@ +# 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" + + def initialize(url, root_url=nil, login=nil, password=nil) + @url = url + @root_url = url + end + + def supports_cat? + false + end + + # Get info about the svn 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 \"hash #{identifier}\"" if identifier + cmd << " #{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.sort_by_name + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path = '.' if path.blank? + revisions = Revisions.new + cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output" + cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from + cmd << " --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, type="inline") + path = '*' if path.blank? + cmd = "#{DARCS_BIN} diff --repodir #{@url}" + if identifier_to.nil? + cmd << " --match \"hash #{identifier_from}\"" + else + cmd << " --to-match \"hash #{identifier_from}\"" + cmd << " --from-match \"hash #{identifier_to}\"" + end + cmd << " -u #{path}" + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + return nil if $? && $?.exitstatus != 0 + DiffTableList.new diff, type + end + + private + + def entry_from_xml(element, path_prefix) + Entry.new({:name => element.attributes['name'], + :path => path_prefix + element.attributes['name'], + :kind => element.name == 'file' ? 'file' : 'dir', + :size => nil, + :lastrev => Revision.new({ + :identifier => nil, + :scmid => element.elements['modified'].elements['patch'].attributes['hash'] + }) + }) + 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 \"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("" + io.read + "") + 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 diff --git a/groups/lib/redmine/scm/adapters/git_adapter.rb b/groups/lib/redmine/scm/adapters/git_adapter.rb new file mode 100644 index 000000000..77604f283 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/git_adapter.rb @@ -0,0 +1,256 @@ +# 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" + + # Get the revision of a particuliar file + def get_rev (rev,path) + cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?) + cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if + rev=='latest' or rev.nil? + rev=[] + i=0 + shellout(cmd) do |io| + files=[] + changeset = {} + parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files + + io.each_line do |line| + if line =~ /^commit ([0-9a-f]{40})$/ + key = "commit" + value = $1 + if (parsing_descr == 1 || parsing_descr == 2) + parsing_descr = 0 + rev = Revision.new({:identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => files + }) + changeset = {} + files = [] + end + changeset[:commit] = $1 + elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/ + key = $1 + value = $2 + if key == "Author" + changeset[:author] = value + elsif key == "Date" + 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) && line.chomp.to_s == "" + parsing_descr = 2 + elsif (parsing_descr == 1) + changeset[:description] << line + end + end + rev = Revision.new({:identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => (changeset[:date] ? Time.parse(changeset[:date]) : nil), + :message => changeset[:description], + :paths => files + }) + + end + + get_rev('latest',path) if rev == [] + + return nil if $? && $?.exitstatus != 0 + return rev + end + + + def info + revs = revisions(url,nil,nil,{:limit => 1}) + if revs && revs.any? + Info.new(:root_url => url, :lastrev => revs.first) + else + nil + end + rescue Errno::ENOENT => e + return nil + 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 + entries << Entry.new({:name => name, + :path => (path.empty? ? name : "#{path}/#{name}"), + :kind => ((type == "tree") ? 'dir' : 'file'), + :size => ((type == "tree") ? nil : size), + :lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}")) + + }) unless entries.detect{|entry| entry.name == name} + end + end + end + return nil if $? && $?.exitstatus != 0 + entries.sort_by_name + end + + def revisions(path, identifier_from, identifier_to, options={}) + revisions = Revisions.new + cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw " + cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit] + cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from + cmd << " #{shell_quote identifier_to} " if identifier_to + #cmd << " HEAD " if !identifier_to + 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 + revisions << Revision.new({:identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => files + }) + 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 == "Date" + 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) && line.chomp.to_s == "" + parsing_descr = 2 + elsif (parsing_descr == 1) + changeset[:description] << line[4..-1] + end + end + + revisions << Revision.new({:identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => files + }) if changeset[:commit] + + end + + return nil if $? && $?.exitstatus != 0 + revisions + end + + def diff(path, identifier_from, identifier_to=nil, type="inline") + path ||= '' + if !identifier_to + identifier_to = nil + end + + cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil? + cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil? + 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 + DiffTableList.new diff, type + 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 + diff --git a/groups/lib/redmine/scm/adapters/mercurial_adapter.rb b/groups/lib/redmine/scm/adapters/mercurial_adapter.rb new file mode 100644 index 000000000..6f42dda06 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -0,0 +1,199 @@ +# 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" + + 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(path)} locate" + cmd << " -r #{identifier.to_i}" if identifier + cmd << " " + shell_quote('glob:**') + shellout(cmd) do |io| + io.each_line do |line| + e = line.chomp.split(%r{[\/\\]}) + entries << Entry.new({:name => e.first, + :path => (path.empty? ? e.first : "#{path}/#{e.first}"), + :kind => (e.size > 1 ? 'dir' : 'file'), + :lastrev => Revision.new + }) unless entries.detect{|entry| entry.name == e.first} + end + end + return nil if $? && $?.exitstatus != 0 + entries.sort_by_name + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + revisions = Revisions.new + cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log" + 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] + shellout(cmd) do |io| + changeset = {} + parsing_descr = false + line_feeds = 0 + + io.each_line do |line| + if line =~ /^(\w+):\s*(.*)$/ + key = $1 + value = $2 + if parsing_descr && line_feeds > 1 + parsing_descr = false + revisions << build_revision_from_changeset(changeset) + changeset = {} + end + if !parsing_descr + changeset.store key.to_sym, value + if $1 == "description" + parsing_descr = true + line_feeds = 0 + next + end + end + end + if parsing_descr + changeset[:description] << line + line_feeds += 1 if line.chomp.empty? + end + end + # Add the last changeset if there is one left + revisions << build_revision_from_changeset(changeset) if changeset[:date] + end + return nil if $? && $?.exitstatus != 0 + revisions + end + + def diff(path, identifier_from, identifier_to=nil, type="inline") + 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 + DiffTableList.new diff, type + end + + def cat(path, identifier=nil) + cmd = "#{HG_BIN} -R #{target('')} cat" + cmd << " -r #{identifier.to_i}" if identifier + 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.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 + + private + + # Builds a revision objet from the changeset returned by hg command + def build_revision_from_changeset(changeset) + rev_id = changeset[:changeset].to_s.split(':').first.to_i + + # Changes + paths = (rev_id == 0) ? + # Can't get changes for revision 0 with hg status + changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} : + status(rev_id) + + Revision.new({:identifier => rev_id, + :scmid => changeset[:changeset].to_s.split(':').last, + :author => changeset[:user], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => paths + }) + end + + # Returns the file changes for a given revision + def status(rev_id) + cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}" + result = [] + shellout(cmd) do |io| + io.each_line do |line| + action, file = line.chomp.split + next unless action && file + file.gsub!("\\", "/") + case action + when 'R' + result << { :action => 'D' , :path => "/#{file}" } + else + result << { :action => action, :path => "/#{file}" } + end + end + end + result + end + end + end + end +end diff --git a/groups/lib/redmine/scm/adapters/subversion_adapter.rb b/groups/lib/redmine/scm/adapters/subversion_adapter.rb new file mode 100644 index 000000000..40c7eb3f1 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/subversion_adapter.rb @@ -0,0 +1,184 @@ +# 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 SubversionAdapter < AbstractAdapter + + # SVN executable name + SVN_BIN = "svn" + + # Get info about the svn repository + def info + cmd = "#{SVN_BIN} info --xml #{target('')}" + cmd << credentials_string + info = nil + shellout(cmd) do |io| + begin + doc = REXML::Document.new(io) + #root_url = doc.elements["info/entry/repository/root"].text + info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text, + :lastrev => Revision.new({ + :identifier => doc.elements["info/entry/commit"].attributes['revision'], + :time => Time.parse(doc.elements["info/entry/commit/date"].text).localtime, + :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "") + }) + }) + rescue + end + end + return nil if $? && $?.exitstatus != 0 + info + rescue CommandFailed + return nil + end + + # Returns an Entries collection + # or nil if the given path doesn't exist in the repository + def entries(path=nil, identifier=nil) + path ||= '' + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + entries = Entries.new + cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}" + cmd << credentials_string + shellout(cmd) do |io| + output = io.read + begin + doc = REXML::Document.new(output) + doc.elements.each("lists/list/entry") do |entry| + entries << Entry.new({:name => entry.elements['name'].text, + :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text), + :kind => entry.attributes['kind'], + :size => (entry.elements['size'] and entry.elements['size'].text).to_i, + :lastrev => Revision.new({ + :identifier => entry.elements['commit'].attributes['revision'], + :time => Time.parse(entry.elements['commit'].elements['date'].text).localtime, + :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "") + }) + }) + end + rescue Exception => e + logger.error("Error parsing svn output: #{e.message}") + logger.error("Output was:\n #{output}") + end + end + return nil if $? && $?.exitstatus != 0 + logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? + entries.sort_by_name + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path ||= '' + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1 + revisions = Revisions.new + cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}" + cmd << credentials_string + cmd << " --verbose " if options[:with_paths] + cmd << ' ' + target(path) + shellout(cmd) do |io| + begin + doc = REXML::Document.new(io) + doc.elements.each("log/logentry") do |logentry| + paths = [] + logentry.elements.each("paths/path") do |path| + paths << {:action => path.attributes['action'], + :path => path.text, + :from_path => path.attributes['copyfrom-path'], + :from_revision => path.attributes['copyfrom-rev'] + } + end + paths.sort! { |x,y| x[:path] <=> y[:path] } + + revisions << Revision.new({:identifier => logentry.attributes['revision'], + :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 + end + end + return nil if $? && $?.exitstatus != 0 + revisions + end + + def diff(path, identifier_from, identifier_to=nil, type="inline") + path ||= '' + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : '' + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) + + cmd = "#{SVN_BIN} diff -r " + cmd << "#{identifier_to}:" + cmd << "#{identifier_from}" + cmd << " #{target(path)}@#{identifier_from}" + cmd << credentials_string + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + return nil if $? && $?.exitstatus != 0 + DiffTableList.new diff, type + end + + def cat(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}" + cmd << credentials_string + cat = nil + shellout(cmd) do |io| + io.binmode + cat = io.read + end + return nil if $? && $?.exitstatus != 0 + cat + end + + def annotate(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}" + cmd << credentials_string + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} + blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip)) + end + end + return nil if $? && $?.exitstatus != 0 + blame + end + + private + + def credentials_string + str = '' + str << " --username #{shell_quote(@login)}" unless @login.blank? + str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank? + str + end + end + end + end +end diff --git a/groups/lib/redmine/themes.rb b/groups/lib/redmine/themes.rb new file mode 100644 index 000000000..d0c90e3a9 --- /dev/null +++ b/groups/lib/redmine/themes.rb @@ -0,0 +1,76 @@ +# 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 diff --git a/groups/lib/redmine/version.rb b/groups/lib/redmine/version.rb new file mode 100644 index 000000000..81006fe2d --- /dev/null +++ b/groups/lib/redmine/version.rb @@ -0,0 +1,35 @@ +require 'rexml/document' + +module Redmine + module VERSION #:nodoc: + MAJOR = 0 + MINOR = 7 + TINY = '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 + STRING = [MAJOR, MINOR, TINY, REVISION].compact.join('.') + + def self.to_s; STRING end + end +end diff --git a/groups/lib/redmine/wiki_formatting.rb b/groups/lib/redmine/wiki_formatting.rb new file mode 100644 index 000000000..79da2a38a --- /dev/null +++ b/groups/lib/redmine/wiki_formatting.rb @@ -0,0 +1,168 @@ +# 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 'redcloth' +require 'coderay' + +module Redmine + module WikiFormatting + + private + + class TextileFormatter < RedCloth + + # auto_link rule after textile rules so that it doesn't break !image_url! tags + RULES = [:textile, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] + + def initialize(*args) + super + self.hard_breaks=true + self.no_span_caps=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. + # http://code.whytheluckystiff.net/redcloth/changeset/128 + def hard_break( text ) + text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
    " ) if hard_breaks + end + + # Patch to add code highlighting support to RedCloth + def smooth_offtags( text ) + unless @pre_list.empty? + ## replace
     content
    +          text.gsub!(//) do
    +            content = @pre_list[$1.to_i]
    +            if content.match(/\s?(.+)/m)
    +              content = "" + 
    +                CodeRay.scan($2, $1).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)
    +        if tag =~ /^h(\d)$/
    +          @toc << [$1.to_i, content]
    +        end
    +        content = "" + content
    +        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!(/

    \{\{([<>]?)toc\}\}<\/p>/i) do + div_class = 'toc' + div_class << ' right' if $1 == '>' + div_class << ' left' if $1 == '<' + out = "

    " + @toc.each_with_index do |heading, index| + # remove wiki links from the item + toc_item = heading.last.gsub(/(\[\[|\]\])/, '') + out << "#{toc_item}" + end + out << '
    ' + 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 + "
    Error executing the #{macro} macro (#{e})
    " + 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 + (?: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 =~ /=]?/ + # don't replace URL's that are already linked + # and URL's prefixed with ! !> !< != (textile images) + all + else + %(#{leading}#{proto + url}#{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 + text = $1 + %{} + end + end + end + + public + + def self.to_html(text, options = {}, &block) + TextileFormatter.new(text).to_html(&block) + end + end +end diff --git a/groups/lib/redmine/wiki_formatting/macros.rb b/groups/lib/redmine/wiki_formatting/macros.rb new file mode 100644 index 000000000..0848aee4e --- /dev/null +++ b/groups/lib/redmine/wiki_formatting/macros.rb @@ -0,0 +1,101 @@ +# 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 + 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 "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| + project = @project + title = args.first.to_s + if title =~ %r{^([^\:]+)\:(.*)$} + project_identifier, title = $1, $2 + project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier) + end + raise 'Unknow project' unless project && User.current.allowed_to?(:view_wiki_pages, project) + raise 'No wiki for this project' unless !project.wiki.nil? + page = project.wiki.find_page(title) + raise "Page #{args.first} doesn't exist" unless page && page.content + @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 diff --git a/groups/lib/tabular_form_builder.rb b/groups/lib/tabular_form_builder.rb new file mode 100644 index 000000000..5b331fe3f --- /dev/null +++ b/groups/lib/tabular_form_builder.rb @@ -0,0 +1,51 @@ +# 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 GLoc + + def initialize(object_name, object, template, options, proc) + set_language_if_valid options.delete(:lang) + @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + end + + (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector| + src = <<-END_SRC + def #{selector}(field, options = {}) + return super if options.delete :no_label + label_text = l(options[:label]) if options[:label] + label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required) + label = @template.content_tag("label", label_text, + :class => (@object && @object.errors[field] ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + label + super + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + def select(field, choices, options = {}, html_options = {}) + label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") + label = @template.content_tag("label", label_text, + :class => (@object && @object.errors[field] ? "error" : nil), + :for => (@object_name.to_s + "_" + field.to_s)) + label + super + end +end diff --git a/groups/lib/tasks/deprecated.rake b/groups/lib/tasks/deprecated.rake new file mode 100644 index 000000000..dca43ddc7 --- /dev/null +++ b/groups/lib/tasks/deprecated.rake @@ -0,0 +1,9 @@ +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" diff --git a/groups/lib/tasks/extract_fixtures.rake b/groups/lib/tasks/extract_fixtures.rake new file mode 100644 index 000000000..49834e5ab --- /dev/null +++ b/groups/lib/tasks/extract_fixtures.rake @@ -0,0 +1,24 @@ +desc 'Create YAML test fixtures from data in an existing database. +Defaults to development database. Set RAILS_ENV to override.' + +task :extract_fixtures => :environment do + sql = "SELECT * FROM %s" + skip_tables = ["schema_info"] + ActiveRecord::Base.establish_connection + (ActiveRecord::Base.connection.tables - skip_tables).each do |table_name| + i = "000" + File.open("#{RAILS_ROOT}/#{table_name}.yml", 'w' ) do |file| + data = ActiveRecord::Base.connection.select_all(sql % table_name) + file.write data.inject({}) { |hash, record| + + # cast extracted values + ActiveRecord::Base.connection.columns(table_name).each { |col| + record[col.name] = col.type_cast(record[col.name]) if record[col.name] + } + + hash["#{table_name}_#{i.succ!}"] = record + hash + }.to_yaml + end + end +end \ No newline at end of file diff --git a/groups/lib/tasks/fetch_changesets.rake b/groups/lib/tasks/fetch_changesets.rake new file mode 100644 index 000000000..681032bd6 --- /dev/null +++ b/groups/lib/tasks/fetch_changesets.rake @@ -0,0 +1,24 @@ +# 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. + +desc 'Fetch changesets from the repositories' + +namespace :redmine do + task :fetch_changesets => :environment do + Repository.fetch_changesets + end +end diff --git a/groups/lib/tasks/load_default_data.rake b/groups/lib/tasks/load_default_data.rake new file mode 100644 index 000000000..6ddd1fb97 --- /dev/null +++ b/groups/lib/tasks/load_default_data.rake @@ -0,0 +1,29 @@ +desc 'Load Redmine default configuration data' + +namespace :redmine do + task :load_default_data => :environment do + include GLoc + set_language_if_valid('en') + puts + + while true + print "Select language: " + print GLoc.valid_languages.sort {|x,y| x.to_s <=> y.to_s }.join(", ") + print " [#{GLoc.current_language}] " + lang = STDIN.gets.chomp! + break if lang.empty? + break if set_language_if_valid(lang) + puts "Unknown language!" + end + + puts "====================================" + + begin + Redmine::DefaultData::Loader.load(current_language) + puts "Default configuration data loaded." + rescue => error + puts "Error: " + error + puts "Default configuration data was not loaded." + end + end +end diff --git a/groups/lib/tasks/migrate_from_mantis.rake b/groups/lib/tasks/migrate_from_mantis.rake new file mode 100644 index 000000000..bf3c32ccd --- /dev/null +++ b/groups/lib/tasks/migrate_from_mantis.rake @@ -0,0 +1,505 @@ +# 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. + +desc 'Mantis migration script' + +require 'active_record' +require 'iconv' +require 'pp' + +namespace :redmine do +task :migrate_from_mantis => :environment do + + module MantisMigrate + + 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 = {10 => DEFAULT_STATUS, # new + 20 => feedback_status, # feedback + 30 => DEFAULT_STATUS, # acknowledged + 40 => DEFAULT_STATUS, # confirmed + 50 => assigned_status, # assigned + 80 => resolved_status, # resolved + 90 => closed_status # closed + } + + priorities = Enumeration.get_values('IPRI') + DEFAULT_PRIORITY = priorities[2] + PRIORITY_MAPPING = {10 => priorities[1], # none + 20 => priorities[1], # low + 30 => priorities[2], # normal + 40 => priorities[3], # high + 50 => priorities[4], # urgent + 60 => priorities[5] # immediate + } + + TRACKER_BUG = Tracker.find_by_position(1) + TRACKER_FEATURE = Tracker.find_by_position(2) + + roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC') + manager_role = roles[0] + developer_role = roles[1] + DEFAULT_ROLE = roles.last + ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer + 25 => DEFAULT_ROLE, # reporter + 40 => DEFAULT_ROLE, # updater + 55 => developer_role, # developer + 70 => manager_role, # manager + 90 => manager_role # administrator + } + + CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String + 1 => 'int', # Numeric + 2 => 'int', # Float + 3 => 'list', # Enumeration + 4 => 'string', # Email + 5 => 'bool', # Checkbox + 6 => 'list', # List + 7 => 'list', # Multiselection list + 8 => 'date', # Date + } + + RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to + 2 => IssueRelation::TYPE_RELATES, # parent of + 3 => IssueRelation::TYPE_RELATES, # child of + 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of + 4 => IssueRelation::TYPE_DUPLICATES # has duplicate + } + + class MantisUser < ActiveRecord::Base + set_table_name :mantis_user_table + + def firstname + @firstname = realname.blank? ? username : realname.split.first[0..29] + @firstname.gsub!(/[^\w\s\'\-]/i, '') + @firstname + end + + def lastname + @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29] + @lastname.gsub!(/[^\w\s\'\-]/i, '') + @lastname = '-' if @lastname.blank? + @lastname + end + + def email + if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) && + !User.find_by_mail(read_attribute(:email)) + @email = read_attribute(:email) + else + @email = "#{username}@foo.bar" + end + end + + def username + read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-') + end + end + + class MantisProject < ActiveRecord::Base + set_table_name :mantis_project_table + has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id + has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id + has_many :news, :class_name => "MantisNews", :foreign_key => :project_id + has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id + + def name + read_attribute(:name)[0..29] + end + + def identifier + read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-') + end + end + + class MantisVersion < ActiveRecord::Base + set_table_name :mantis_project_version_table + + def version + read_attribute(:version)[0..29] + end + + def description + read_attribute(:description)[0..254] + end + end + + class MantisCategory < ActiveRecord::Base + set_table_name :mantis_project_category_table + end + + class MantisProjectUser < ActiveRecord::Base + set_table_name :mantis_project_user_list_table + end + + class MantisBug < ActiveRecord::Base + set_table_name :mantis_bug_table + belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id + has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id + has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id + has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id + end + + class MantisBugText < ActiveRecord::Base + set_table_name :mantis_bug_text_table + + # Adds Mantis steps_to_reproduce and additional_information fields + # to description if any + def full_description + full_description = description + full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank? + full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank? + full_description + end + end + + class MantisBugNote < ActiveRecord::Base + set_table_name :mantis_bugnote_table + belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id + belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id + end + + class MantisBugNoteText < ActiveRecord::Base + set_table_name :mantis_bugnote_text_table + end + + class MantisBugFile < ActiveRecord::Base + set_table_name :mantis_bug_file_table + + def size + filesize + end + + def original_filename + MantisMigrate.encode(filename) + end + + def content_type + file_type + end + + def read + content + end + end + + class MantisBugRelationship < ActiveRecord::Base + set_table_name :mantis_bug_relationship_table + end + + class MantisBugMonitor < ActiveRecord::Base + set_table_name :mantis_bug_monitor_table + end + + class MantisNews < ActiveRecord::Base + set_table_name :mantis_news_table + end + + class MantisCustomField < ActiveRecord::Base + set_table_name :mantis_custom_field_table + set_inheritance_column :none + has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id + has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id + + def format + read_attribute :type + end + + def name + read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-') + end + end + + class MantisCustomFieldProject < ActiveRecord::Base + set_table_name :mantis_custom_field_project_table + end + + class MantisCustomFieldString < ActiveRecord::Base + set_table_name :mantis_custom_field_string_table + end + + + def self.migrate + + # Users + print "Migrating users" + User.delete_all "login <> 'admin'" + users_map = {} + users_migrated = 0 + MantisUser.find(:all).each do |user| + u = User.new :firstname => encode(user.firstname), + :lastname => encode(user.lastname), + :mail => user.email, + :last_login_on => user.last_visit + u.login = user.username + u.password = 'mantis' + u.status = User::STATUS_LOCKED if user.enabled != 1 + u.admin = true if user.access_level == 90 + next unless u.save! + users_migrated += 1 + users_map[user.id] = u.id + print '.' + end + puts + + # Projects + print "Migrating projects" + Project.destroy_all + projects_map = {} + versions_map = {} + categories_map = {} + MantisProject.find(:all).each do |project| + p = Project.new :name => encode(project.name), + :description => encode(project.description) + p.identifier = project.identifier + next unless p.save + projects_map[project.id] = p.id + p.enabled_module_names = ['issue_tracking', 'news', 'wiki'] + p.trackers << TRACKER_BUG + p.trackers << TRACKER_FEATURE + print '.' + + # Project members + project.members.each do |member| + m = Member.new :user => User.find_by_id(users_map[member.user_id]), + :role => ROLE_MAPPING[member.access_level] || DEFAULT_ROLE + m.project = p + m.save + end + + # Project versions + project.versions.each do |version| + v = Version.new :name => encode(version.version), + :description => encode(version.description), + :effective_date => version.date_order.to_date + v.project = p + v.save + versions_map[version.id] = v.id + end + + # Project categories + project.categories.each do |category| + g = IssueCategory.new :name => category.category[0,30] + g.project = p + g.save + categories_map[category.category] = g.id + end + end + puts + + # Bugs + print "Migrating bugs" + Issue.destroy_all + issues_map = {} + keep_bug_ids = (Issue.count == 0) + MantisBug.find(:all, :order => 'id ASC').each do |bug| + next unless projects_map[bug.project_id] && users_map[bug.reporter_id] + i = Issue.new :project_id => projects_map[bug.project_id], + :subject => encode(bug.summary), + :description => encode(bug.bug_text.full_description), + :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY, + :created_on => bug.date_submitted, + :updated_on => bug.last_updated + i.author = User.find_by_id(users_map[bug.reporter_id]) + i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank? + i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank? + i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS + i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG) + i.id = bug.id if keep_bug_ids + next unless i.save + issues_map[bug.id] = i.id + print '.' + + # Assignee + # Redmine checks that the assignee is a project member + if (bug.handler_id && users_map[bug.handler_id]) + i.assigned_to = User.find_by_id(users_map[bug.handler_id]) + i.save_with_validation(false) + end + + # Bug notes + bug.bug_notes.each do |note| + next unless users_map[note.reporter_id] + n = Journal.new :notes => encode(note.bug_note_text.note), + :created_on => note.date_submitted + n.user = User.find_by_id(users_map[note.reporter_id]) + n.journalized = i + n.save + end + + # Bug files + bug.bug_files.each do |file| + a = Attachment.new :created_on => file.date_added + a.file = file + a.author = User.find :first + a.container = i + a.save + end + + # Bug monitors + bug.bug_monitors.each do |monitor| + next unless users_map[monitor.user_id] + i.add_watcher(User.find_by_id(users_map[monitor.user_id])) + end + 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 + + # Bug relationships + print "Migrating bug relations" + MantisBugRelationship.find(:all).each do |relation| + next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id] + r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type] + r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id]) + r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id]) + pp r unless r.save + print '.' + end + puts + + # News + print "Migrating news" + News.destroy_all + MantisNews.find(:all, :conditions => 'project_id > 0').each do |news| + next unless projects_map[news.project_id] + n = News.new :project_id => projects_map[news.project_id], + :title => encode(news.headline[0..59]), + :description => encode(news.body), + :created_on => news.date_posted + n.author = User.find_by_id(users_map[news.poster_id]) + n.save + print '.' + end + puts + + # Custom fields + print "Migrating custom fields" + IssueCustomField.destroy_all + MantisCustomField.find(:all).each do |field| + f = IssueCustomField.new :name => field.name[0..29], + :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format], + :min_length => field.length_min, + :max_length => field.length_max, + :regexp => field.valid_regexp, + :possible_values => field.possible_values.split('|'), + :is_required => field.require_report? + next unless f.save + print '.' + + # Trackers association + f.trackers = Tracker.find :all + + # Projects association + field.projects.each do |project| + f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id] + end + + # Values + field.values.each do |value| + v = CustomValue.new :custom_field_id => f.id, + :value => value.value + v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id] + v.save + end unless f.new_record? + end + puts + + puts + puts "Users: #{users_migrated}/#{MantisUser.count}" + puts "Projects: #{Project.count}/#{MantisProject.count}" + puts "Memberships: #{Member.count}/#{MantisProjectUser.count}" + puts "Versions: #{Version.count}/#{MantisVersion.count}" + puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}" + puts "Bugs: #{Issue.count}/#{MantisBug.count}" + puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}" + puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}" + puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}" + puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}" + puts "News: #{News.count}/#{MantisNews.count}" + puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}" + end + + def self.encoding(charset) + @ic = Iconv.new('UTF-8', charset) + rescue Iconv::InvalidEncoding + return false + end + + def self.establish_connection(params) + constants.each do |const| + klass = const_get(const) + next unless klass.respond_to? 'establish_connection' + klass.establish_connection params + end + end + + 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: Your Redmine data will be deleted during this process." + print "Are you sure you want to continue ? [y/N] " + break unless STDIN.gets.match(/^y$/i) + + # Default Mantis database settings + db_params = {:adapter => 'mysql', + :database => 'bugtracker', + :host => 'localhost', + :username => 'root', + :password => '' } + + puts + puts "Please enter settings for your Mantis database" + [:adapter, :host, :database, :username, :password].each do |param| + print "#{param} [#{db_params[param]}]: " + value = STDIN.gets.chomp! + db_params[param] = value unless value.blank? + end + + while true + print "encoding [UTF-8]: " + encoding = STDIN.gets.chomp! + encoding = 'UTF-8' if encoding.blank? + break if MantisMigrate.encoding encoding + puts "Invalid encoding!" + end + puts + + # Make sure bugs can refer bugs in other projects + Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations' + + MantisMigrate.establish_connection db_params + MantisMigrate.migrate +end +end diff --git a/groups/lib/tasks/migrate_from_trac.rake b/groups/lib/tasks/migrate_from_trac.rake new file mode 100644 index 000000000..7fe1f09ac --- /dev/null +++ b/groups/lib/tasks/migrate_from_trac.rake @@ -0,0 +1,645 @@ +# 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.get_values('IPRI') + 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 + + def due + if read_attribute(:due) > 0 + Time.at(read_attribute(:due)).to_date + else + nil + end + end + end + + class TracTicketCustom < ActiveRecord::Base + set_table_name :ticket_custom + end + + class TracAttachment < ActiveRecord::Base + set_table_name :attachment + set_inheritance_column :none + + def time; Time.at(read_attribute(:time)) end + + def original_filename + filename + end + + def content_type + Redmine::MimeType.of(filename) || '' + end + + def exist? + File.file? trac_fullpath + end + + def read + File.open("#{trac_fullpath}", 'rb').read + end + + 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", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'" + 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", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'" + + 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 + + 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')] + mail = "#{mail}@foo.bar" unless mail.include?("@") + u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'), + :lastname => '-', + :mail => mail.gsub(/[^-@a-z0-9\.]/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, :role => 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}"} + # 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(',./?;|:')}]]"} + + # 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 + # Preformatted blocks + text = text.gsub(/\{\{\{/, '
    ')
    +        text = text.gsub(/\}\}\}/, '
    ') + # 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 + + # 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 + v = Version.new :project => @target_project, + :name => encode(milestone.name[0, limit_for(Version, 'name')]), + :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]), + :effective_date => milestone.due + 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(:all, :order => 'id ASC').each 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.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank? + i.id = ticket.id unless Issue.exists?(ticket.id) + next unless Time.fake(ticket.changetime) { i.save } + TICKET_MAP[ticket.id] = i.id + migrated_tickets += 1 + + # Owner + 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? + a = Attachment.new :created_on => attachment.time + a.file = attachment + a.author = find_or_create_user(attachment.author) + a.container = i + migrated_ticket_attachments += 1 if a.save + end + + # Custom fields + ticket.customs.each do |custom| + next if custom_field_map[custom.name].nil? + v = CustomValue.new :custom_field => custom_field_map[custom.name], + :value => custom.value + v.customized = i + next unless v.save + migrated_custom_values += 1 + end + 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" + @target_project.wiki.destroy if @target_project.wiki + @target_project.reload + wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart') + wiki_edit_count = 0 + 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 + a = Attachment.new :created_on => attachment.time + a.file = attachment + a.author = find_or_create_user(attachment.author) + 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 + + TracMigrate.migrate + end +end diff --git a/groups/lib/tasks/migrate_plugins.rake b/groups/lib/tasks/migrate_plugins.rake new file mode 100644 index 000000000..61df9c3f0 --- /dev/null +++ b/groups/lib/tasks/migrate_plugins.rake @@ -0,0 +1,15 @@ +namespace :db do + desc 'Migrates installed plugins.' + task :migrate_plugins => :environment do + if Rails.respond_to?('plugins') + Rails.plugins.each do |plugin| + next unless plugin.respond_to?('migrate') + puts "Migrating #{plugin.name}..." + plugin.migrate + end + else + puts "Undefined method plugins for Rails!" + puts "Make sure engines plugin is installed." + end + end +end diff --git a/groups/log/delete.me b/groups/log/delete.me new file mode 100644 index 000000000..18beddaa8 --- /dev/null +++ b/groups/log/delete.me @@ -0,0 +1 @@ +default directory for uploaded files \ No newline at end of file diff --git a/groups/public/.htaccess b/groups/public/.htaccess new file mode 100644 index 000000000..3d3fb88bc --- /dev/null +++ b/groups/public/.htaccess @@ -0,0 +1,55 @@ +# General Apache options + + AddHandler fastcgi-script .fcgi + + + AddHandler fcgid-script .fcgi + + + AddHandler cgi-script .cgi + +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f + + RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] + + + RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] + + + RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

    Application error

    Rails application failed to start properly" \ No newline at end of file diff --git a/groups/public/404.html b/groups/public/404.html new file mode 100644 index 000000000..ddf424b09 --- /dev/null +++ b/groups/public/404.html @@ -0,0 +1,23 @@ + + +redMine 404 error + + +

    Page not found

    +

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

    +

    Back

    + + \ No newline at end of file diff --git a/groups/public/500.html b/groups/public/500.html new file mode 100644 index 000000000..93eb0f128 --- /dev/null +++ b/groups/public/500.html @@ -0,0 +1,24 @@ + + +redMine 500 error + + +

    Internal error

    +

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

    +

    Back

    + + \ No newline at end of file diff --git a/groups/public/dispatch.cgi.example b/groups/public/dispatch.cgi.example new file mode 100755 index 000000000..9730473f2 --- /dev/null +++ b/groups/public/dispatch.cgi.example @@ -0,0 +1,10 @@ +#!/usr/bin/ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/groups/public/dispatch.fcgi.example b/groups/public/dispatch.fcgi.example new file mode 100755 index 000000000..f934b3002 --- /dev/null +++ b/groups/public/dispatch.fcgi.example @@ -0,0 +1,24 @@ +#!/usr/bin/ruby +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/groups/public/dispatch.rb.example b/groups/public/dispatch.rb.example new file mode 100755 index 000000000..9730473f2 --- /dev/null +++ b/groups/public/dispatch.rb.example @@ -0,0 +1,10 @@ +#!/usr/bin/ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/groups/public/favicon.ico b/groups/public/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/groups/public/help/wiki_syntax.html b/groups/public/help/wiki_syntax.html new file mode 100644 index 000000000..6a0e10022 --- /dev/null +++ b/groups/public/help/wiki_syntax.html @@ -0,0 +1,66 @@ + + + + + +Wiki formatting + + + + +

    Wiki Syntax Quick Reference

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Font Styles
    Strong*Strong*Strong
    Italic_Italic_Italic
    Underline+Underline+Underline
    Deleted-Deleted-Deleted
    ??Quote??Quote
    Inline Code@Inline Code@Inline Code
    Preformatted text<pre>
     lines
     of code
    </pre>
    +
    + lines
    + of code
    +
    +
    Lists
    Unordered list* Item 1
    * Item 2
    • Item 1
    • Item 2
    Ordered list# Item 1
    # Item 2
    1. Item 1
    2. Item 2
    Headings
    Heading 1h1. Title 1

    Title 1

    Heading 2h2. Title 2

    Title 2

    Heading 3h3. Title 3

    Title 3

    Links
    http://foo.barhttp://foo.bar
    "Foo":http://foo.barFoo
    Redmine links
    Link to a Wiki page[[Wiki page]]Wiki page
    Issue #12Issue #12
    Revision r43Revision r43
    commit:"f30e13e43"f30e13e4
    source:some/filesource:some/file
    Inline images
    Image!image_url!
    !attached_image!
    + +

    More Information

    + + + diff --git a/groups/public/help/wiki_syntax_detailed.html b/groups/public/help/wiki_syntax_detailed.html new file mode 100644 index 000000000..6e23594f6 --- /dev/null +++ b/groups/public/help/wiki_syntax_detailed.html @@ -0,0 +1,352 @@ + + + +RedmineWikiFormatting + + + + + +

    Wiki formatting

    + + + +

    Links

    + + +

    Redmine links

    + + +

    Redmine allows hyperlinking between issues, changesets and wiki pages from anywhere wiki formatting is used.

    + + +
      +
    • Link to an issue: #124 (displays #124, link is striked-through if the issue is closed)
    • +
    • Link to a changeset: r758 (displays r758)
    • +
    • Link to a changeset with a non-numeric hash: commit:"c6f4d0fd" (displays c6f4d0fd). Added in r1236.
    • +
    + + +

    Wiki links:

    + + +
      +
    • [[Guide]] displays a link to the page named 'Guide': Guide
    • +
    • [[Guide|User manual]] displays a link to the same page but with a different text: User manual
    • +
    + + +

    You can also link to pages of an other project wiki:

    + + +
      +
    • [[sandbox:some page]] displays a link to the page named 'Some page' of the Sandbox wiki
    • +
    • [[sandbox:]] displays a link to the Sandbox wiki main page
    • +
    + + +

    Wiki links are displayed in red if the page doesn't exist yet, eg: Nonexistent page.

    + + +

    Links to others resources (0.7):

    + + +
      +
    • Documents: + +
        +
      • document#17 (link to document with id 17)
      • +
      • document:Greetings (link to the document with title "Greetings")
      • +
      • document:"Some document" (double quotes can be used when document title contains spaces)
      • +
    • +
    + + +
      +
    • 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"
      • +
    • +
    + + +
      +
    • Attachments: + +
        +
      • attachment:file.zip (link to the attachment of the current object named file.zip)
      • +
      • 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)
      • +
    • +
    + + +
      +
    • Repository 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
      • +
    • + +
    + + + +

    Escaping (0.7):

    + + +
      +
    • You can prevent Redmine links from being parsed by preceding them with an exclamation mark: !
    • +
    + + +

    External links

    + + +

    HTTP URLs and email addresses are automatically turned into clickable links:

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

    displays: http://www.redmine.org,

    + + +

    If you want to display a specific text instead of the URL, you can use the standard textile syntax:

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

    displays: Redmine web site

    + + +

    Text formatting

    + + +

    For things such as headlines, bold, tables, lists, Redmine supports Textile syntax. See http://hobix.com/textile/ for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.

    + + +

    Font style

    + + +
    +* *bold*
    +* _italic_
    +* _*bold italic*_
    +* +underline+
    +* -strike-through-
    +
    + +

    Display:

    + + +
      +
    • bold
    • +
    • italic
    • +
    • *bold italic*
    • +
    • underline
    • +
    • strike-through
    • +
    + + +

    Inline images

    + + +
      +
    • !image_url! displays an image located at image_url (textile syntax)
    • +
    • !>image_url! right floating image
    • +
    • If you have an image attached to your wiki page, it can be displayed inline using its filename: !attached_image.png!
    • +
    + + +

    Headings

    + + +
    +h1. Heading
    +h2. Subheading
    +h3. Subheading
    +
    + +

    Paragraphs

    + + +
    +p>. right aligned
    +p=. centered
    +
    + +

    This is centered paragraph.

    + + +

    Blockquotes

    + + +

    Start the paragraph with bq.

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

    Display:

    + + +
    +

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

    +
    + + +

    Table of content

    + + +
    +{{toc}} => left aligned toc
    +{{>toc}} => right aligned toc
    +
    + +

    Macros

    + + +

    Redmine has the following builtin macros:

    + + +

    hello_world

    Sample macro.

    include

    Include a wiki page. Example:

    + + +
    {{include(Foo)}}
    macro_list

    Displays a list of all available macros, including description if available.

    + + +

    Code highlighting

    + + +

    Code highlightment relies on CodeRay, a fast syntax highlighting library written completely in Ruby. It currently supports c, html, javascript, rhtml, ruby, scheme, xml languages.

    + + +

    You can highlight code in your wiki page using this syntax:

    + + +
    +<pre><code class="ruby">
    +  Place you code here.
    +</code></pre>
    +
    + +

    Example:

    + + +
     1 # The Greeter class
    + 2 class Greeter
    + 3   def initialize(name)
    + 4     @name = name.capitalize
    + 5   end
    + 6 
    + 7   def salute
    + 8     puts "Hello #{@name}!" 
    + 9   end
    +10 end
    +
    +
    + + diff --git a/groups/public/images/1downarrow.png b/groups/public/images/1downarrow.png new file mode 100644 index 000000000..dd5b65d6a Binary files /dev/null and b/groups/public/images/1downarrow.png differ diff --git a/groups/public/images/1uparrow.png b/groups/public/images/1uparrow.png new file mode 100644 index 000000000..cd514d19c Binary files /dev/null and b/groups/public/images/1uparrow.png differ diff --git a/groups/public/images/22x22/authent.png b/groups/public/images/22x22/authent.png new file mode 100644 index 000000000..d2b29945f Binary files /dev/null and b/groups/public/images/22x22/authent.png differ diff --git a/groups/public/images/22x22/comment.png b/groups/public/images/22x22/comment.png new file mode 100644 index 000000000..e2f4e701c Binary files /dev/null and b/groups/public/images/22x22/comment.png differ diff --git a/groups/public/images/22x22/file.png b/groups/public/images/22x22/file.png new file mode 100644 index 000000000..96c56a2b5 Binary files /dev/null and b/groups/public/images/22x22/file.png differ diff --git a/groups/public/images/22x22/info.png b/groups/public/images/22x22/info.png new file mode 100644 index 000000000..cf54e2c6a Binary files /dev/null and b/groups/public/images/22x22/info.png differ diff --git a/groups/public/images/22x22/notifications.png b/groups/public/images/22x22/notifications.png new file mode 100644 index 000000000..972f4a24d Binary files /dev/null and b/groups/public/images/22x22/notifications.png differ diff --git a/groups/public/images/22x22/options.png b/groups/public/images/22x22/options.png new file mode 100644 index 000000000..48da1516c Binary files /dev/null and b/groups/public/images/22x22/options.png differ diff --git a/groups/public/images/22x22/package.png b/groups/public/images/22x22/package.png new file mode 100644 index 000000000..f1a98dcde Binary files /dev/null and b/groups/public/images/22x22/package.png differ diff --git a/groups/public/images/22x22/plugin.png b/groups/public/images/22x22/plugin.png new file mode 100644 index 000000000..455fa6a80 Binary files /dev/null and b/groups/public/images/22x22/plugin.png differ diff --git a/groups/public/images/22x22/projects.png b/groups/public/images/22x22/projects.png new file mode 100644 index 000000000..4f023bedb Binary files /dev/null and b/groups/public/images/22x22/projects.png differ diff --git a/groups/public/images/22x22/role.png b/groups/public/images/22x22/role.png new file mode 100644 index 000000000..4de98edd4 Binary files /dev/null and b/groups/public/images/22x22/role.png differ diff --git a/groups/public/images/22x22/settings.png b/groups/public/images/22x22/settings.png new file mode 100644 index 000000000..54a3b4730 Binary files /dev/null and b/groups/public/images/22x22/settings.png differ diff --git a/groups/public/images/22x22/tracker.png b/groups/public/images/22x22/tracker.png new file mode 100644 index 000000000..f51394186 Binary files /dev/null and b/groups/public/images/22x22/tracker.png differ diff --git a/groups/public/images/22x22/users.png b/groups/public/images/22x22/users.png new file mode 100644 index 000000000..92f396207 Binary files /dev/null and b/groups/public/images/22x22/users.png differ diff --git a/groups/public/images/22x22/workflow.png b/groups/public/images/22x22/workflow.png new file mode 100644 index 000000000..9d1b9d8b9 Binary files /dev/null and b/groups/public/images/22x22/workflow.png differ diff --git a/groups/public/images/2downarrow.png b/groups/public/images/2downarrow.png new file mode 100644 index 000000000..05880f381 Binary files /dev/null and b/groups/public/images/2downarrow.png differ diff --git a/groups/public/images/2uparrow.png b/groups/public/images/2uparrow.png new file mode 100644 index 000000000..6a87aabbd Binary files /dev/null and b/groups/public/images/2uparrow.png differ diff --git a/groups/public/images/32x32/file.png b/groups/public/images/32x32/file.png new file mode 100644 index 000000000..1662b5302 Binary files /dev/null and b/groups/public/images/32x32/file.png differ diff --git a/groups/public/images/add.png b/groups/public/images/add.png new file mode 100644 index 000000000..db59058e5 Binary files /dev/null and b/groups/public/images/add.png differ diff --git a/groups/public/images/admin.png b/groups/public/images/admin.png new file mode 100644 index 000000000..c98330ca1 Binary files /dev/null and b/groups/public/images/admin.png differ diff --git a/groups/public/images/arrow_bw.png b/groups/public/images/arrow_bw.png new file mode 100644 index 000000000..2af9e2cd4 Binary files /dev/null and b/groups/public/images/arrow_bw.png differ diff --git a/groups/public/images/arrow_down.png b/groups/public/images/arrow_down.png new file mode 100644 index 000000000..ea37f3a9e Binary files /dev/null and b/groups/public/images/arrow_down.png differ diff --git a/groups/public/images/arrow_from.png b/groups/public/images/arrow_from.png new file mode 100644 index 000000000..7d94ad185 Binary files /dev/null and b/groups/public/images/arrow_from.png differ diff --git a/groups/public/images/arrow_to.png b/groups/public/images/arrow_to.png new file mode 100644 index 000000000..f021e98c9 Binary files /dev/null and b/groups/public/images/arrow_to.png differ diff --git a/groups/public/images/attachment.png b/groups/public/images/attachment.png new file mode 100644 index 000000000..b7ce3c445 Binary files /dev/null and b/groups/public/images/attachment.png differ diff --git a/groups/public/images/calendar.png b/groups/public/images/calendar.png new file mode 100644 index 000000000..619172a99 Binary files /dev/null and b/groups/public/images/calendar.png differ diff --git a/groups/public/images/cancel.png b/groups/public/images/cancel.png new file mode 100644 index 000000000..0840438c5 Binary files /dev/null and b/groups/public/images/cancel.png differ diff --git a/groups/public/images/changeset.png b/groups/public/images/changeset.png new file mode 100644 index 000000000..67de2c6cc Binary files /dev/null and b/groups/public/images/changeset.png differ diff --git a/groups/public/images/close.png b/groups/public/images/close.png new file mode 100644 index 000000000..3501ed4d5 Binary files /dev/null and b/groups/public/images/close.png differ diff --git a/groups/public/images/close_hl.png b/groups/public/images/close_hl.png new file mode 100644 index 000000000..a433f7515 Binary files /dev/null and b/groups/public/images/close_hl.png differ diff --git a/groups/public/images/comments.png b/groups/public/images/comments.png new file mode 100644 index 000000000..39433cf78 Binary files /dev/null and b/groups/public/images/comments.png differ diff --git a/groups/public/images/copy.png b/groups/public/images/copy.png new file mode 100644 index 000000000..dccaa0614 Binary files /dev/null and b/groups/public/images/copy.png differ diff --git a/groups/public/images/csv.png b/groups/public/images/csv.png new file mode 100644 index 000000000..405863116 Binary files /dev/null and b/groups/public/images/csv.png differ diff --git a/groups/public/images/delete.png b/groups/public/images/delete.png new file mode 100644 index 000000000..a1af31d83 Binary files /dev/null and b/groups/public/images/delete.png differ diff --git a/groups/public/images/document.png b/groups/public/images/document.png new file mode 100644 index 000000000..d00b9b2f4 Binary files /dev/null and b/groups/public/images/document.png differ diff --git a/groups/public/images/draft.png b/groups/public/images/draft.png new file mode 100644 index 000000000..9eda38b54 Binary files /dev/null and b/groups/public/images/draft.png differ diff --git a/groups/public/images/edit.png b/groups/public/images/edit.png new file mode 100644 index 000000000..1b6a9e315 Binary files /dev/null and b/groups/public/images/edit.png differ diff --git a/groups/public/images/expand.png b/groups/public/images/expand.png new file mode 100644 index 000000000..3e3aaa441 Binary files /dev/null and b/groups/public/images/expand.png differ diff --git a/groups/public/images/external.png b/groups/public/images/external.png new file mode 100644 index 000000000..45df6404f Binary files /dev/null and b/groups/public/images/external.png differ diff --git a/groups/public/images/false.png b/groups/public/images/false.png new file mode 100644 index 000000000..e308ddcd6 Binary files /dev/null and b/groups/public/images/false.png differ diff --git a/groups/public/images/fav.png b/groups/public/images/fav.png new file mode 100644 index 000000000..49c0f473a Binary files /dev/null and b/groups/public/images/fav.png differ diff --git a/groups/public/images/fav_off.png b/groups/public/images/fav_off.png new file mode 100644 index 000000000..5b10e9df5 Binary files /dev/null and b/groups/public/images/fav_off.png differ diff --git a/groups/public/images/feed.png b/groups/public/images/feed.png new file mode 100644 index 000000000..900188ae0 Binary files /dev/null and b/groups/public/images/feed.png differ diff --git a/groups/public/images/file.png b/groups/public/images/file.png new file mode 100644 index 000000000..f387dd305 Binary files /dev/null and b/groups/public/images/file.png differ diff --git a/groups/public/images/folder.png b/groups/public/images/folder.png new file mode 100644 index 000000000..d2ab69ad5 Binary files /dev/null and b/groups/public/images/folder.png differ diff --git a/groups/public/images/folder_open.png b/groups/public/images/folder_open.png new file mode 100644 index 000000000..e8e8c412e Binary files /dev/null and b/groups/public/images/folder_open.png differ diff --git a/groups/public/images/help.png b/groups/public/images/help.png new file mode 100644 index 000000000..af4e6ff46 Binary files /dev/null and b/groups/public/images/help.png differ diff --git a/groups/public/images/history.png b/groups/public/images/history.png new file mode 100644 index 000000000..c6a9607eb Binary files /dev/null and b/groups/public/images/history.png differ diff --git a/groups/public/images/home.png b/groups/public/images/home.png new file mode 100644 index 000000000..21ee5470e Binary files /dev/null and b/groups/public/images/home.png differ diff --git a/groups/public/images/html.png b/groups/public/images/html.png new file mode 100644 index 000000000..efb32e7c5 Binary files /dev/null and b/groups/public/images/html.png differ diff --git a/groups/public/images/image.png b/groups/public/images/image.png new file mode 100644 index 000000000..a22cf7f6a Binary files /dev/null and b/groups/public/images/image.png differ diff --git a/groups/public/images/index.png b/groups/public/images/index.png new file mode 100644 index 000000000..1ada3b2dc Binary files /dev/null and b/groups/public/images/index.png differ diff --git a/groups/public/images/jstoolbar/bt_code.png b/groups/public/images/jstoolbar/bt_code.png new file mode 100644 index 000000000..8b6aefbb5 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_code.png differ diff --git a/groups/public/images/jstoolbar/bt_del.png b/groups/public/images/jstoolbar/bt_del.png new file mode 100644 index 000000000..36a912b65 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_del.png differ diff --git a/groups/public/images/jstoolbar/bt_em.png b/groups/public/images/jstoolbar/bt_em.png new file mode 100644 index 000000000..caa808236 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_em.png differ diff --git a/groups/public/images/jstoolbar/bt_h1.png b/groups/public/images/jstoolbar/bt_h1.png new file mode 100644 index 000000000..b64e75994 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_h1.png differ diff --git a/groups/public/images/jstoolbar/bt_h2.png b/groups/public/images/jstoolbar/bt_h2.png new file mode 100644 index 000000000..1e88cb936 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_h2.png differ diff --git a/groups/public/images/jstoolbar/bt_h3.png b/groups/public/images/jstoolbar/bt_h3.png new file mode 100644 index 000000000..646bad11d Binary files /dev/null and b/groups/public/images/jstoolbar/bt_h3.png differ diff --git a/groups/public/images/jstoolbar/bt_img.png b/groups/public/images/jstoolbar/bt_img.png new file mode 100644 index 000000000..ddded465c Binary files /dev/null and b/groups/public/images/jstoolbar/bt_img.png differ diff --git a/groups/public/images/jstoolbar/bt_ins.png b/groups/public/images/jstoolbar/bt_ins.png new file mode 100644 index 000000000..92c8dff69 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_ins.png differ diff --git a/groups/public/images/jstoolbar/bt_link.png b/groups/public/images/jstoolbar/bt_link.png new file mode 100644 index 000000000..8d67e3d90 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_link.png differ diff --git a/groups/public/images/jstoolbar/bt_ol.png b/groups/public/images/jstoolbar/bt_ol.png new file mode 100644 index 000000000..0cce2fae0 Binary files /dev/null and b/groups/public/images/jstoolbar/bt_ol.png differ diff --git a/groups/public/images/jstoolbar/bt_pre.png b/groups/public/images/jstoolbar/bt_pre.png new file mode 100644 index 000000000..99a7d52ce Binary files /dev/null and b/groups/public/images/jstoolbar/bt_pre.png differ diff --git a/groups/public/images/jstoolbar/bt_strong.png b/groups/public/images/jstoolbar/bt_strong.png new file mode 100644 index 000000000..33e6daadd Binary files /dev/null and b/groups/public/images/jstoolbar/bt_strong.png differ diff --git a/groups/public/images/jstoolbar/bt_ul.png b/groups/public/images/jstoolbar/bt_ul.png new file mode 100644 index 000000000..df9ecc0cf Binary files /dev/null and b/groups/public/images/jstoolbar/bt_ul.png differ diff --git a/groups/public/images/loading.gif b/groups/public/images/loading.gif new file mode 100644 index 000000000..085ccaeca Binary files /dev/null and b/groups/public/images/loading.gif differ diff --git a/groups/public/images/locked.png b/groups/public/images/locked.png new file mode 100644 index 000000000..c2789e35c Binary files /dev/null and b/groups/public/images/locked.png differ diff --git a/groups/public/images/message.png b/groups/public/images/message.png new file mode 100644 index 000000000..252ea14d5 Binary files /dev/null and b/groups/public/images/message.png differ diff --git a/groups/public/images/milestone.png b/groups/public/images/milestone.png new file mode 100644 index 000000000..3df96fc24 Binary files /dev/null and b/groups/public/images/milestone.png differ diff --git a/groups/public/images/move.png b/groups/public/images/move.png new file mode 100644 index 000000000..32fdb846d Binary files /dev/null and b/groups/public/images/move.png differ diff --git a/groups/public/images/news.png b/groups/public/images/news.png new file mode 100644 index 000000000..6a2ecce1b Binary files /dev/null and b/groups/public/images/news.png differ diff --git a/groups/public/images/package.png b/groups/public/images/package.png new file mode 100644 index 000000000..ff629d117 Binary files /dev/null and b/groups/public/images/package.png differ diff --git a/groups/public/images/pdf.png b/groups/public/images/pdf.png new file mode 100644 index 000000000..68c9bada8 Binary files /dev/null and b/groups/public/images/pdf.png differ diff --git a/groups/public/images/projects.png b/groups/public/images/projects.png new file mode 100644 index 000000000..244c896f0 Binary files /dev/null and b/groups/public/images/projects.png differ diff --git a/groups/public/images/reload.png b/groups/public/images/reload.png new file mode 100644 index 000000000..c5eb34ee0 Binary files /dev/null and b/groups/public/images/reload.png differ diff --git a/groups/public/images/report.png b/groups/public/images/report.png new file mode 100644 index 000000000..05386ac4f Binary files /dev/null and b/groups/public/images/report.png differ diff --git a/groups/public/images/save.png b/groups/public/images/save.png new file mode 100644 index 000000000..f379d9f34 Binary files /dev/null and b/groups/public/images/save.png differ diff --git a/groups/public/images/sort_asc.png b/groups/public/images/sort_asc.png new file mode 100644 index 000000000..e9cb0f4f2 Binary files /dev/null and b/groups/public/images/sort_asc.png differ diff --git a/groups/public/images/sort_desc.png b/groups/public/images/sort_desc.png new file mode 100644 index 000000000..fc80a5cc9 Binary files /dev/null and b/groups/public/images/sort_desc.png differ diff --git a/groups/public/images/stats.png b/groups/public/images/stats.png new file mode 100644 index 000000000..22ae78ab4 Binary files /dev/null and b/groups/public/images/stats.png differ diff --git a/groups/public/images/sticky.png b/groups/public/images/sticky.png new file mode 100644 index 000000000..d32ee63a4 Binary files /dev/null and b/groups/public/images/sticky.png differ diff --git a/groups/public/images/sub.gif b/groups/public/images/sub.gif new file mode 100644 index 000000000..52e4065d5 Binary files /dev/null and b/groups/public/images/sub.gif differ diff --git a/groups/public/images/task_done.png b/groups/public/images/task_done.png new file mode 100644 index 000000000..2a4c81e9d Binary files /dev/null and b/groups/public/images/task_done.png differ diff --git a/groups/public/images/task_late.png b/groups/public/images/task_late.png new file mode 100644 index 000000000..2e8a40d6e Binary files /dev/null and b/groups/public/images/task_late.png differ diff --git a/groups/public/images/task_todo.png b/groups/public/images/task_todo.png new file mode 100644 index 000000000..43c1eb9b9 Binary files /dev/null and b/groups/public/images/task_todo.png differ diff --git a/groups/public/images/ticket.png b/groups/public/images/ticket.png new file mode 100644 index 000000000..244e6ca04 Binary files /dev/null and b/groups/public/images/ticket.png differ diff --git a/groups/public/images/ticket_checked.png b/groups/public/images/ticket_checked.png new file mode 100644 index 000000000..4b1dfbc3e Binary files /dev/null and b/groups/public/images/ticket_checked.png differ diff --git a/groups/public/images/ticket_edit.png b/groups/public/images/ticket_edit.png new file mode 100644 index 000000000..291bfc764 Binary files /dev/null and b/groups/public/images/ticket_edit.png differ diff --git a/groups/public/images/time.png b/groups/public/images/time.png new file mode 100644 index 000000000..81aa780e3 Binary files /dev/null and b/groups/public/images/time.png differ diff --git a/groups/public/images/toggle_check.png b/groups/public/images/toggle_check.png new file mode 100644 index 000000000..aca5e4321 Binary files /dev/null and b/groups/public/images/toggle_check.png differ diff --git a/groups/public/images/true.png b/groups/public/images/true.png new file mode 100644 index 000000000..cecf618d8 Binary files /dev/null and b/groups/public/images/true.png differ diff --git a/groups/public/images/txt.png b/groups/public/images/txt.png new file mode 100644 index 000000000..2978385e7 Binary files /dev/null and b/groups/public/images/txt.png differ diff --git a/groups/public/images/unlock.png b/groups/public/images/unlock.png new file mode 100644 index 000000000..e0d414978 Binary files /dev/null and b/groups/public/images/unlock.png differ diff --git a/groups/public/images/user.png b/groups/public/images/user.png new file mode 100644 index 000000000..5f55e7e49 Binary files /dev/null and b/groups/public/images/user.png differ diff --git a/groups/public/images/user_new.png b/groups/public/images/user_new.png new file mode 100644 index 000000000..aaa430dea Binary files /dev/null and b/groups/public/images/user_new.png differ diff --git a/groups/public/images/user_page.png b/groups/public/images/user_page.png new file mode 100644 index 000000000..78144862c Binary files /dev/null and b/groups/public/images/user_page.png differ diff --git a/groups/public/images/users.png b/groups/public/images/users.png new file mode 100644 index 000000000..f3a07c3f7 Binary files /dev/null and b/groups/public/images/users.png differ diff --git a/groups/public/images/warning.png b/groups/public/images/warning.png new file mode 100644 index 000000000..bbef670b6 Binary files /dev/null and b/groups/public/images/warning.png differ diff --git a/groups/public/images/wiki_edit.png b/groups/public/images/wiki_edit.png new file mode 100644 index 000000000..bdc333a65 Binary files /dev/null and b/groups/public/images/wiki_edit.png differ diff --git a/groups/public/images/zoom_in.png b/groups/public/images/zoom_in.png new file mode 100644 index 000000000..d9abe7f52 Binary files /dev/null and b/groups/public/images/zoom_in.png differ diff --git a/groups/public/images/zoom_in_g.png b/groups/public/images/zoom_in_g.png new file mode 100644 index 000000000..72b271c5e Binary files /dev/null and b/groups/public/images/zoom_in_g.png differ diff --git a/groups/public/images/zoom_out.png b/groups/public/images/zoom_out.png new file mode 100644 index 000000000..906e4a4e5 Binary files /dev/null and b/groups/public/images/zoom_out.png differ diff --git a/groups/public/images/zoom_out_g.png b/groups/public/images/zoom_out_g.png new file mode 100644 index 000000000..7f2416be2 Binary files /dev/null and b/groups/public/images/zoom_out_g.png differ diff --git a/groups/public/javascripts/application.js b/groups/public/javascripts/application.js new file mode 100644 index 000000000..4e8849842 --- /dev/null +++ b/groups/public/javascripts/application.js @@ -0,0 +1,122 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + +function checkAll (id, checked) { + var el = document.getElementById(id); + for (var i = 0; i < el.elements.length; i++) { + if (el.elements[i].disabled==false) { + el.elements[i].checked = checked; + } + } +} + +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 0) { + Element.show('ajax-indicator'); + } + }, + onComplete: function(){ + if ($('ajax-indicator') && Ajax.activeRequestCount == 0) { + Element.hide('ajax-indicator'); + } + } +}); diff --git a/groups/public/javascripts/calendar/calendar-setup.js b/groups/public/javascripts/calendar/calendar-setup.js new file mode 100644 index 000000000..f2b485430 --- /dev/null +++ b/groups/public/javascripts/calendar/calendar-setup.js @@ -0,0 +1,200 @@ +/* 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; +}; diff --git a/groups/public/javascripts/calendar/calendar.js b/groups/public/javascripts/calendar/calendar.js new file mode 100644 index 000000000..9088e0e89 --- /dev/null +++ b/groups/public/javascripts/calendar/calendar.js @@ -0,0 +1,1806 @@ +/* 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 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 = "
    " + text + "
    "; + 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; diff --git a/groups/public/javascripts/calendar/lang/calendar-bg.js b/groups/public/javascripts/calendar/lang/calendar-bg.js new file mode 100644 index 000000000..edc870e3b --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-bg.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar BG language +// Author: Nikolay Solakov, +// 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"] = "ЧаÑ:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-cs.js b/groups/public/javascripts/calendar/lang/calendar-cs.js new file mode 100644 index 000000000..406ac6695 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-cs.js @@ -0,0 +1,69 @@ +/* + 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-da.js b/groups/public/javascripts/calendar/lang/calendar-da.js new file mode 100644 index 000000000..2cba5f683 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-da.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// Encoding: any +// Translater: Mads N. Vestergaard +// 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 muse tasten inde pÃ¥ punkterne for at vælge hurtigere."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Tids valg:\n" + +"- Klik pÃ¥ en af tidsramerne 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 idag"; +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"] = " (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"] = "6,7"; + +Calendar._TT["CLOSE"] = "Luk"; +Calendar._TT["TODAY"] = "Idag"; +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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-de.js b/groups/public/javascripts/calendar/lang/calendar-de.js new file mode 100644 index 000000000..c320699ca --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-de.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar DE language +// Author: Jack (tR), +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-en.js b/groups/public/javascripts/calendar/lang/calendar-en.js new file mode 100644 index 000000000..0dbde793d --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-en.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-es.js b/groups/public/javascripts/calendar/lang/calendar-es.js new file mode 100644 index 000000000..11d0b53d5 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-es.js @@ -0,0 +1,129 @@ +// ** I18N + +// Calendar ES (spanish) language +// Author: Mihai Bazon, +// Updater: Servilio Afre Puentes +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-fi.js b/groups/public/javascripts/calendar/lang/calendar-fi.js new file mode 100644 index 000000000..1e65eee42 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-fi.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar FI language +// Author: Antti Perkiömäki +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-fr.js b/groups/public/javascripts/calendar/lang/calendar-fr.js new file mode 100644 index 000000000..ee2a486fd --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-fr.js @@ -0,0 +1,129 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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, 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 :"; diff --git a/groups/public/javascripts/calendar/lang/calendar-he.js b/groups/public/javascripts/calendar/lang/calendar-he.js new file mode 100644 index 000000000..bd92e0073 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-he.js @@ -0,0 +1,127 @@ +// ** 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"] = "6,7"; + +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"] = "זמן:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-it.js b/groups/public/javascripts/calendar/lang/calendar-it.js new file mode 100644 index 000000000..fbc80c935 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-it.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-ja.js b/groups/public/javascripts/calendar/lang/calendar-ja.js new file mode 100644 index 000000000..1bcc8c38c --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-ja.js @@ -0,0 +1,87 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-ko.js b/groups/public/javascripts/calendar/lang/calendar-ko.js new file mode 100644 index 000000000..016453bfc --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-ko.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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"] = "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"] = "ì´ì „ í•´"; +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-)í´ë¦­ 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"] = "주"; +Calendar._TT["TIME"] = "Time:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-lt.js b/groups/public/javascripts/calendar/lang/calendar-lt.js new file mode 100644 index 000000000..888cfc801 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-lt.js @@ -0,0 +1,128 @@ +// ** I18N + +// Calendar LT language +// Author: Gediminas Muižis, +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-nl.js b/groups/public/javascripts/calendar/lang/calendar-nl.js new file mode 100644 index 000000000..69a0d8d52 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-nl.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar NL language +// Author: Linda van den Brink, +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-no.js b/groups/public/javascripts/calendar/lang/calendar-no.js new file mode 100644 index 000000000..0506b83e2 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-no.js @@ -0,0 +1,86 @@ +// ** I18N + +// Calendar NO language (Norwegian/Norsk bokmÃ¥l) +// Author: Kai Olav Fredriksen + +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-pl.js b/groups/public/javascripts/calendar/lang/calendar-pl.js new file mode 100644 index 000000000..32273d674 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-pl.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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 +("Niedziela", + "PoniedziaÅ‚ek", + "Wtorek", + "Åšroda", + "Czwartek", + "PiÄ…tek", + "Sobota", + "Niedziela"); + +// 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 +("Nie", + "Pon", + "Wto", + "Åšro", + "Czw", + "PiÄ…", + "Sob", + "Nie"); + +// 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 +("StyczeÅ„", + "Luty", + "Marzec", + "KwiecieÅ„", + "Maj", + "Czerwiec", + "Lipiec", + "SierpieÅ„", + "WrzesieÅ„", + "Październik", + "Listopad", + "GrudzieÅ„"); + +// short month names +Calendar._SMN = new Array +("Sty", + "Lut", + "Mar", + "Kwi", + "Maj", + "Cze", + "Lip", + "Sie", + "Wrz", + "Paź", + "Lis", + "Gru"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "O kalendarzu"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Po ostatniÄ… wersjÄ™ odwiedź: http://www.dynarch.com/projects/calendar/\n" + +"Rozpowszechniany pod licencjÄ… GNU LGPL. Zobacz: http://gnu.org/licenses/lgpl.html z celu zapoznania siÄ™ ze szczegółami." + +"\n\n" + +"Wybór daty:\n" + +"- Użyj \xab, \xbb przycisków by zaznaczyć rok\n" + +"- Użyj " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " przycisków by zaznaczyć miesiÄ…c\n" + +"- Trzymaj wciÅ›niÄ™ty przycisk myszy na każdym z powyższych przycisków by przyÅ›pieszyć zaznaczanie."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Wybór czasu:\n" + +"- Kliknij na każdym przedziale czasu aby go powiÄ™kszyć\n" + +"- lub kliknij z przyciskiem Shift by go zmniejszyć\n" + +"- lub kliknij i przeciÄ…gnij dla szybszego zaznaczenia."; + +Calendar._TT["PREV_YEAR"] = "Poprz. rok (przytrzymaj dla menu)"; +Calendar._TT["PREV_MONTH"] = "Poprz. miesiÄ…c (przytrzymaj dla menu)"; +Calendar._TT["GO_TODAY"] = "Idź do Dzisiaj"; +Calendar._TT["NEXT_MONTH"] = "NastÄ™pny miesiÄ…c(przytrzymaj dla menu)"; +Calendar._TT["NEXT_YEAR"] = "NastÄ™pny rok (przytrzymaj dla menu)"; +Calendar._TT["SEL_DATE"] = "Zaznacz datÄ™"; +Calendar._TT["DRAG_TO_MOVE"] = "PrzeciÄ…gnij by przenieść"; +Calendar._TT["PART_TODAY"] = " (dzisiaj)"; + +// 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"] = "Pokaż %s pierwszy"; + +// 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"] = "Zamknij"; +Calendar._TT["TODAY"] = "Dzisiaj"; +Calendar._TT["TIME_PART"] = "(Shift-)Kliknij lub upuść by zmienić wartość"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%R-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Czas:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-pt-br.js b/groups/public/javascripts/calendar/lang/calendar-pt-br.js new file mode 100644 index 000000000..5d4d014ce --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-pt-br.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar pt_BR language +// Author: Adalberto Machado, +// 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", + "Terca", + "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 = 1; + +// full month names +Calendar._MN = new Array +("Janeiro", + "Fevereiro", + "Marco", + "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 calendario"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Ultima versao visite: http://www.dynarch.com/projects/calendar/\n" + +"Distribuido sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." + +"\n\n" + +"Selecao de data:\n" + +"- Use os botoes \xab, \xbb para selecionar o ano\n" + +"- Use os botoes " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para selecionar o mes\n" + +"- Segure o botao do mouse em qualquer um desses botoes para selecao rapida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selecao de hora:\n" + +"- Clique em qualquer parte da hora para incrementar\n" + +"- ou Shift-click para decrementar\n" + +"- ou clique e segure para selecao rapida."; + +Calendar._TT["PREV_YEAR"] = "Ant. ano (segure para menu)"; +Calendar._TT["PREV_MONTH"] = "Ant. mes (segure para menu)"; +Calendar._TT["GO_TODAY"] = "Hoje"; +Calendar._TT["NEXT_MONTH"] = "Prox. mes (segure para menu)"; +Calendar._TT["NEXT_YEAR"] = "Prox. 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-pt.js b/groups/public/javascripts/calendar/lang/calendar-pt.js new file mode 100644 index 000000000..5d4d014ce --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-pt.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar pt_BR language +// Author: Adalberto Machado, +// 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", + "Terca", + "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 = 1; + +// full month names +Calendar._MN = new Array +("Janeiro", + "Fevereiro", + "Marco", + "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 calendario"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"Ultima versao visite: http://www.dynarch.com/projects/calendar/\n" + +"Distribuido sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." + +"\n\n" + +"Selecao de data:\n" + +"- Use os botoes \xab, \xbb para selecionar o ano\n" + +"- Use os botoes " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para selecionar o mes\n" + +"- Segure o botao do mouse em qualquer um desses botoes para selecao rapida."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Selecao de hora:\n" + +"- Clique em qualquer parte da hora para incrementar\n" + +"- ou Shift-click para decrementar\n" + +"- ou clique e segure para selecao rapida."; + +Calendar._TT["PREV_YEAR"] = "Ant. ano (segure para menu)"; +Calendar._TT["PREV_MONTH"] = "Ant. mes (segure para menu)"; +Calendar._TT["GO_TODAY"] = "Hoje"; +Calendar._TT["NEXT_MONTH"] = "Prox. mes (segure para menu)"; +Calendar._TT["NEXT_YEAR"] = "Prox. 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-ro.js b/groups/public/javascripts/calendar/lang/calendar-ro.js new file mode 100644 index 000000000..fa34ab1ea --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-ro.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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 +("Duminica", + "Luni", + "Marti", + "Miercuri", + "Joi", + "Vineri", + "Sambata", + "Duminica"); + +// 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", + "Sam", + "Dum"); + +// 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 +("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"] = "Data de azi"; +Calendar._TT["NEXT_MONTH"] = "Luna viitoare (apasati pentru meniu)"; +Calendar._TT["NEXT_YEAR"] = "Anul viitor (apasati pentru meniu)"; +Calendar._TT["SEL_DATE"] = "Selectie 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"] = "Vizualizeaza %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"] = "inchide"; +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"] = "sapt"; +Calendar._TT["TIME"] = "Ora:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-ru.js b/groups/public/javascripts/calendar/lang/calendar-ru.js new file mode 100644 index 000000000..6274cc892 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-ru.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar RU language +// Translation: Sly Golovanov, http://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"] = "ВремÑ:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-sr.js b/groups/public/javascripts/calendar/lang/calendar-sr.js new file mode 100644 index 000000000..626cbdc64 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-sr.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar SR language +// Author: Dragan Matic, +// 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", + "Ponedeljak", + "Utorak", + "Sreda", + "ÄŒ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", + "Sre", + "ÄŒ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 (hold for menu)"; +Calendar._TT["PREV_MONTH"] = "Preth. mesec (hold for menu)"; +Calendar._TT["GO_TODAY"] = "Na danaÅ¡nji dan"; +Calendar._TT["NEXT_MONTH"] = "Naredni mesec (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "Naredna godina (hold for menu)"; +Calendar._TT["SEL_DATE"] = "Izbor datuma"; +Calendar._TT["DRAG_TO_MOVE"] = "Prevucite za izmenu"; +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"] = "Prikazi %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"] = "Close"; +Calendar._TT["TODAY"] = "Danas"; +Calendar._TT["TIME_PART"] = "(Shift-)Klik ili prevlaÄenje za izmenu vrednosti"; + +// 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"] = "Vreme:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-sv.js b/groups/public/javascripts/calendar/lang/calendar-sv.js new file mode 100644 index 000000000..7e73d7926 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-sv.js @@ -0,0 +1,84 @@ +// ** 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 = 0; + +// 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"] = "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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-uk.js b/groups/public/javascripts/calendar/lang/calendar-uk.js new file mode 100644 index 000000000..0dbde793d --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-uk.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-zh-tw.js b/groups/public/javascripts/calendar/lang/calendar-zh-tw.js new file mode 100644 index 000000000..c48d25b0e --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-zh-tw.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// 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 ;-) +"最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" + +"æ™‚é–“é¸æ“‡æ–¹å¼ï¼š\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"] = "時間:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-zh.js b/groups/public/javascripts/calendar/lang/calendar-zh.js new file mode 100644 index 000000000..ddb092bfa --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-zh.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar Chinese language +// Author: Andy Wu, +// 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" + +"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"] = "回到今天"; +Calendar._TT["NEXT_MONTH"] = "下月 (hold for menu)"; +Calendar._TT["NEXT_YEAR"] = "下年 (hold for menu)"; +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"] = "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"] = "关闭"; +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"] = "%a, %b %e"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "Time:"; diff --git a/groups/public/javascripts/context_menu.js b/groups/public/javascripts/context_menu.js new file mode 100644 index 000000000..3e2d571fa --- /dev/null +++ b/groups/public/javascripts/context_menu.js @@ -0,0 +1,218 @@ +/* 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 selection when using Ctrl/Shit key + var tables = $$('table.issues'); + for (i=0; i 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 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}; +} diff --git a/groups/public/javascripts/controls.js b/groups/public/javascripts/controls.js new file mode 100644 index 000000000..8c273f874 --- /dev/null +++ b/groups/public/javascripts/controls.js @@ -0,0 +1,833 @@ +// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005, 2006 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 = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + 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); + + 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, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (navigator.appVersion.indexOf('MSIE')>0) && + (navigator.userAgent.indexOf('Opera')<0) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + 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(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (navigator.appVersion.indexOf('AppleWebKit') > 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 = document.getElementsByClassName(this.options.select, selectedElement) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = 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; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + 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() { + 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.Local.prototype = Object.extend(new 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("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + 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("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + 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 "
      " + ret.join('') + "
    "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +// 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(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + paramName: "value", + okButton: true, + okText: "ok", + cancelLink: true, + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + submitOnBlur: false, + ajaxOptions: {}, + evalScripts: false + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function(evt) { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + Event.stop(evt); + } + return false; + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + okButton.className = 'editor_ok_button'; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + cancelLink.className = 'editor_cancel'; + this.form.appendChild(cancelLink); + } + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
    /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.obj = this; + textField.type = "text"; + textField.name = this.options.paramName; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + textField.className = 'editor_field'; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + if (this.options.submitOnBlur) + textField.onblur = this.onSubmit.bind(this); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.obj = this; + textArea.name = this.options.paramName; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + textArea.className = 'editor_field'; + if (this.options.submitOnBlur) + textArea.onblur = this.onSubmit.bind(this); + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + Field.scrollFreeActivate(this.editField); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + if (this.options.evalScripts) { + new Ajax.Request( + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this), + asynchronous:true, + evalScripts:true + }, this.options.ajaxOptions)); + } else { + new Ajax.Updater( + { success: this.element, + // don't update on failure (this could be an option) + failure: null }, + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions)); + } + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; + +Ajax.InPlaceCollectionEditor = Class.create(); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, { + createEditField: function() { + if (!this.cached_selectTag) { + var selectTag = document.createElement("select"); + var collection = this.options.collection || []; + var optionTag; + collection.each(function(e,i) { + optionTag = document.createElement("option"); + optionTag.value = (e instanceof Array) ? e[0] : e; + if((typeof this.options.value == 'undefined') && + ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true; + if(this.options.value==optionTag.value) optionTag.selected = true; + optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); + selectTag.appendChild(optionTag); + }.bind(this)); + this.cached_selectTag = selectTag; + } + + this.editField = this.cached_selectTag; + if(this.options.loadTextURL) this.loadExternalText(); + this.form.appendChild(this.editField); + this.options.callback = function(form, value) { + return "value=" + encodeURIComponent(value); + } + } +}); + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create(); +Form.Element.DelayedObserver.prototype = { + 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)); + } +}; diff --git a/groups/public/javascripts/dragdrop.js b/groups/public/javascripts/dragdrop.js new file mode 100644 index 000000000..c71ddb827 --- /dev/null +++ b/groups/public/javascripts/dragdrop.js @@ -0,0 +1,942 @@ +// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005, 2006 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(typeof Effect == 'undefined') + 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((typeof containment == 'object') && + (containment.constructor == Array)) { + 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 affected = []; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + 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); + }, + + 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(); +Draggable._dragging = {}; + +Draggable.prototype = { + 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 = typeof element._opacity == 'number' ? 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, + 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] || typeof arguments[1].endeffect == 'undefined') + 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 && (typeof options.handle == 'string')) + 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.delta = this.currentDelta(); + 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(typeof Draggable._dragging[this.element] != 'undefined' && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='OPTION' || + src.tagName=='BUTTON' || + src.tagName=='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.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); + 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); + 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(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + 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(typeof this.options.snap == 'function') { + p = this.options.snap(p[0],p[1],this); + } else { + if(this.options.snap instanceof Array) { + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return Math.round(v/this.options.snap)*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 }; + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + 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 != "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){ + var s = Sortable.options(element); + + 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, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + 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, + 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); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + $(e).down('.'+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')]; +} diff --git a/groups/public/javascripts/effects.js b/groups/public/javascripts/effects.js new file mode 100644 index 000000000..3b02eda2b --- /dev/null +++ b/groups/public/javascripts/effects.js @@ -0,0 +1,1088 @@ +// Copyright (c) 2005, 2006 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(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + return element; +} + +Element.getOpacity = function(element){ + element = $(element); + var opacity; + if (opacity = element.getStyle('opacity')) + return parseFloat(opacity); + if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + if (value == 1){ + element.setStyle({ opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : 1.0 }); + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + element.setStyle({opacity: value}); + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.setStyle( + { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } + 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) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + tagifyText: function(element) { + if(typeof Builder == 'undefined') + throw("Effect.tagifyText requires including script.aculo.us' builder.js library"); + + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (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); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + Math.round((pos % (1/pulses)) * pulses) == 0 ? + ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : + 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) + ); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } +}; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, 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 = (typeof effect.options.queue == 'string') ? + 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), 40); + }, + 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(); + this.effects.invoke('loop', timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + 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.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + '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.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.state == 'running') { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + '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() { + return '#'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + 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.Event = Class.create(); +Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), { + initialize: function() { + var options = Object.extend({ + duration: 0 + }, arguments[0] || {}); + this.start(options); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && !window.opera && (!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(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + 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() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: Math.round(this.options.x * position + this.originalLeft) + 'px', + top: Math.round(this.options.y * position + this.originalTop) + '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(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + 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 = Math.round(width) + 'px'; + if(this.options.scaleY) d.height = Math.round(height) + '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(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + 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 = { + 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+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- 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 oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, 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'); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: 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().setStyle({bottom: oldInnerBottom}); + effect.element.down().undoPositioned(); + } + }, 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] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + 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(); +Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: '' + }, arguments[1] || {}); + 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.options.style.parseStyle().map(function(property){ + var originalValue = this.element.getStyle(property[0]); + return $H({ + style: property[0], + originalValue: property[1].unit=='color' ? + parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: property[1].unit=='color' ? + parseColor(property[1].value) : property[1].value, + unit: property[1].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 = $H(), value = null; + this.transforms.each(function(transform){ + value = transform.unit=='color' ? + $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(transform.originalValue[i]+ + (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : + transform.originalValue + Math.round( + ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit; + style[transform.style] = value; + }); + this.element.setStyle(style); + } +}); + +Effect.Transform = Class.create(); +Object.extend(Effect.Transform.prototype, { + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || {}; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + var data = $H(track).values().first(); + this.tracks.push($H({ + ids: $H(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 elements = [$(track.ids) || $$(track.ids)].flatten(); + return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', + 'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', + 'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth', + 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor', + 'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content', + 'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction', + 'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', + 'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight', + 'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight', + 'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity', + 'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY', + 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore', + 'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes', + 'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress', + 'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top', + 'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows', + 'width', 'wordSpacing', 'zIndex']; + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.prototype.parseStyle = function(){ + var element = Element.extend(document.createElement('div')); + element.innerHTML = '

    '; + var style = element.down().style, styleRules = $H(); + + Element.CSS_PROPERTIES.each(function(property){ + if(style[property]) styleRules[property] = style[property]; + }); + + var result = $H(); + + styleRules.each(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if(value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if(Element.CSS_LENGTH.test(value)) + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/), + value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null; + + result[property.underscore().dasherize()] = $H({ value:value, unit:unit }); + }.bind(this)); + + return result; +}; + +Element.morph = function(element, style) { + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {})); + return element; +}; + +['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.gsub(/_/, '-').camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods(); \ No newline at end of file diff --git a/groups/public/javascripts/jstoolbar/jstoolbar.js b/groups/public/javascripts/jstoolbar/jstoolbar.js new file mode 100644 index 000000000..be982d4b9 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/jstoolbar.js @@ -0,0 +1,528 @@ +/* ***** 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; + 'Aide'; + 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); +}; + +// Elements definition ------------------------------------ + +// 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# "); + }); + } + } +} + +// pre +jsToolBar.prototype.elements.pre = { + type: 'button', + title: 'Preformatted text', + fn: { + wiki: function() { this.encloseLineSelection('
    \n', '\n
    ') } + } +} + +// spacer +jsToolBar.prototype.elements.space3 = {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("!", "!") } + } +} diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js new file mode 100644 index 000000000..8a59a8162 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'PÅ™edformátovaný text'; +jsToolBar.strings['Wiki link'] = 'Vložit odkaz na Wiki stránku'; +jsToolBar.strings['Image'] = 'Vložit obrázek'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js new file mode 100644 index 000000000..9996acaf3 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js @@ -0,0 +1,14 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Fed'; +jsToolBar.strings['Italic'] = 'Kursiv'; +jsToolBar.strings['Underline'] = 'Underskrevet'; +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 list'; +jsToolBar.strings['Ordered list'] = 'Nummereret list'; +jsToolBar.strings['Preformatted text'] = 'Preformatteret tekst'; +jsToolBar.strings['Wiki link'] = 'Link til en Wiki side'; +jsToolBar.strings['Image'] = 'Billede'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js new file mode 100644 index 000000000..e2ba3fc1c --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Präformatierter Text'; +jsToolBar.strings['Wiki link'] = 'Verweis (Link) zu einer Wiki-Seite'; +jsToolBar.strings['Image'] = 'Grafik'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js new file mode 100644 index 000000000..357d25951 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Ennaltamuotoiltu teksti'; +jsToolBar.strings['Wiki link'] = 'Linkki Wiki sivulle'; +jsToolBar.strings['Image'] = 'Kuva'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js new file mode 100644 index 000000000..3cbc67863 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Texte préformaté'; +jsToolBar.strings['Wiki link'] = 'Lien vers une page Wiki'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js new file mode 100644 index 000000000..fc4d987de --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = '整形済ã¿ãƒ†ã‚­ã‚¹ãƒˆ'; +jsToolBar.strings['Wiki link'] = 'Wiki ページã¸ã®ãƒªãƒ³ã‚¯'; +jsToolBar.strings['Image'] = 'ç”»åƒ'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js new file mode 100644 index 000000000..f0a7c5d90 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Preformatuotas tekstas'; +jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį'; +jsToolBar.strings['Image'] = 'Paveikslas'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js new file mode 100644 index 000000000..cf6e19ff9 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Preformatert tekst'; +jsToolBar.strings['Wiki link'] = 'Lenke til Wiki-side'; +jsToolBar.strings['Image'] = 'Bilde'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js new file mode 100644 index 000000000..6370a3e2d --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Заранее форматированный текÑÑ‚'; +jsToolBar.strings['Wiki link'] = 'СÑылка на Ñтраницу в Wiki'; +jsToolBar.strings['Image'] = 'Ð’Ñтавка изображениÑ'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js new file mode 100644 index 000000000..1e46e2470 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'æ ¼å¼åŒ–文字'; +jsToolBar.strings['Wiki link'] = '連çµè‡³ Wiki é é¢'; +jsToolBar.strings['Image'] = '圖片'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js new file mode 100644 index 000000000..cd36a4b55 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js @@ -0,0 +1,14 @@ +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 to a Wiki page'; +jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/prototype.js b/groups/public/javascripts/prototype.js new file mode 100644 index 000000000..2735d10dc --- /dev/null +++ b/groups/public/javascripts/prototype.js @@ -0,0 +1,2515 @@ +/* Prototype JavaScript framework, version 1.5.0 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0', + BrowserFeatures: { + XPath: !!document.evaluate + }, + + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + emptyFunction: function() {}, + K: function(x) { return x } +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +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 === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + 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); + } +}); + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments))); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +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; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + 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); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(this); + } finally { + this.currentlyExecuting = false; + } + } + } +} +String.interpret = function(value){ + return value == null ? '' : String(value); +} + +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 = count === undefined ? 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 this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : 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 div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('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 name = decodeURIComponent(pair[0]); + var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; + + if (hash[name] !== undefined) { + if (hash[name].constructor != Array) + hash[name] = [hash[name]]; + if (value) hash[name].push(value); + } + else hash[name] = 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); + }, + + 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.replace(/\\/g, '\\\\'); + if (useDoubleQuotes) + return '"' + escapedString.replace(/"/g, '\\"') + '"'; + else + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + String.interpret(object[match[3]]); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator) { + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.map(iterator); + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = false; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push((iterator || Prototype.K)(value, index)); + }); + return results; + }, + + detect: function(iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(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) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.map(function(value, index) { + return {value: value, criteria: iterator(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 (typeof args.last() == 'function') + 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 '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0, length = iterable.length; i < length; i++) + results.push(iterable[i]); + return results; + } +} + +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(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0, length = this.length; i < length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function() { + return this.inject([], function(array, value) { + return array.include(value) ? array : array.concat([value]); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string){ + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if(window.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(arguments[i].constructor == Array) { + for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + } +} +var Hash = function(obj) { + Object.extend(this, obj || {}); +}; + +Object.extend(Hash, { + toQueryString: function(obj) { + var parts = []; + + this.prototype._each.call(obj, function(pair) { + if (!pair.key) return; + + if (pair.value && pair.value.constructor == Array) { + var values = pair.value.compact(); + if (values.length < 2) pair.value = values.reduce(); + else { + key = encodeURIComponent(pair.key); + values.each(function(value) { + value = value != undefined ? encodeURIComponent(value) : ''; + parts.push(key + '=' + encodeURIComponent(value)); + }); + return; + } + } + if (pair.value == undefined) pair[1] = ''; + parts.push(pair.map(encodeURIComponent).join('=')); + }); + + return parts.join('&'); + } +}); + +Object.extend(Hash.prototype, Enumerable); +Object.extend(Hash.prototype, { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (value && value == Hash.prototype[key]) continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject(this, function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + remove: function() { + var result; + for(var i = 0, length = arguments.length; i < length; i++) { + var value = this[arguments[i]]; + if (value !== undefined){ + if (result === undefined) result = value; + else { + if (result.constructor != Array) result = [result]; + result.push(value) + } + } + delete this[arguments[i]]; + } + return result; + }, + + toQueryString: function() { + return Hash.toQueryString(this); + }, + + inspect: function() { + return '#'; + } +}); + +function $H(object) { + if (object && object.constructor == Hash) return object; + return new Hash(object); +}; +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + 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 (typeof responder[callback] == 'function') { + 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 = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '' + } + Object.extend(this.options, options || {}); + + this.options.method = this.options.method.toLowerCase(); + if (typeof this.options.parameters == 'string') + this.options.parameters = this.options.parameters.toQueryParams(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + _complete: false, + + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = this.options.parameters; + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + params = Hash.toQueryString(params); + if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_=' + + // when GET, append parameters to URL + if (this.method == 'get' && params) + this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params; + + try { + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) + setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + var body = this.method == 'post' ? (this.options.postBody || params) : null; + + this.transport.send(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 (typeof extras.push == 'function') + 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() { + return !this.transport.status + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + this.transport.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.getHeader('Content-type') || 'text/javascript').strip(). + match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + state, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalJSON: function() { + try { + var json = this.getHeader('X-JSON'); + return json ? eval('(' + json + ')') : null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, param) { + this.updateContent(); + onComplete(transport, param); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.container[this.success() ? 'success' : 'failure']; + var response = this.transport.responseText; + + if (!this.options.evalScripts) response = response.stripScripts(); + + if (receiver = $(receiver)) { + if (this.options.insertion) + new this.options.insertion(receiver, response); + else + receiver.update(response); + } + + if (this.success()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(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(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + 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 (typeof element == 'string') + 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(query.snapshotItem(i)); + return results; + }; +} + +document.getElementsByClassName = function(className, parentElement) { + if (Prototype.BrowserFeatures.XPath) { + var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; + return document._getElementsByXPath(q, parentElement); + } else { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + var elements = [], child; + for (var i = 0, length = children.length; i < length; i++) { + child = children[i]; + if (Element.hasClassName(child, className)) + elements.push(Element.extend(child)); + } + return elements; + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element || _nativeExtensions || element.nodeType == 3) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Object.clone(Element.Methods), cache = Element.extend.cache; + + if (element.tagName == 'FORM') + Object.extend(methods, Form.Methods); + if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName)) + Object.extend(methods, Form.Element.Methods); + + Object.extend(methods, Element.Methods.Simulated); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +}; + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +}; + +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).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, html) { + html = typeof html == 'undefined' ? '' : html.toString(); + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + replace: function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + 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 $A($(element).getElementsByTagName('*')); + }, + + 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 (typeof selector == 'string') + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + return Selector.findElement($(element).ancestors(), expression, index); + }, + + down: function(element, expression, index) { + return Selector.findElement($(element).descendants(), expression, index); + }, + + previous: function(element, expression, index) { + return Selector.findElement($(element).previousSiblings(), expression, index); + }, + + next: function(element, expression, index) { + return Selector.findElement($(element).nextSiblings(), expression, index); + }, + + getElementsBySelector: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + getElementsByClassName: function(element, className) { + return document.getElementsByClassName(className, element); + }, + + readAttribute: function(element, name) { + element = $(element); + if (document.all && !window.opera) { + var t = Element._attributeTranslations; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + var attribute = element.attributes[name]; + if(attribute) return attribute.nodeValue; + } + return element.getAttribute(name); + }, + + 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; + if (elementClassName.length == 0) return false; + if (elementClassName == className || + elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + return true; + return false; + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).add(className); + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).remove(className); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className); + return element; + }, + + observe: function() { + Event.observe.apply(Event, arguments); + return $A(arguments).first(); + }, + + stopObserving: function() { + Event.stopObserving.apply(Event, arguments); + return $A(arguments).first(); + }, + + // 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.match(/^\s*$/); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = Position.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + if (['float','cssFloat'].include(style)) + style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat'); + style = style.camelize(); + var value = element.style[style]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } else if (element.currentStyle) { + value = element.currentStyle[style]; + } + } + + if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none')) + value = element['offset'+style.capitalize()] + 'px'; + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + if(style == 'opacity') { + if(value) return parseFloat(value); + if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) { + var value = style[name]; + if(name == 'opacity') { + if (value == 1) { + value = (/Gecko/.test(navigator.userAgent) && + !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else if(value == '') { + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')'; + } + } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat'; + element.style[name.camelize()] = 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 (window.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.style.overflow || 'auto'; + if ((Element.getStyle(element, 'overflow') || 'visible') != '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; + } +}; + +Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf}); + +Element._attributeTranslations = {}; + +Element._attributeTranslations.names = { + colspan: "colSpan", + rowspan: "rowSpan", + valign: "vAlign", + datetime: "dateTime", + accesskey: "accessKey", + tabindex: "tabIndex", + enctype: "encType", + maxlength: "maxLength", + readonly: "readOnly", + longdesc: "longDesc" +}; + +Element._attributeTranslations.values = { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + + title: function(element) { + var node = element.getAttributeNode('title'); + return node.specified ? node.nodeValue : null; + } +}; + +Object.extend(Element._attributeTranslations.values, { + href: Element._attributeTranslations.values._getAttr, + src: Element._attributeTranslations.values._getAttr, + disabled: Element._attributeTranslations.values._flag, + checked: Element._attributeTranslations.values._flag, + readonly: Element._attributeTranslations.values._flag, + multiple: Element._attributeTranslations.values._flag +}); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + var t = Element._attributeTranslations; + attribute = t.names[attribute] || attribute; + return $(element).getAttributeNode(attribute).specified; + } +}; + +// IE is missing .innerHTML support for TABLE-related elements +if (document.all && !window.opera){ + Element.Methods.update = function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + var tagName = element.tagName.toUpperCase(); + if (['THEAD','TBODY','TR','TD'].include(tagName)) { + var div = document.createElement('div'); + switch (tagName) { + case 'THEAD': + case 'TBODY': + div.innerHTML = '' + html.stripScripts() + '
    '; + depth = 2; + break; + case 'TR': + div.innerHTML = '' + html.stripScripts() + '
    '; + depth = 3; + break; + case 'TD': + div.innerHTML = '
    ' + html.stripScripts() + '
    '; + depth = 4; + } + $A(element.childNodes).each(function(node){ + element.removeChild(node) + }); + depth.times(function(){ div = div.firstChild }); + + $A(div.childNodes).each( + function(node){ element.appendChild(node) }); + } else { + element.innerHTML = html.stripScripts(); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + } +}; + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { + var className = 'HTML' + tag + 'Element'; + if(window[className]) return; + var klass = window[className] = {}; + klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; + }); + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + var cache = Element.extend.cache; + for (var property in methods) { + var value = methods[property]; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = cache.findOrStore(value); + } + } + + if (typeof HTMLElement != 'undefined') { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + copy(Form.Methods, HTMLFormElement.prototype); + [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) { + copy(Form.Element.Methods, klass.prototype); + }); + _nativeExtensions = true; + } +} + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toUpperCase(); + if (['TBODY', 'TR'].include(tagName)) { + this.insertContent(this.contentFromAnonymousTable()._reverse()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
    '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +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); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.readAttribute("id") == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0, length = clause.length; i < length; i++) + conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.readAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + element = $(element); \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0, length = scope.length; i < length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +Object.extend(Selector, { + matchElements: function(elements, expression) { + var selector = new Selector(expression); + return elements.select(selector.match.bind(selector)).map(Element.extend); + }, + + findElement: function(elements, expression, index) { + if (typeof expression == 'number') index = expression, expression = false; + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + return expressions.map(function(expression) { + return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.inject([], function(elements, result) { + return elements.concat(selector.findElements(result || element)); + }); + }); + }).flatten(); + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, getHash) { + var data = elements.inject({}, function(result, element) { + if (!element.disabled && element.name) { + var key = element.name, value = $(element).getValue(); + if (value != undefined) { + if (result[key]) { + if (result[key].constructor != Array) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return getHash ? data : Hash.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, getHash) { + return Form.serializeElements(Form.getElements(form), getHash); + }, + + 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().each(function(element) { + element.blur(); + element.disabled = 'true'; + }); + return form; + }, + + enable: function(form) { + form = $(form); + form.getElements().each(function(element) { + element.disabled = ''; + }); + return form; + }, + + findFirstElement: function(form) { + return $(form).getElements().find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + } +} + +Object.extend(Form, Form.Methods); + +/*--------------------------------------------------------------------------*/ + +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 Hash.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select && ( element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type) ) ) + element.select(); + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.blur(); + element.disabled = false; + return element; + } +} + +Object.extend(Form.Element, Form.Element.Methods); +var Field = Form.Element; +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + default: + return Form.Element.Serializers.textarea(element); + } + }, + + inputSelector: function(element) { + return element.checked ? element.value : null; + }, + + textarea: function(element) { + return element.value; + }, + + select: function(element) { + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + 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 = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + var changed = ('string' == typeof this.lastValue && 'string' == typeof value + ? this.lastValue != value : String(this.lastValue) != String(value)); + if (changed) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + 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.bind(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(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +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, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0, length = Event.observers.length; i < length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + Event._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + try { + element.detachEvent('on' + name, observer); + } catch (e) {} + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +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; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [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=='BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: 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; + }, + + // 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 = this.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 = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.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; + }, + + page: 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) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!window.opera || element.tagName=='BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + 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 = Position.page(source); + + // find coordinate system to use + target = $(target); + 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(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // 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) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + 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'; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + 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; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.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 [valueL, valueT]; + } +} + +Element.addMethods(); \ No newline at end of file diff --git a/groups/public/javascripts/select_list_move.js b/groups/public/javascripts/select_list_move.js new file mode 100644 index 000000000..1ced88232 --- /dev/null +++ b/groups/public/javascripts/select_list_move.js @@ -0,0 +1,55 @@ +var NS4 = (navigator.appName == "Netscape" && parseInt(navigator.appVersion) < 5); + +function addOption(theSel, theText, theValue) +{ + var newOpt = new Option(theText, theValue); + var selLength = theSel.length; + theSel.options[selLength] = newOpt; +} + +function deleteOption(theSel, theIndex) +{ + var selLength = theSel.length; + if(selLength>0) + { + theSel.options[theIndex] = null; + } +} + +function moveOptions(theSelFrom, theSelTo) +{ + + var selLength = theSelFrom.length; + var selectedText = new Array(); + var selectedValues = new Array(); + var selectedCount = 0; + + var i; + + for(i=selLength-1; i>=0; i--) + { + if(theSelFrom.options[i].selected) + { + selectedText[selectedCount] = theSelFrom.options[i].text; + selectedValues[selectedCount] = theSelFrom.options[i].value; + deleteOption(theSelFrom, i); + selectedCount++; + } + } + + for(i=selectedCount-1; i>=0; i--) + { + addOption(theSelTo, selectedText[i], selectedValues[i]); + } + + if(NS4) history.go(0); +} + +function selectAllOptions(id) +{ + var select = $(id); + for (var i=0; ibody #content { height: auto; min-height: 600px; overflow: auto; } + +#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;} + +.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, .issue.closed a { 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 { overflow: hidden; vertical-align: top;} +table.list td.id { width: 2%; text-align: center;} +table.list td.checkbox { width: 15px; padding: 0px;} + +table.list.issues { margin-top: 10px; } +tr.issue { text-align: center; white-space: nowrap; } +tr.issue td.subject, tr.issue td.category { 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.changeset td.author { text-align: center; width: 15%; } +tr.changeset td.committed_on { text-align: center; width: 15%; } + +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.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.list tbody tr:hover { background-color:#ffffdd; } +table td {padding:2px;} +table p {margin:0;} +.odd {background-color:#f6f7f8;} +.even {background-color: #fff;} + +.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 {font-size:0.9em;} + +.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;} +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; } + +fieldset#filters { padding: 0.7em; } +fieldset#filters p { margin: 1.2em 0 0.8em 2px; } +fieldset#filters .buttons { font-size: 0.9em; } +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; } + +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 { margin-left: 2em; } +div#activity dd { margin-bottom: 1em; padding-left: 18px; } +div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt .time { color: #777; font-size: 80%; } +div#activity dd .description { font-style: italic; } +div#activity span.project:after { content: " -"; } +div#activity dt.issue { background-image: url(../images/ticket.png); } +div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); } +div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); } +div#activity dt.changeset { background-image: url(../images/changeset.png); } +div#activity dt.news { background-image: url(../images/news.png); } +div#activity dt.message { background-image: url(../images/message.png); } +div#activity dt.reply { background-image: url(../images/comments.png); } +div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); } +div#activity dt.attachment { background-image: url(../images/attachment.png); } +div#activity dt.document { background-image: url(../images/document.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; } + +.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; +} + +.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: 200px; +} + +#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 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.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } + +/***** 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; +} + +.nodata, .warning { + text-align: center; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; +} + +#errorExplanation ul { font-size: 0.9em;} + +/***** 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: 8px 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; +} + +/***** 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 div.toc { + background-color: #ffffdd; + border: 1px solid #e4e4e4; + padding: 4px; + line-height: 1.2em; + margin-bottom: 12px; + margin-right: 12px; + display: table +} +* html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */ + +div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } +div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } + +div.wiki div.toc a { + display: block; + font-size: 0.9em; + font-weight: normal; + text-decoration: none; + color: #606060; +} +div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;} + +div.wiki div.toc a.heading2 { margin-left: 6px; } +div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; } + +/***** 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-file { background-image: url(../images/file.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-logout { background-image: url(../images/logout.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-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); } + +.icon22-projects { background-image: url(../images/22x22/projects.png); } +.icon22-users { background-image: url(../images/22x22/users.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); } + +/***** 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; } +} diff --git a/groups/public/stylesheets/calendar.css b/groups/public/stylesheets/calendar.css new file mode 100644 index 000000000..c8d2dd619 --- /dev/null +++ b/groups/public/stylesheets/calendar.css @@ -0,0 +1,237 @@ +/* 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 containing navigation buttons */ + background: #467aa7; + color: #fff; +} + +.calendar thead .daynames { /* Row containing the day names */ + background: #bdf; +} + +.calendar thead .name { /* Cells 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 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 */ + background: #80b0da; + padding: 1px 3px 1px 1px; + border: 1px solid #bbb; +} + +.calendar tbody td.active { /* Active (pressed) cells */ + 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 in footer (only one right now) */ + text-align: center; + background: #556; + color: #fff; +} + +.calendar tfoot .ttip { /* Tooltip (status bar) cell */ + 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; +} diff --git a/groups/public/stylesheets/context_menu.css b/groups/public/stylesheets/context_menu.css new file mode 100644 index 000000000..e5a83be0d --- /dev/null +++ b/groups/public/stylesheets/context_menu.css @@ -0,0 +1,52 @@ +#context-menu { position: absolute; z-index: 10;} + +#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:9; +} +#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; + background-repeat: no-repeat; + background-position: 1px 50%; + padding: 2px 0px 2px 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:10; } +#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; } diff --git a/groups/public/stylesheets/csshover.htc b/groups/public/stylesheets/csshover.htc new file mode 100644 index 000000000..5b58bf0f4 --- /dev/null +++ b/groups/public/stylesheets/csshover.htc @@ -0,0 +1,122 @@ + + \ No newline at end of file diff --git a/groups/public/stylesheets/jstoolbar.css b/groups/public/stylesheets/jstoolbar.css new file mode 100644 index 000000000..c4ab55711 --- /dev/null +++ b/groups/public/stylesheets/jstoolbar.css @@ -0,0 +1,95 @@ +.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_quote { + background-image: url(../images/jstoolbar/bt_quote.png); +} +.jstb_code { + background-image: url(../images/jstoolbar/bt_code.png); +} +.jstb_br { + background-image: url(../images/jstoolbar/bt_br.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_pre { + background-image: url(../images/jstoolbar/bt_pre.png); +} +.jstb_link { + background-image: url(../images/jstoolbar/bt_link.png); +} +.jstb_img { + background-image: url(../images/jstoolbar/bt_img.png); +} diff --git a/groups/public/stylesheets/scm.css b/groups/public/stylesheets/scm.css new file mode 100644 index 000000000..66847af8c --- /dev/null +++ b/groups/public/stylesheets/scm.css @@ -0,0 +1,150 @@ + +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: #ddc; text-align: left; } +table.filecontent tr.spacing { border: 1px solid #d7d7d7; } +table.filecontent th.line-num { + border: 1px solid #d7d7d7; + font-size: 0.8em; + text-align: right; + width: 2%; + padding-right: 3px; +} +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 } diff --git a/groups/public/themes/README b/groups/public/themes/README new file mode 100644 index 000000000..1af3d1992 --- /dev/null +++ b/groups/public/themes/README @@ -0,0 +1 @@ +Put your Redmine themes here. diff --git a/groups/public/themes/alternate/stylesheets/application.css b/groups/public/themes/alternate/stylesheets/application.css new file mode 100644 index 000000000..19cbc0061 --- /dev/null +++ b/groups/public/themes/alternate/stylesheets/application.css @@ -0,0 +1,70 @@ +@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 { 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"], textarea, select { padding: 2px; border: 1px solid #d7d7d7; } +input[type="text"] { padding: 3px; } +input[type="text"]:focus, textarea:focus, select:focus { border: 1px solid #888866; } +option { border-bottom: 1px dotted #d7d7d7; } + +/* Misc */ +.box { background-color: #fcfcfc; } diff --git a/groups/public/themes/classic/stylesheets/application.css b/groups/public/themes/classic/stylesheets/application.css new file mode 100644 index 000000000..57f911da6 --- /dev/null +++ b/groups/public/themes/classic/stylesheets/application.css @@ -0,0 +1,41 @@ +@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.mypage, #top-menu a.projects, #top-menu a.admin, #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.mypage { background-image: url(../../../images/user_page.png); } +#top-menu a.projects { background-image: url(../../../images/projects.png); } +#top-menu a.admin { background-image: url(../../../images/admin.png); } +#top-menu a.help { background-image: url(../../../images/help.png); } diff --git a/groups/script/about b/groups/script/about new file mode 100755 index 000000000..7b07d46a3 --- /dev/null +++ b/groups/script/about @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/about' \ No newline at end of file diff --git a/groups/script/breakpointer b/groups/script/breakpointer new file mode 100755 index 000000000..64af76edd --- /dev/null +++ b/groups/script/breakpointer @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/breakpointer' \ No newline at end of file diff --git a/groups/script/console b/groups/script/console new file mode 100755 index 000000000..42f28f7d6 --- /dev/null +++ b/groups/script/console @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/console' \ No newline at end of file diff --git a/groups/script/destroy b/groups/script/destroy new file mode 100755 index 000000000..fa0e6fcd0 --- /dev/null +++ b/groups/script/destroy @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/destroy' \ No newline at end of file diff --git a/groups/script/generate b/groups/script/generate new file mode 100755 index 000000000..ef976e09f --- /dev/null +++ b/groups/script/generate @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/generate' \ No newline at end of file diff --git a/groups/script/performance/benchmarker b/groups/script/performance/benchmarker new file mode 100755 index 000000000..c842d35d3 --- /dev/null +++ b/groups/script/performance/benchmarker @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/benchmarker' diff --git a/groups/script/performance/profiler b/groups/script/performance/profiler new file mode 100755 index 000000000..d855ac8b1 --- /dev/null +++ b/groups/script/performance/profiler @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/profiler' diff --git a/groups/script/plugin b/groups/script/plugin new file mode 100755 index 000000000..26ca64c06 --- /dev/null +++ b/groups/script/plugin @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/plugin' \ No newline at end of file diff --git a/groups/script/process/reaper b/groups/script/process/reaper new file mode 100755 index 000000000..c77f04535 --- /dev/null +++ b/groups/script/process/reaper @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/reaper' diff --git a/groups/script/process/spawner b/groups/script/process/spawner new file mode 100755 index 000000000..7118f3983 --- /dev/null +++ b/groups/script/process/spawner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/spawner' diff --git a/groups/script/process/spinner b/groups/script/process/spinner new file mode 100755 index 000000000..6816b32ef --- /dev/null +++ b/groups/script/process/spinner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/spinner' diff --git a/groups/script/runner b/groups/script/runner new file mode 100755 index 000000000..ccc30f9d2 --- /dev/null +++ b/groups/script/runner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/runner' \ No newline at end of file diff --git a/groups/script/server b/groups/script/server new file mode 100755 index 000000000..dfabcb881 --- /dev/null +++ b/groups/script/server @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/server' \ No newline at end of file diff --git a/groups/test/fixtures/attachments.yml b/groups/test/fixtures/attachments.yml new file mode 100644 index 000000000..162d44720 --- /dev/null +++ b/groups/test/fixtures/attachments.yml @@ -0,0 +1,39 @@ +--- +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 + \ No newline at end of file diff --git a/groups/test/fixtures/auth_sources.yml b/groups/test/fixtures/auth_sources.yml new file mode 100644 index 000000000..086c00f62 --- /dev/null +++ b/groups/test/fixtures/auth_sources.yml @@ -0,0 +1,2 @@ +--- {} + diff --git a/groups/test/fixtures/boards.yml b/groups/test/fixtures/boards.yml new file mode 100644 index 000000000..b6b42aaa3 --- /dev/null +++ b/groups/test/fixtures/boards.yml @@ -0,0 +1,19 @@ +--- +boards_001: + name: Help + project_id: 1 + topics_count: 2 + id: 1 + description: Help board + position: 1 + last_message_id: 5 + messages_count: 5 +boards_002: + name: Discussion + project_id: 1 + topics_count: 0 + id: 2 + description: Discussion board + position: 2 + last_message_id: + messages_count: 0 diff --git a/groups/test/fixtures/changes.yml b/groups/test/fixtures/changes.yml new file mode 100644 index 000000000..30acbd02d --- /dev/null +++ b/groups/test/fixtures/changes.yml @@ -0,0 +1,16 @@ +--- +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: + \ No newline at end of file diff --git a/groups/test/fixtures/changesets.yml b/groups/test/fixtures/changesets.yml new file mode 100644 index 000000000..3b47eecd8 --- /dev/null +++ b/groups/test/fixtures/changesets.yml @@ -0,0 +1,38 @@ +--- +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 +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 +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 +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 + \ No newline at end of file diff --git a/groups/test/fixtures/comments.yml b/groups/test/fixtures/comments.yml new file mode 100644 index 000000000..538f67ed3 --- /dev/null +++ b/groups/test/fixtures/comments.yml @@ -0,0 +1,18 @@ +# 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 diff --git a/groups/test/fixtures/custom_fields.yml b/groups/test/fixtures/custom_fields.yml new file mode 100644 index 000000000..6be840fcc --- /dev/null +++ b/groups/test/fixtures/custom_fields.yml @@ -0,0 +1,63 @@ +--- +custom_fields_001: + name: Database + min_length: 0 + regexp: "" + is_for_all: true + type: IssueCustomField + max_length: 0 + possible_values: MySQL|PostgreSQL|Oracle + id: 1 + is_required: false + field_format: list + default_value: "" +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" +custom_fields_003: + name: Development status + min_length: 0 + regexp: "" + is_for_all: false + type: ProjectCustomField + max_length: 0 + possible_values: Stable|Beta|Alpha|Planning + id: 3 + is_required: true + field_format: list + default_value: "" +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: "" +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: "" + \ No newline at end of file diff --git a/groups/test/fixtures/custom_fields_projects.yml b/groups/test/fixtures/custom_fields_projects.yml new file mode 100644 index 000000000..086c00f62 --- /dev/null +++ b/groups/test/fixtures/custom_fields_projects.yml @@ -0,0 +1,2 @@ +--- {} + diff --git a/groups/test/fixtures/custom_fields_trackers.yml b/groups/test/fixtures/custom_fields_trackers.yml new file mode 100644 index 000000000..cb06d2fcf --- /dev/null +++ b/groups/test/fixtures/custom_fields_trackers.yml @@ -0,0 +1,10 @@ +--- +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 diff --git a/groups/test/fixtures/custom_values.yml b/groups/test/fixtures/custom_values.yml new file mode 100644 index 000000000..572142889 --- /dev/null +++ b/groups/test/fixtures/custom_values.yml @@ -0,0 +1,56 @@ +--- +custom_values_006: + customized_type: Issue + custom_field_id: 2 + customized_id: 3 + id: 9 + value: "125" +custom_values_007: + customized_type: Project + custom_field_id: 3 + customized_id: 1 + id: 10 + value: Stable +custom_values_001: + customized_type: User + custom_field_id: 4 + customized_id: 3 + id: 2 + value: "" +custom_values_002: + customized_type: User + custom_field_id: 4 + customized_id: 4 + id: 3 + value: 01 23 45 67 89 +custom_values_003: + customized_type: User + custom_field_id: 4 + customized_id: 2 + id: 4 + value: "" +custom_values_004: + customized_type: Issue + custom_field_id: 2 + customized_id: 1 + id: 7 + value: "125" +custom_values_005: + customized_type: Issue + custom_field_id: 2 + customized_id: 2 + id: 8 + value: "" +custom_values_008: + customized_type: Issue + custom_field_id: 1 + customized_id: 3 + id: 11 + value: "MySQL" +custom_values_009: + customized_type: Issue + custom_field_id: 2 + customized_id: 3 + id: 12 + value: "this is a stringforcustomfield search" + \ No newline at end of file diff --git a/groups/test/fixtures/documents.yml b/groups/test/fixtures/documents.yml new file mode 100644 index 000000000..0dbca2a4f --- /dev/null +++ b/groups/test/fixtures/documents.yml @@ -0,0 +1,7 @@ +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 diff --git a/groups/test/fixtures/enabled_modules.yml b/groups/test/fixtures/enabled_modules.yml new file mode 100644 index 000000000..8d1565534 --- /dev/null +++ b/groups/test/fixtures/enabled_modules.yml @@ -0,0 +1,42 @@ +--- +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 + \ No newline at end of file diff --git a/groups/test/fixtures/enumerations.yml b/groups/test/fixtures/enumerations.yml new file mode 100644 index 000000000..c90a997ee --- /dev/null +++ b/groups/test/fixtures/enumerations.yml @@ -0,0 +1,42 @@ +--- +enumerations_001: + name: Uncategorized + id: 1 + opt: DCAT +enumerations_002: + name: User documentation + id: 2 + opt: DCAT +enumerations_003: + name: Technical documentation + id: 3 + opt: DCAT +enumerations_004: + name: Low + id: 4 + opt: IPRI +enumerations_005: + name: Normal + id: 5 + opt: IPRI +enumerations_006: + name: High + id: 6 + opt: IPRI +enumerations_007: + name: Urgent + id: 7 + opt: IPRI +enumerations_008: + name: Immediate + id: 8 + opt: IPRI +enumerations_009: + name: Design + id: 9 + opt: ACTI +enumerations_010: + name: Development + id: 10 + opt: ACTI + \ No newline at end of file diff --git a/groups/test/fixtures/files/testfile.txt b/groups/test/fixtures/files/testfile.txt new file mode 100644 index 000000000..4b2a49c69 --- /dev/null +++ b/groups/test/fixtures/files/testfile.txt @@ -0,0 +1 @@ +this is a text file for upload tests \ No newline at end of file diff --git a/groups/test/fixtures/issue_categories.yml b/groups/test/fixtures/issue_categories.yml new file mode 100644 index 000000000..6c2a07b58 --- /dev/null +++ b/groups/test/fixtures/issue_categories.yml @@ -0,0 +1,11 @@ +--- +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 diff --git a/groups/test/fixtures/issue_statuses.yml b/groups/test/fixtures/issue_statuses.yml new file mode 100644 index 000000000..c7b10ba07 --- /dev/null +++ b/groups/test/fixtures/issue_statuses.yml @@ -0,0 +1,31 @@ +--- +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 diff --git a/groups/test/fixtures/issues.yml b/groups/test/fixtures/issues.yml new file mode 100644 index 000000000..4f42d93c4 --- /dev/null +++ b/groups/test/fixtures/issues.yml @@ -0,0 +1,74 @@ +--- +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 +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: + 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 +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: + 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: + 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 + diff --git a/groups/test/fixtures/journal_details.yml b/groups/test/fixtures/journal_details.yml new file mode 100644 index 000000000..058abd112 --- /dev/null +++ b/groups/test/fixtures/journal_details.yml @@ -0,0 +1,15 @@ +--- +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 diff --git a/groups/test/fixtures/journals.yml b/groups/test/fixtures/journals.yml new file mode 100644 index 000000000..70aa5da73 --- /dev/null +++ b/groups/test/fixtures/journals.yml @@ -0,0 +1,16 @@ +--- +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 + \ No newline at end of file diff --git a/groups/test/fixtures/mail_handler/add_note_to_issue.txt b/groups/test/fixtures/mail_handler/add_note_to_issue.txt new file mode 100644 index 000000000..4fc6b68fb --- /dev/null +++ b/groups/test/fixtures/mail_handler/add_note_to_issue.txt @@ -0,0 +1,14 @@ +x-sender: +x-receiver: +Received: from somenet.foo ([127.0.0.1]) by somenet.foo; + Sun, 25 Feb 2007 09:57:56 GMT +Date: Sun, 25 Feb 2007 10:57:56 +0100 +From: jsmith@somenet.foo +To: redmine@somenet.foo +Message-Id: <45e15df440c00_b90238570a27b@osiris.tmail> +In-Reply-To: <45e15df440c29_b90238570a27b@osiris.tmail> +Subject: [Cookbook - Feature #2] +Mime-Version: 1.0 +Content-Type: text/plain; charset=utf-8 + +Note added by mail diff --git a/groups/test/fixtures/members.yml b/groups/test/fixtures/members.yml new file mode 100644 index 000000000..2c9209131 --- /dev/null +++ b/groups/test/fixtures/members.yml @@ -0,0 +1,27 @@ +--- +members_001: + created_on: 2006-07-19 19:35:33 +02:00 + project_id: 1 + role_id: 1 + id: 1 + user_id: 2 +members_002: + created_on: 2006-07-19 19:35:36 +02:00 + project_id: 1 + role_id: 2 + id: 2 + user_id: 3 +members_003: + created_on: 2006-07-19 19:35:36 +02:00 + project_id: 2 + role_id: 2 + id: 3 + user_id: 2 +members_004: + id: 4 + created_on: 2006-07-19 19:35:36 +02:00 + project_id: 1 + role_id: 2 + # Locked user + user_id: 5 + \ No newline at end of file diff --git a/groups/test/fixtures/messages.yml b/groups/test/fixtures/messages.yml new file mode 100644 index 000000000..f82f376c1 --- /dev/null +++ b/groups/test/fixtures/messages.yml @@ -0,0 +1,57 @@ +--- +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: + 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: 1 + last_reply_id: 5 + 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 diff --git a/groups/test/fixtures/news.yml b/groups/test/fixtures/news.yml new file mode 100644 index 000000000..2c2e2c134 --- /dev/null +++ b/groups/test/fixtures/news.yml @@ -0,0 +1,22 @@ +--- +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 diff --git a/groups/test/fixtures/projects.yml b/groups/test/fixtures/projects.yml new file mode 100644 index 000000000..ad5cf4aa2 --- /dev/null +++ b/groups/test/fixtures/projects.yml @@ -0,0 +1,45 @@ +--- +projects_001: + created_on: 2006-07-19 19:13:59 +02:00 + name: eCookbook + updated_on: 2006-07-19 22:53:01 +02:00 + projects_count: 2 + id: 1 + description: Recipes management application + homepage: http://ecookbook.somenet.foo/ + is_public: true + identifier: ecookbook + parent_id: +projects_002: + created_on: 2006-07-19 19:14:19 +02:00 + name: OnlineStore + updated_on: 2006-07-19 19:14:19 +02:00 + projects_count: 0 + id: 2 + description: E-commerce web site + homepage: "" + is_public: false + identifier: onlinestore + parent_id: +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 + projects_count: 0 + id: 3 + description: eCookBook Subproject 1 + homepage: "" + is_public: true + identifier: subproject1 + parent_id: 1 +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 + projects_count: 0 + id: 4 + description: eCookbook Subproject 2 + homepage: "" + is_public: true + identifier: subproject2 + parent_id: 1 diff --git a/groups/test/fixtures/projects_trackers.yml b/groups/test/fixtures/projects_trackers.yml new file mode 100644 index 000000000..8eb7d85ab --- /dev/null +++ b/groups/test/fixtures/projects_trackers.yml @@ -0,0 +1,40 @@ +--- +projects_trackers_012: + project_id: 4 + tracker_id: 3 +projects_trackers_001: + project_id: 1 + tracker_id: 1 +projects_trackers_013: + project_id: 5 + tracker_id: 1 +projects_trackers_002: + project_id: 1 + tracker_id: 2 +projects_trackers_014: + project_id: 5 + tracker_id: 2 +projects_trackers_015: + project_id: 5 + tracker_id: 3 +projects_trackers_004: + project_id: 2 + tracker_id: 1 +projects_trackers_005: + project_id: 2 + tracker_id: 2 +projects_trackers_006: + project_id: 2 + tracker_id: 3 +projects_trackers_008: + project_id: 3 + tracker_id: 2 +projects_trackers_009: + project_id: 3 + tracker_id: 3 +projects_trackers_010: + project_id: 4 + tracker_id: 1 +projects_trackers_011: + project_id: 4 + tracker_id: 2 diff --git a/groups/test/fixtures/queries.yml b/groups/test/fixtures/queries.yml new file mode 100644 index 000000000..f12022729 --- /dev/null +++ b/groups/test/fixtures/queries.yml @@ -0,0 +1,69 @@ +--- +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: diff --git a/groups/test/fixtures/repositories.yml b/groups/test/fixtures/repositories.yml new file mode 100644 index 000000000..d86e301c9 --- /dev/null +++ b/groups/test/fixtures/repositories.yml @@ -0,0 +1,17 @@ +--- +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 diff --git a/groups/test/fixtures/repositories/bazaar_repository.tar.gz b/groups/test/fixtures/repositories/bazaar_repository.tar.gz new file mode 100644 index 000000000..621c2f145 Binary files /dev/null and b/groups/test/fixtures/repositories/bazaar_repository.tar.gz differ diff --git a/groups/test/fixtures/repositories/cvs_repository.tar.gz b/groups/test/fixtures/repositories/cvs_repository.tar.gz new file mode 100644 index 000000000..638b166b5 Binary files /dev/null and b/groups/test/fixtures/repositories/cvs_repository.tar.gz differ diff --git a/groups/test/fixtures/repositories/darcs_repository.tar.gz b/groups/test/fixtures/repositories/darcs_repository.tar.gz new file mode 100644 index 000000000..ba4d8d04c Binary files /dev/null and b/groups/test/fixtures/repositories/darcs_repository.tar.gz differ diff --git a/groups/test/fixtures/repositories/git_repository.tar.gz b/groups/test/fixtures/repositories/git_repository.tar.gz new file mode 100644 index 000000000..84de88aa7 Binary files /dev/null and b/groups/test/fixtures/repositories/git_repository.tar.gz differ diff --git a/groups/test/fixtures/repositories/mercurial_repository.tar.gz b/groups/test/fixtures/repositories/mercurial_repository.tar.gz new file mode 100644 index 000000000..1d8ad3057 Binary files /dev/null and b/groups/test/fixtures/repositories/mercurial_repository.tar.gz differ diff --git a/groups/test/fixtures/repositories/subversion_repository.dump.gz b/groups/test/fixtures/repositories/subversion_repository.dump.gz new file mode 100644 index 000000000..997835048 Binary files /dev/null and b/groups/test/fixtures/repositories/subversion_repository.dump.gz differ diff --git a/groups/test/fixtures/roles.yml b/groups/test/fixtures/roles.yml new file mode 100644 index 000000000..1ede6fca9 --- /dev/null +++ b/groups/test/fixtures/roles.yml @@ -0,0 +1,161 @@ +--- +roles_001: + name: Manager + id: 1 + builtin: 0 + permissions: | + --- + - :edit_project + - :manage_members + - :manage_versions + - :manage_categories + - :add_issues + - :edit_issues + - :manage_issue_relations + - :add_issue_notes + - :move_issues + - :delete_issues + - :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 + - :edit_wiki_pages + - :delete_wiki_pages + - :rename_wiki_pages + - :add_messages + - :edit_messages + - :delete_messages + - :manage_boards + - :view_files + - :manage_files + - :browse_repository + - :view_changesets + + position: 1 +roles_002: + name: Developer + id: 2 + builtin: 0 + permissions: | + --- + - :edit_project + - :manage_members + - :manage_versions + - :manage_categories + - :add_issues + - :edit_issues + - :manage_issue_relations + - :add_issue_notes + - :move_issues + - :delete_issues + - :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 + - :edit_wiki_pages + - :delete_wiki_pages + - :add_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 + - :add_issues + - :edit_issues + - :manage_issue_relations + - :add_issue_notes + - :move_issues + - :save_queries + - :view_gantt + - :view_calendar + - :log_time + - :view_time_entries + - :manage_news + - :comment_news + - :view_documents + - :manage_documents + - :view_wiki_pages + - :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: | + --- + - :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 + - :edit_wiki_pages + - :add_messages + - :view_files + - :manage_files + - :browse_repository + - :view_changesets + + position: 4 +roles_005: + name: Anonymous + id: 5 + builtin: 2 + permissions: | + --- + - :add_issue_notes + - :view_gantt + - :view_calendar + - :view_time_entries + - :view_documents + - :view_wiki_pages + - :view_files + - :browse_repository + - :view_changesets + + position: 5 + \ No newline at end of file diff --git a/groups/test/fixtures/time_entries.yml b/groups/test/fixtures/time_entries.yml new file mode 100644 index 000000000..4a8a4a2a4 --- /dev/null +++ b/groups/test/fixtures/time_entries.yml @@ -0,0 +1,58 @@ +--- +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 + \ No newline at end of file diff --git a/groups/test/fixtures/tokens.yml b/groups/test/fixtures/tokens.yml new file mode 100644 index 000000000..e040a39e9 --- /dev/null +++ b/groups/test/fixtures/tokens.yml @@ -0,0 +1,13 @@ +--- +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 diff --git a/groups/test/fixtures/trackers.yml b/groups/test/fixtures/trackers.yml new file mode 100644 index 000000000..2643e8d1a --- /dev/null +++ b/groups/test/fixtures/trackers.yml @@ -0,0 +1,16 @@ +--- +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 diff --git a/groups/test/fixtures/user_preferences.yml b/groups/test/fixtures/user_preferences.yml new file mode 100644 index 000000000..b9ba37765 --- /dev/null +++ b/groups/test/fixtures/user_preferences.yml @@ -0,0 +1,24 @@ +--- +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 diff --git a/groups/test/fixtures/users.yml b/groups/test/fixtures/users.yml new file mode 100644 index 000000000..de3553173 --- /dev/null +++ b/groups/test/fixtures/users.yml @@ -0,0 +1,100 @@ +--- +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: 1 + 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 + + \ No newline at end of file diff --git a/groups/test/fixtures/versions.yml b/groups/test/fixtures/versions.yml new file mode 100644 index 000000000..bf08660d5 --- /dev/null +++ b/groups/test/fixtures/versions.yml @@ -0,0 +1,26 @@ +--- +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 +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: 2006-07-19 +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: + \ No newline at end of file diff --git a/groups/test/fixtures/wiki_content_versions.yml b/groups/test/fixtures/wiki_content_versions.yml new file mode 100644 index 000000000..260149060 --- /dev/null +++ b/groups/test/fixtures/wiki_content_versions.yml @@ -0,0 +1,52 @@ +--- +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: + diff --git a/groups/test/fixtures/wiki_contents.yml b/groups/test/fixtures/wiki_contents.yml new file mode 100644 index 000000000..5d6d3f1de --- /dev/null +++ b/groups/test/fixtures/wiki_contents.yml @@ -0,0 +1,50 @@ +--- +wiki_contents_001: + text: |- + h1. CookBook documentation + + 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: + \ No newline at end of file diff --git a/groups/test/fixtures/wiki_pages.yml b/groups/test/fixtures/wiki_pages.yml new file mode 100644 index 000000000..f89832e44 --- /dev/null +++ b/groups/test/fixtures/wiki_pages.yml @@ -0,0 +1,22 @@ +--- +wiki_pages_001: + created_on: 2007-03-07 00:08:07 +01:00 + title: CookBook_documentation + id: 1 + wiki_id: 1 +wiki_pages_002: + created_on: 2007-03-08 00:18:07 +01:00 + title: Another_page + id: 2 + wiki_id: 1 +wiki_pages_003: + created_on: 2007-03-08 00:18:07 +01:00 + title: Start_page + id: 3 + wiki_id: 2 +wiki_pages_004: + created_on: 2007-03-08 00:18:07 +01:00 + title: Page_with_an_inline_image + id: 4 + wiki_id: 1 + \ No newline at end of file diff --git a/groups/test/fixtures/wikis.yml b/groups/test/fixtures/wikis.yml new file mode 100644 index 000000000..dd1c55cea --- /dev/null +++ b/groups/test/fixtures/wikis.yml @@ -0,0 +1,12 @@ +--- +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 diff --git a/groups/test/fixtures/workflows.yml b/groups/test/fixtures/workflows.yml new file mode 100644 index 000000000..bef5ae016 --- /dev/null +++ b/groups/test/fixtures/workflows.yml @@ -0,0 +1,1615 @@ +--- +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 diff --git a/groups/test/functional/account_controller_test.rb b/groups/test/functional/account_controller_test.rb new file mode 100644 index 000000000..666acf0dd --- /dev/null +++ b/groups/test/functional/account_controller_test.rb @@ -0,0 +1,73 @@ +# 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 < Test::Unit::TestCase + fixtures :users + + def setup + @controller = AccountController.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(:user) + end + + def test_show_inactive + get :show, :id => 5 + assert_response 404 + assert_nil assigns(:user) + 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 + + def test_autologin + Setting.autologin = "7" + Token.delete_all + post :login, :username => 'admin', :password => 'admin', :autologin => 1 + assert_redirected_to 'my/page' + token = Token.find :first + assert_not_nil token + assert_equal User.find_by_login('admin'), token.user + assert_equal 'autologin', token.action + end + + def test_logout + @request.session[:user_id] = 2 + get :logout + assert_redirected_to '' + assert_nil @request.session[:user_id] + end +end diff --git a/groups/test/functional/admin_controller_test.rb b/groups/test/functional/admin_controller_test.rb new file mode 100644 index 000000000..05205b399 --- /dev/null +++ b/groups/test/functional/admin_controller_test.rb @@ -0,0 +1,75 @@ +# 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 < Test::Unit::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_index_with_no_configuration_data + delete_configuration_data + get :index + assert_tag :tag => 'div', + :attributes => { :class => /nodata/ } + 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' + mail = ActionMailer::Base.deliveries.last + assert_kind_of TMail::Mail, mail + user = User.find(1) + assert_equal [user.mail], mail.bcc + end + + def test_info + get :info + assert_response :success + assert_template 'info' + end + + def delete_configuration_data + Role.delete_all('builtin = 0') + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + end +end diff --git a/groups/test/functional/application_controller_test.rb b/groups/test/functional/application_controller_test.rb new file mode 100644 index 000000000..6fcf8fe9a --- /dev/null +++ b/groups/test/functional/application_controller_test.rb @@ -0,0 +1,40 @@ +# 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' + +# Re-raise errors caught by the controller. +class ApplicationController; def rescue_action(e) raise e end; end + +class ApplicationControllerTest < Test::Unit::TestCase + 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}/lang/*.yml"].size + assert_equal lang_files_count, GLoc.valid_languages.size + GLoc.valid_languages.each do |lang| + assert set_language_if_valid(lang) + end + set_language_if_valid('en') + end +end diff --git a/groups/test/functional/boards_controller_test.rb b/groups/test/functional/boards_controller_test.rb new file mode 100644 index 000000000..3ff71bc4e --- /dev/null +++ b/groups/test/functional/boards_controller_test.rb @@ -0,0 +1,50 @@ +# 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 'boards_controller' + +# Re-raise errors caught by the controller. +class BoardsController; def rescue_action(e) raise e end; end + +class BoardsControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :members, :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 + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:boards) + assert_not_nil assigns(:project) + 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 +end diff --git a/groups/test/functional/documents_controller_test.rb b/groups/test/functional/documents_controller_test.rb new file mode 100644 index 000000000..f150a5b7a --- /dev/null +++ b/groups/test/functional/documents_controller_test.rb @@ -0,0 +1,64 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :enabled_modules, :documents, :enumerations + + def setup + @controller = DocumentsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_index + get :index, :project_id => 'ecookbook' + assert_response :success + assert_template 'index' + assert_not_nil assigns(:grouped) + end + + def test_new_with_one_attachment + @request.session[:user_id] = 2 + post :new, :project_id => 'ecookbook', + :document => { :title => 'DocumentsControllerTest#test_post_new', + :description => 'This is a new document', + :category_id => 2}, + :attachments => {'1' => {'file' => test_uploaded_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 + 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 diff --git a/groups/test/functional/issues_controller_test.rb b/groups/test/functional/issues_controller_test.rb new file mode 100644 index 000000000..042a8f3f2 --- /dev/null +++ b/groups/test/functional/issues_controller_test.rb @@ -0,0 +1,507 @@ +# 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 'issues_controller' + +# Re-raise errors caught by the controller. +class IssuesController; def rescue_action(e) raise e end; end + +class IssuesControllerTest < Test::Unit::TestCase + fixtures :projects, + :users, + :roles, + :members, + :issues, + :issue_statuses, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_trackers, + :time_entries + + def setup + @controller = IssuesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_index + get :index + assert_response :success + assert_template 'index.rhtml' + assert_not_nil assigns(:issues) + assert_nil assigns(:project) + end + + def test_index_with_project + get :index, :project_id => 1 + assert_response :success + assert_template 'index.rhtml' + assert_not_nil assigns(:issues) + 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_csv_with_project + get :index, :format => 'csv' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal 'text/csv', @response.content_type + + 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_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 + 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_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_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 => 'custom_fields[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_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 + post :new, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5}, + :custom_fields => {'2' => 'Value for field 2'} + assert_redirected_to 'issues/show' + + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_equal 2, issue.author_id + v = issue.custom_values.find_by_custom_field_id(2) + assert_not_nil v + assert_equal 'Value for field 2', v.value + end + + def test_post_new_without_custom_fields_param + @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_redirected_to 'issues/show' + 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_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_post_edit + @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} + assert_redirected_to 'issues/show/1' + issue.reload + assert_equal new_subject, issue.subject + + 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_status_and_assignee_change + issue = Issue.find(1) + assert_equal 1, issue.status_id + @request.session[:user_id] = 2 + post :edit, + :id => 1, + :issue => { :status_id => 2, :assigned_to_id => 3 }, + :notes => 'Assigned to dlopper' + assert_redirected_to 'issues/show/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") + 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 'issues/show/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 + post :edit, + :id => 1, + :notes => '2.5 hours added', + :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } + assert_redirected_to 'issues/show/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 + # anonymous user + post :edit, + :id => 1, + :notes => '', + :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}} + assert_redirected_to 'issues/show/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 'issues/show/1' + + issue.reload + assert issue.journals.empty? + # No email should be sent + assert ActionMailer::Base.deliveries.empty? + end + + def test_bulk_edit + @request.session[:user_id] = 2 + # update issues priority + post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => '' + 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} + assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes + 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_one_issue_to_another_project + @request.session[:user_id] = 1 + post :move, :id => 1, :new_project_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + assert_equal 2, Issue.find(1).project_id + end + + def test_bulk_move_to_another_project + @request.session[:user_id] = 1 + post :move, :ids => [1, 2], :new_project_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + # 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] = 1 + post :move, :ids => [1, 2], :new_tracker_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + assert_equal 2, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + 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/edit/1', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5', + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8', + :class => '' } + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1', + :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 => '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_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 'projects/ecookbook/issues' + 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 'projects/ecookbook/issues' + 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 'projects/ecookbook/issues' + 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 'projects/ecookbook/issues' + assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) + assert_equal 2, TimeEntry.find(1).issue_id + assert_equal 2, TimeEntry.find(2).issue_id + end + + def test_destroy_attachment + issue = Issue.find(3) + a = issue.attachments.size + @request.session[:user_id] = 2 + post :destroy_attachment, :id => 3, :attachment_id => 1 + assert_redirected_to 'issues/show/3' + assert_nil Attachment.find_by_id(1) + issue.reload + assert_equal((a-1), issue.attachments.size) + j = issue.journals.find(:first, :order => 'created_on DESC') + assert_equal 'attachment', j.details.first.property + end +end diff --git a/groups/test/functional/journals_controller_test.rb b/groups/test/functional/journals_controller_test.rb new file mode 100644 index 000000000..327c7e79a --- /dev/null +++ b/groups/test/functional/journals_controller_test.rb @@ -0,0 +1,59 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :members, :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 diff --git a/groups/test/functional/messages_controller_test.rb b/groups/test/functional/messages_controller_test.rb new file mode 100644 index 000000000..1fe8d086a --- /dev/null +++ b/groups/test/functional/messages_controller_test.rb @@ -0,0 +1,111 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules + + def setup + @controller = MessagesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:board) + assert_not_nil assigns(:project) + assert_not_nil assigns(:topic) + end + + def test_show_message_not_found + get :show, :board_id => 1, :id => 99999 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :board_id => 1 + assert_response :success + assert_template 'new' + end + + def test_post_new + @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + Setting.notified_events << 'message_posted' + + post :new, :board_id => 1, + :message => { :subject => 'Test created message', + :content => 'Message body'} + assert_redirected_to 'messages/show' + message = Message.find_by_subject('Test created message') + assert_not_nil message + 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}] 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_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 'messages/show' + message = Message.find(1) + assert_equal 'New subject', message.subject + assert_equal 'New body', message.content + 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 'messages/show' + assert Message.find_by_subject('Test reply') + end + + def test_destroy_topic + @request.session[:user_id] = 2 + post :destroy, :board_id => 1, :id => 1 + assert_redirected_to 'boards/show' + assert_nil Message.find_by_id(1) + end +end diff --git a/groups/test/functional/my_controller_test.rb b/groups/test/functional/my_controller_test.rb new file mode 100644 index 000000000..c1349ace4 --- /dev/null +++ b/groups/test/functional/my_controller_test.rb @@ -0,0 +1,91 @@ +# 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 < Test::Unit::TestCase + fixtures :users, :issues, :issue_statuses, :trackers, :enumerations + + 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_get_account + get :account + assert_response :success + assert_template 'account' + assert_equal User.find(2), assigns(:user) + end + + def test_update_account + post :account, :user => {:firstname => "Joe", :login => "root", :admin => 1} + 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 !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 diff --git a/groups/test/functional/news_controller_test.rb b/groups/test/functional/news_controller_test.rb new file mode 100644 index 000000000..01f8015b9 --- /dev/null +++ b/groups/test/functional/news_controller_test.rb @@ -0,0 +1,147 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :enabled_modules, :news, :comments + + def setup + @controller = NewsController.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(:newss) + assert_nil assigns(:project) + 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 + 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_get_new + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + end + + def test_post_new + @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 + 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/show/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/show/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_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/show/1' + assert_nil Comment.find_by_id(2) + assert_equal comments_count - 1, News.find(1).comments.size + 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 diff --git a/groups/test/functional/projects_controller_test.rb b/groups/test/functional/projects_controller_test.rb new file mode 100644 index 000000000..eb5795152 --- /dev/null +++ b/groups/test/functional/projects_controller_test.rb @@ -0,0 +1,268 @@ +# 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 'projects_controller' + +# Re-raise errors caught by the controller. +class ProjectsController; def rescue_action(e) raise e end; end + +class ProjectsControllerTest < Test::Unit::TestCase + fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + + def setup + @controller = ProjectsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_index + get :index + assert_response :success + assert_template 'list' + end + + def test_list + get :list + assert_response :success + assert_template 'list' + assert_not_nil assigns(:project_tree) + # Root project as hash key + assert assigns(:project_tree).has_key?(Project.find(1)) + # Subproject in corresponding value + assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) + 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_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', + :custom_field_ids => ['']} + assert_redirected_to 'projects/settings/ecookbook' + project = Project.find(1) + assert_equal 'Test changed name', project.name + 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_list_files + get :list_files, :id => 1 + assert_response :success + assert_template 'list_files' + assert_not_nil assigns(:versions) + end + + def test_changelog + get :changelog, :id => 1 + assert_response :success + assert_template 'changelog' + assert_not_nil assigns(:versions) + 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 + get :activity, :id => 1, :with_subprojects => 0 + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events_by_day) + assert_not_nil assigns(:events) + + # subproject issue not included by default + assert !assigns(:events).include?(Issue.find(5)) + + 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})/, + } + } + } + + 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_activity_with_subprojects + get :activity, :id => 1, :with_subprojects => 1 + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events) + + assert assigns(:events).include?(Issue.find(1)) + assert !assigns(:events).include?(Issue.find(4)) + # subproject issue + assert assigns(:events).include?(Issue.find(5)) + end + + def test_global_activity_anonymous + get :activity + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events) + + assert assigns(:events).include?(Issue.find(1)) + # Issue of a private project + assert !assigns(:events).include?(Issue.find(4)) + end + + def test_global_activity_logged_user + @request.session[:user_id] = 2 # manager + get :activity + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events) + + assert assigns(:events).include?(Issue.find(1)) + # Issue of a private project the user belongs to + assert assigns(:events).include?(Issue.find(4)) + end + + + def test_global_activity_with_all_types + get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1 + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events) + + assert assigns(:events).include?(Issue.find(1)) + assert !assigns(:events).include?(Issue.find(4)) + assert assigns(:events).include?(Message.find(5)) + end + + def test_calendar + get :calendar, :id => 1 + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + end + + def test_calendar_with_subprojects + get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + end + + def test_gantt + get :gantt, :id => 1 + assert_response :success + assert_template 'gantt.rhtml' + assert_not_nil assigns(:events) + end + + def test_gantt_with_subprojects + get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] + assert_response :success + assert_template 'gantt.rhtml' + assert_not_nil assigns(:events) + end + + def test_gantt_export_to_pdf + get :gantt, :id => 1, :format => 'pdf' + assert_response :success + assert_template 'gantt.rfpdf' + assert_equal 'application/pdf', @response.content_type + assert_not_nil assigns(:events) + 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 + @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 +end diff --git a/groups/test/functional/queries_controller_test.rb b/groups/test/functional/queries_controller_test.rb new file mode 100644 index 000000000..de08b4245 --- /dev/null +++ b/groups/test/functional/queries_controller_test.rb @@ -0,0 +1,211 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :members, :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', :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', :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', :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_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_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 diff --git a/groups/test/functional/repositories_bazaar_controller_test.rb b/groups/test/functional/repositories_bazaar_controller_test.rb new file mode 100644 index 000000000..acb6c1d21 --- /dev/null +++ b/groups/test/functional/repositories_bazaar_controller_test.rb @@ -0,0 +1,137 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :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 :browse, :id => 3 + assert_response :success + assert_template 'browse' + 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 :browse, :id => 3, :path => ['directory'] + assert_response :success + assert_template 'browse' + 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 :browse, :id => 3, :path => [], :rev => 3 + assert_response :success + assert_template 'browse' + 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 'browse' + 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 diff --git a/groups/test/functional/repositories_controller_test.rb b/groups/test/functional/repositories_controller_test.rb new file mode 100644 index 000000000..47455dc55 --- /dev/null +++ b/groups/test/functional/repositories_controller_test.rb @@ -0,0 +1,64 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :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_revisions + get :revisions, :id => 1 + assert_response :success + assert_template 'revisions' + assert_not_nil assigns(:changesets) + 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 => '/repositories/revision/ecookbook?rev=0'} + } + assert_tag :tag => "div", :attributes => { :class => "contextual" }, + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=2'} + } + 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 +end diff --git a/groups/test/functional/repositories_cvs_controller_test.rb b/groups/test/functional/repositories_cvs_controller_test.rb new file mode 100644 index 000000000..e12bb53ac --- /dev/null +++ b/groups/test/functional/repositories_cvs_controller_test.rb @@ -0,0 +1,152 @@ +# 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 < Test::Unit::TestCase + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository' + REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_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 :browse, :id => 1 + assert_response :success + assert_template 'browse' + 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 :browse, :id => 1, :path => ['images'] + assert_response :success + assert_template 'browse' + 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 :browse, :id => 1, :path => ['images'], :rev => 1 + assert_response :success + assert_template 'browse' + 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' + 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 'browse' + 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 diff --git a/groups/test/functional/repositories_darcs_controller_test.rb b/groups/test/functional/repositories_darcs_controller_test.rb new file mode 100644 index 000000000..43c715924 --- /dev/null +++ b/groups/test/functional/repositories_darcs_controller_test.rb @@ -0,0 +1,103 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :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 :browse, :id => 3 + assert_response :success + assert_template 'browse' + 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 :browse, :id => 3, :path => ['images'] + assert_response :success + assert_template 'browse' + 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 :browse, :id => 3, :path => ['images'], :rev => 1 + assert_response :success + assert_template 'browse' + 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 diff --git a/groups/test/functional/repositories_git_controller_test.rb b/groups/test/functional/repositories_git_controller_test.rb new file mode 100644 index 000000000..339e22897 --- /dev/null +++ b/groups/test/functional/repositories_git_controller_test.rb @@ -0,0 +1,146 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository' + REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_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 :browse, :id => 3 + assert_response :success + assert_template 'browse' + 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 :browse, :id => 3, :path => ['images'] + assert_response :success + assert_template 'browse' + 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 + get :browse, :id => 3, :path => ['images'], :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518' + assert_response :success + assert_template 'browse' + 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 'browse' + 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 => /23/, + :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', 'delete.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 diff --git a/groups/test/functional/repositories_mercurial_controller_test.rb b/groups/test/functional/repositories_mercurial_controller_test.rb new file mode 100644 index 000000000..cb870aa32 --- /dev/null +++ b/groups/test/functional/repositories_mercurial_controller_test.rb @@ -0,0 +1,138 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :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_browse_root + get :browse, :id => 3 + assert_response :success + assert_template 'browse' + 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 :browse, :id => 3, :path => ['images'] + assert_response :success + assert_template 'browse' + 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 + get :browse, :id => 3, :path => ['images'], :rev => 0 + assert_response :success + assert_template 'browse' + 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 'browse' + 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 diff --git a/groups/test/functional/repositories_subversion_controller_test.rb b/groups/test/functional/repositories_subversion_controller_test.rb new file mode 100644 index 000000000..dd56947fc --- /dev/null +++ b/groups/test/functional/repositories_subversion_controller_test.rb @@ -0,0 +1,115 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :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 :browse, :id => 1 + assert_response :success + assert_template 'browse' + 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 :browse, :id => 1, :path => ['subversion_test'] + assert_response :success + assert_template 'browse' + 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 + end + + def test_browse_at_given_revision + get :browse, :id => 1, :path => ['subversion_test'], :rev => 4 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name) + end + + def test_entry + get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'] + assert_response :success + assert_template 'entry' + 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 + end + + def test_directory_entry + get :entry, :id => 1, :path => ['subversion_test', 'folder'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entry) + assert_equal 'folder', assigns(:entry).name + end + + def test_diff + get :diff, :id => 1, :rev => 3 + assert_response :success + assert_template 'diff' + 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 diff --git a/groups/test/functional/roles_controller_test.rb b/groups/test/functional/roles_controller_test.rb new file mode 100644 index 000000000..d70a4f0c3 --- /dev/null +++ b/groups/test/functional/roles_controller_test.rb @@ -0,0 +1,220 @@ +# 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 < Test::Unit::TestCase + fixtures :roles, :users, :members, :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/list' + 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/list' + 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/list' + 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/list' + 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_workflow + get :workflow + assert_response :success + assert_template 'workflow' + assert_not_nil assigns(:roles) + assert_not_nil assigns(:trackers) + end + + def test_get_workflow_with_role_and_tracker + get :workflow, :role_id => 2, :tracker_id => 1 + assert_response :success + assert_template 'workflow' + # 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_workflow + post :workflow, :role_id => 2, :tracker_id => 1, :issue_status => {'4' => ['5'], '3' => ['1', '2']} + assert_redirected_to 'roles/workflow' + + 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 :workflow, :role_id => 2, :tracker_id => 1 + assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) + 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/list' + + 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/list' + assert Role.find(1).permissions.empty? + end + + def test_move_highest + post :move, :id => 3, :position => 'highest' + assert_redirected_to 'roles/list' + assert_equal 1, Role.find(3).position + end + + def test_move_higher + position = Role.find(3).position + post :move, :id => 3, :position => 'higher' + assert_redirected_to 'roles/list' + assert_equal position - 1, Role.find(3).position + end + + def test_move_lower + position = Role.find(2).position + post :move, :id => 2, :position => 'lower' + assert_redirected_to 'roles/list' + assert_equal position + 1, Role.find(2).position + end + + def test_move_lowest + post :move, :id => 2, :position => 'lowest' + assert_redirected_to 'roles/list' + assert_equal Role.count, Role.find(2).position + end +end diff --git a/groups/test/functional/search_controller_test.rb b/groups/test/functional/search_controller_test.rb new file mode 100644 index 000000000..49004c7e6 --- /dev/null +++ b/groups/test/functional/search_controller_test.rb @@ -0,0 +1,102 @@ +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 < Test::Unit::TestCase + fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values + + 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_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/show/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 diff --git a/groups/test/functional/settings_controller_test.rb b/groups/test/functional/settings_controller_test.rb new file mode 100644 index 000000000..0e919a741 --- /dev/null +++ b/groups/test/functional/settings_controller_test.rb @@ -0,0 +1,53 @@ +# 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 < Test::Unit::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_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 diff --git a/groups/test/functional/sys_api_test.rb b/groups/test/functional/sys_api_test.rb new file mode 100644 index 000000000..ec8d0964e --- /dev/null +++ b/groups/test/functional/sys_api_test.rb @@ -0,0 +1,31 @@ +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 < Test::Unit::TestCase + fixtures :projects, :repositories + + def setup + @controller = SysController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + # Enable WS + Setting.sys_api_enabled = 1 + end + + def test_projects + result = invoke :projects + assert_equal Project.count, result.size + assert result.first.is_a?(Project) + end + + def test_repository_created + project = Project.find(3) + assert_nil project.repository + assert invoke(:repository_created, project.identifier, 'http://localhost/svn') + project.reload + assert_not_nil project.repository + end +end diff --git a/groups/test/functional/timelog_controller_test.rb b/groups/test/functional/timelog_controller_test.rb new file mode 100644 index 000000000..e80a67728 --- /dev/null +++ b/groups/test/functional/timelog_controller_test.rb @@ -0,0 +1,208 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :enabled_modules, :roles, :members, :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_create + @request.session[:user_id] = 3 + post :edit, :project_id => 1, + :time_entry => {:comments => 'Some work on TimelogControllerTest', + :activity_id => '10', + :spent_on => '2008-03-14', + :issue_id => '1', + :hours => '7.3'} + assert_redirected_to 'projects/ecookbook/timelog/details' + + i = Issue.find(1) + t = TimeEntry.find_by_comments('Some work on TimelogControllerTest') + assert_not_nil t + 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 'projects/ecookbook/timelog/details' + entry.reload + + assert_equal 8, entry.hours + assert_equal 2, entry.issue_id + assert_equal 2, entry.user_id + end + + def destroy + @request.session[:user_id] = 2 + post :destroy, :id => 1 + assert_redirected_to 'projects/ecookbook/timelog/details' + assert_nil TimeEntry.find_by_id(1) + end + + def test_report_no_criteria + get :report, :project_id => 1 + assert_response :success + assert_template 'report' + 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_custom_field_criteria + get :report, :project_id => 1, :criterias => ['project', 'cf_1'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_not_nil assigns(:criterias) + assert_equal 2, 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' }} + 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_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_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_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_csv_export + 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 diff --git a/groups/test/functional/users_controller_test.rb b/groups/test/functional/users_controller_test.rb new file mode 100644 index 000000000..8629a7131 --- /dev/null +++ b/groups/test/functional/users_controller_test.rb @@ -0,0 +1,62 @@ +# 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 < Test::Unit::TestCase + fixtures :users, :projects, :members + + 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 + get :index + assert_response :success + assert_template 'list' + end + + def test_list + get :list + assert_response :success + assert_template 'list' + assert_not_nil assigns(:users) + # active users only + assert_nil assigns(:users).detect {|u| !u.active?} + end + + def test_edit_membership + post :edit_membership, :id => 2, :membership_id => 1, + :membership => { :role_id => 2} + assert_redirected_to 'users/edit/2' + assert_equal 2, Member.find(1).role_id + end + + def test_destroy_membership + post :destroy_membership, :id => 2, :membership_id => 1 + assert_redirected_to 'users/edit/2' + assert_nil Member.find_by_id(1) + end +end diff --git a/groups/test/functional/versions_controller_test.rb b/groups/test/functional/versions_controller_test.rb new file mode 100644 index 000000000..3477c5edd --- /dev/null +++ b/groups/test/functional/versions_controller_test.rb @@ -0,0 +1,73 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :versions, :users, :roles, :members, :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_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 'projects/settings/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 => 2 + assert_redirected_to 'projects/settings/ecookbook' + assert_nil Version.find_by_id(2) + end + + def test_issue_status_by + xhr :get, :status_by, :id => 2 + assert_response :success + assert_template '_issue_counts' + end +end diff --git a/groups/test/functional/welcome_controller_test.rb b/groups/test/functional/welcome_controller_test.rb new file mode 100644 index 000000000..18146c6aa --- /dev/null +++ b/groups/test/functional/welcome_controller_test.rb @@ -0,0 +1,49 @@ +# 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 < Test::Unit::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 +end diff --git a/groups/test/functional/wiki_controller_test.rb b/groups/test/functional/wiki_controller_test.rb new file mode 100644 index 000000000..bf31e6614 --- /dev/null +++ b/groups/test/functional/wiki_controller_test.rb @@ -0,0 +1,163 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :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_show_start_page + get :index, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /CookBook documentation/ + 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_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 'wiki/ecookbook/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 + @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_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 + 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 + 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_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 'wiki/ecookbook/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 'wiki/ecookbook/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 + @request.session[:user_id] = 2 + post :destroy, :id => 1, :page => 'CookBook_documentation' + assert_redirected_to 'wiki/ecookbook/Page_index/special' + 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 :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, + :content => /CookBook documentation/ + end + + def test_not_found + get :index, :id => 999 + assert_response 404 + end +end diff --git a/groups/test/functional/wikis_controller_test.rb b/groups/test/functional/wikis_controller_test.rb new file mode 100644 index 000000000..3e51314a5 --- /dev/null +++ b/groups/test/functional/wikis_controller_test.rb @@ -0,0 +1,56 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis + + def setup + @controller = WikisController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + 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 + @request.session[:user_id] = 1 + post :destroy, :id => 1, :confirm => 1 + assert_redirected_to 'projects/settings/ecookbook' + 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 diff --git a/groups/test/helper_testcase.rb b/groups/test/helper_testcase.rb new file mode 100644 index 000000000..aba6784a0 --- /dev/null +++ b/groups/test/helper_testcase.rb @@ -0,0 +1,35 @@ +# Re-raise errors caught by the controller. +class StubController < ApplicationController + def rescue_action(e) raise e end; + attr_accessor :request, :url +end + +class HelperTestCase < Test::Unit::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 diff --git a/groups/test/integration/account_test.rb b/groups/test/integration/account_test.rb new file mode 100644 index 000000000..e9d665d19 --- /dev/null +++ b/groups/test/integration/account_test.rb @@ -0,0 +1,101 @@ +# 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 AccountTest < ActionController::IntegrationTest + fixtures :users + + # Replace this with your real tests. + def test_login + get "my/page" + assert_redirected_to "account/login" + log_user('jsmith', 'jsmith') + + get "my/account" + assert_response :success + assert_template "my/account" + 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 "account/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 "account/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 'account/login' + log_user('newuser', 'newpass') + 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 'account/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 'account/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 'account/login' + log_user('newuser', 'newpass') + end +end diff --git a/groups/test/integration/admin_test.rb b/groups/test/integration/admin_test.rb new file mode 100644 index 000000000..a424247cc --- /dev/null +++ b/groups/test/integration/admin_test.rb @@ -0,0 +1,65 @@ +# 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 :users + + 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" + assert_redirected_to "users/list" + + user = User.find_by_login("psmith") + assert_kind_of User, user + 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/list" + locked_user = User.try_to_login("psmith", "psmith09") + assert_equal nil, locked_user + end + + def test_add_project + log_user("admin", "admin") + get "projects/add" + assert_response :success + assert_template "projects/add" + post "projects/add", :project => { :name => "blog", + :description => "weblog", + :identifier => "blog", + :is_public => 1 }, + 'custom_fields[3]' => 'Beta' + assert_redirected_to "admin/projects" + assert_equal 'Successful creation.', flash[:notice] + + project = Project.find_by_name("blog") + assert_kind_of Project, project + assert_equal "weblog", project.description + assert_equal true, project.is_public? + + get "admin/projects" + assert_response :success + assert_template "admin/projects" + end +end diff --git a/groups/test/integration/application_test.rb b/groups/test/integration/application_test.rb new file mode 100644 index 000000000..18f4891c2 --- /dev/null +++ b/groups/test/integration/application_test.rb @@ -0,0 +1,43 @@ +# 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 + fixtures :users + + 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', User.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', User.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 +end diff --git a/groups/test/integration/issues_test.rb b/groups/test/integration/issues_test.rb new file mode 100644 index 000000000..b9e21719c --- /dev/null +++ b/groups/test/integration/issues_test.rb @@ -0,0 +1,71 @@ +require "#{File.dirname(__FILE__)}/../test_helper" + +class IssuesTest < ActionController::IntegrationTest + fixtures :projects, + :users, + :trackers, + :projects_trackers, + :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/new', :tracker_id => "1", + :issue => { :start_date => "2006-12-26", + :priority_id => "3", + :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 "issues/show" + 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') + + post 'issues/edit/1', + :notes => 'Some notes', + :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}} + assert_redirected_to "issues/show/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 + +end diff --git a/groups/test/integration/projects_test.rb b/groups/test/integration/projects_test.rb new file mode 100644 index 000000000..e56bee484 --- /dev/null +++ b/groups/test/integration/projects_test.rb @@ -0,0 +1,44 @@ +# 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/show", :id => 1 + assert_response 403 + get "projects/show", :id => subproject.id + assert_response 403 + + post "projects/unarchive", :id => 1 + assert_redirected_to "admin/projects" + assert Project.find(1).active? + get "projects/show", :id => 1 + assert_response :success + end +end diff --git a/groups/test/test_helper.rb b/groups/test/test_helper.rb new file mode 100644 index 000000000..61670318a --- /dev/null +++ b/groups/test/test_helper.rb @@ -0,0 +1,77 @@ +# 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') + +class Test::Unit::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 "/account/login" + assert_equal nil, session[:user_id] + assert_response :success + assert_template "account/login" + post "/account/login", :username => login, :password => password + assert_redirected_to "my/page" + assert_equal login, User.find(session[:user_id]).login + end + + def test_uploaded_file(name, mime) + ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + "/files/#{name}", mime) + end +end + + +# ActionController::TestUploadedFile bug +# see http://dev.rubyonrails.org/ticket/4635 +class String + def original_filename + "testfile.txt" + end + + def content_type + "text/plain" + end + + def read + self.to_s + end +end diff --git a/groups/test/unit/board_test.rb b/groups/test/unit/board_test.rb new file mode 100644 index 000000000..3ba4b2d97 --- /dev/null +++ b/groups/test/unit/board_test.rb @@ -0,0 +1,30 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class BoardTest < Test::Unit::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 diff --git a/groups/test/unit/calendar_test.rb b/groups/test/unit/calendar_test.rb new file mode 100644 index 000000000..98d856921 --- /dev/null +++ b/groups/test/unit/calendar_test.rb @@ -0,0 +1,43 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/changeset_test.rb b/groups/test/unit/changeset_test.rb new file mode 100644 index 000000000..bbfe6952d --- /dev/null +++ b/groups/test/unit/changeset_test.rb @@ -0,0 +1,62 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :trackers + + def setup + end + + def test_ref_keywords_any + 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 + 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('4') + assert_nil changeset.next + end +end diff --git a/groups/test/unit/comment_test.rb b/groups/test/unit/comment_test.rb new file mode 100644 index 000000000..c07ee8273 --- /dev/null +++ b/groups/test/unit/comment_test.rb @@ -0,0 +1,47 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/custom_field_test.rb b/groups/test/unit/custom_field_test.rb new file mode 100644 index 000000000..1b9c9aea9 --- /dev/null +++ b/groups/test/unit/custom_field_test.rb @@ -0,0 +1,32 @@ +# 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 < Test::Unit::TestCase + fixtures :custom_fields + + def test_create + field = UserCustomField.new(:name => 'Money money money', :field_format => 'float') + assert field.save + end + + def test_destroy + field = CustomField.find(1) + assert field.destroy + end +end diff --git a/groups/test/unit/custom_value_test.rb b/groups/test/unit/custom_value_test.rb new file mode 100644 index 000000000..11578ae6b --- /dev/null +++ b/groups/test/unit/custom_value_test.rb @@ -0,0 +1,117 @@ +# 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 < Test::Unit::TestCase + fixtures :custom_fields + + 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 +end diff --git a/groups/test/unit/helpers/application_helper_test.rb b/groups/test/unit/helpers/application_helper_test.rb new file mode 100644 index 000000000..fa2109131 --- /dev/null +++ b/groups/test/unit/helpers/application_helper_test.rb @@ -0,0 +1,216 @@ +# 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 ApplicationHelperTest < HelperTestCase + include ApplicationHelper + include ActionView::Helpers::TextHelper + fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules + + def setup + super + end + + def test_auto_links + to_test = { + 'http://foo.bar' => 'http://foo.bar', + 'http://foo.bar/~user' => 'http://foo.bar/~user', + 'http://foo.bar.' => 'http://foo.bar.', + 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.', + 'www.foo.bar' => 'www.foo.bar', + 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', + 'http://foo.bar/page#125' => 'http://foo.bar/page#125' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_auto_mailto + assert_equal '

    ', + textilizable('test@foo.bar') + end + + def test_inline_images + to_test = { + '!http://foo.bar/image.jpg!' => '', + 'floating !>http://foo.bar/image.jpg!' => 'floating
    ', + 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class ', + 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style ', + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_textile_external_links + to_test = { + 'This is a "link":http://foo.bar' => 'This is a link', + 'This is an intern "link":/foo/bar' => 'This is an intern link', + '"link (Link title)":http://foo.bar' => 'link' + } + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_redmine_links + issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3}, + :class => 'issue', :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') + + 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') + + source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => 'some/file'} + + to_test = { + # tickets + '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.", + # changesets + 'r1' => changeset_link, + # 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@52' => link_to('source:/some/file@52', source_url.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@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'), + # 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:' + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_wiki_links + to_test = { + '[[CookBook documentation]]' => 'CookBook documentation', + '[[Another page|Page]]' => 'Page', + # page that doesn't exist + '[[Unknown page]]' => 'Unknown page', + '[[Unknown page|404]]' => '404', + # link to another project wiki + '[[onlinestore:]]' => 'onlinestore', + '[[onlinestore:|Wiki]]' => 'Wiki', + '[[onlinestore:Start page]]' => 'Start page', + '[[onlinestore:Start page|Text]]' => 'Text', + '[[onlinestore:Unknown page]]' => 'Unknown page', + # escaping + '![[Another page|Page]]' => '[[Another page|Page]]', + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + end + + def test_html_tags + to_test = { + "
    content
    " => "

    <div>content</div>

    ", + "
    content
    " => "

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

    ", + "" => "

    <script>some script;</script>

    ", + # do not escape pre/code tags + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", + "
    content
    " => "
    <div>content</div>
    ", + } + to_test.each { |text, result| assert_equal result, textilizable(text) } + end + + def test_wiki_links_in_tables + to_test = {"|Cell 11|Cell 12|Cell 13|\n|Cell 21|Cell 22||\n|Cell 31||Cell 33|" => + 'Cell 11Cell 12Cell 13' + + 'Cell 21Cell 22' + + 'Cell 31Cell 33', + + "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" => + 'Link title' + + 'Other title' + + 'Cell 21Last page' + } + @project = Project.find(1) + to_test.each { |text, result| assert_equal "#{result}
    ", textilizable(text).gsub(/[\t\n]/, '') } + end + + def test_macro_hello_world + text = "{{hello_world}}" + assert textilizable(text).match(/Hello world!/) + # escaping + text = "!{{hello_world}}" + assert_equal '

    {{hello_world}}

    ', 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(/Unknow project/) + end + + def test_date_format_default + today = Date.today + Setting.date_format = '' + assert_equal l_date(today), format_date(today) + end + + def test_date_format + today = Date.today + Setting.date_format = '%d %m %Y' + assert_equal today.strftime('%d %m %Y'), format_date(today) + end + + def test_time_format_default + now = Time.now + Setting.date_format = '' + Setting.time_format = '' + assert_equal l_datetime(now), format_time(now) + assert_equal l_time(now), format_time(now, false) + end + + def test_time_format + 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 +end diff --git a/groups/test/unit/helpers/projects_helper_test.rb b/groups/test/unit/helpers/projects_helper_test.rb new file mode 100644 index 000000000..d76d92bc9 --- /dev/null +++ b/groups/test/unit/helpers/projects_helper_test.rb @@ -0,0 +1,41 @@ +# 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 ProjectsHelperTest < HelperTestCase + include ProjectsHelper + include ActionView::Helpers::TextHelper + fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories + + def setup + super + end + + if Object.const_defined?(:Magick) + def test_gantt_image + assert gantt_image(Issue.find(:all, :conditions => "start_date IS NOT NULL AND due_date IS NOT NULL"), Date.today, 6, 2) + end + + def test_gantt_image_with_days + assert gantt_image(Issue.find(:all, :conditions => "start_date IS NOT NULL AND due_date IS NOT NULL"), Date.today, 3, 4) + end + else + puts "RMagick not installed. Skipping tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/issue_category_test.rb b/groups/test/unit/issue_category_test.rb new file mode 100644 index 000000000..a6edb3c7b --- /dev/null +++ b/groups/test/unit/issue_category_test.rb @@ -0,0 +1,41 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/issue_status_test.rb b/groups/test/unit/issue_status_test.rb new file mode 100644 index 000000000..404bc36ba --- /dev/null +++ b/groups/test/unit/issue_status_test.rb @@ -0,0 +1,49 @@ +# 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 < Test::Unit::TestCase + fixtures :issue_statuses + + 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_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 +end diff --git a/groups/test/unit/issue_test.rb b/groups/test/unit/issue_test.rb new file mode 100644 index 000000000..36ba1fb45 --- /dev/null +++ b/groups/test/unit/issue_test.rb @@ -0,0 +1,88 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries + + def test_create + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').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_category_based_assignment + issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').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_close_duplicates + # Create 3 issues + issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').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 => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 2 + IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 1 (circular duplicates) + IssueRelation.create(:issue_from => issue1, :issue_to => issue3, :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_move_to_another_project + issue = Issue.find(1) + assert issue.move_to(Project.find(2)) + issue.reload + assert_equal 2, issue.project_id + # Category removed + assert_nil issue.category + # Make sure time entries were move to the target project + assert_equal 2, issue.time_entries.first.project_id + 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 +end diff --git a/groups/test/unit/journal_test.rb b/groups/test/unit/journal_test.rb new file mode 100644 index 000000000..b177f3198 --- /dev/null +++ b/groups/test/unit/journal_test.rb @@ -0,0 +1,39 @@ +# 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 < Test::Unit::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 +end diff --git a/groups/test/unit/mail_handler_test.rb b/groups/test/unit/mail_handler_test.rb new file mode 100644 index 000000000..d0fc68de8 --- /dev/null +++ b/groups/test/unit/mail_handler_test.rb @@ -0,0 +1,57 @@ +# 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 MailHandlerTest < Test::Unit::TestCase + fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' + CHARSET = "utf-8" + + include ActionMailer::Quoting + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @expected = TMail::Mail.new + @expected.set_content_type "text", "plain", { "charset" => CHARSET } + @expected.mime_version = '1.0' + end + + def test_add_note_to_issue + raw = read_fixture("add_note_to_issue.txt").join + MailHandler.receive(raw) + + issue = Issue.find(2) + journal = issue.journals.find(:first, :order => "created_on DESC") + assert journal + assert_equal User.find_by_mail("jsmith@somenet.foo"), journal.user + assert_equal "Note added by mail", journal.notes + end + + private + def read_fixture(action) + IO.readlines("#{FIXTURES_PATH}/mail_handler/#{action}") + end + + def encode(subject) + quoted_printable(subject, CHARSET) + end +end diff --git a/groups/test/unit/mailer_test.rb b/groups/test/unit/mailer_test.rb new file mode 100644 index 000000000..64648b94c --- /dev/null +++ b/groups/test/unit/mailer_test.rb @@ -0,0 +1,119 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :issues, :users, :members, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations + + 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 + # link to the main ticket + assert mail.body.include?('Bug #1: Can\'t print recipes') + + # link to a referenced ticket + assert mail.body.include?('#2') + # link to a changeset + assert mail.body.include?('r2') + end + + # test mailer methods for each language + def test_issue_add + issue = Issue.find(1) + GLoc.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) + GLoc.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) + GLoc.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') ] + GLoc.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) + GLoc.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 + GLoc.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) + GLoc.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) + GLoc.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) + GLoc.valid_languages.each do |lang| + token.user.update_attribute :language, lang.to_s + token.reload + assert Mailer.deliver_register(token) + end + end +end diff --git a/groups/test/unit/member_test.rb b/groups/test/unit/member_test.rb new file mode 100644 index 000000000..079782306 --- /dev/null +++ b/groups/test/unit/member_test.rb @@ -0,0 +1,51 @@ +# 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 < Test::Unit::TestCase + fixtures :users, :projects, :roles, :members + + def setup + @jsmith = Member.find(1) + end + + def test_create + member = Member.new(:project_id => 1, :user_id => 4, :role_id => 1) + assert member.save + end + + def test_update + assert_equal "eCookbook", @jsmith.project.name + assert_equal "Manager", @jsmith.role.name + assert_equal "jsmith", @jsmith.user.login + + @jsmith.role = Role.find(2) + assert @jsmith.save + end + + def test_validate + member = Member.new(:project_id => 1, :user_id => 2, :role_id =>2) + # same use can't have more than one role for a project + assert !member.save + end + + def test_destroy + @jsmith.destroy + assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) } + end +end diff --git a/groups/test/unit/message_test.rb b/groups/test/unit/message_test.rb new file mode 100644 index 000000000..82ed3fe13 --- /dev/null +++ b/groups/test/unit/message_test.rb @@ -0,0 +1,70 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class MessageTest < Test::Unit::TestCase + fixtures :projects, :boards, :messages + + 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 + end + + def test_reply + topics_count = @board.topics_count + messages_count = @board.messages_count + @message = Message.find(1) + replies_count = @message.replies_count + + reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => @user) + 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 + end + + def test_destroy_topic + message = Message.find(1) + board = message.board + topics_count, messages_count = board.topics_count, board.messages_count + assert message.destroy + 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 + 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 +end diff --git a/groups/test/unit/project_test.rb b/groups/test/unit/project_test.rb new file mode 100644 index 000000000..9af68c231 --- /dev/null +++ b/groups/test/unit/project_test.rb @@ -0,0 +1,132 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers, :boards + + def setup + @ecookbook = Project.find(1) + @ecookbook_sub1 = Project.find(3) + 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 + @ecookbook.name = "" + assert !@ecookbook.save + assert_equal 1, @ecookbook.errors.count + assert_equal "activerecord_error_blank", @ecookbook.errors.on(:name) + end + + def test_public_projects + public_projects = Project.find(:all, :conditions => ["is_public=?", true]) + assert_equal 3, public_projects.length + assert_equal true, public_projects[0].is_public? + 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.active_children.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_subproject_ok + sub = Project.find(2) + sub.parent = @ecookbook + assert sub.save + assert_equal @ecookbook.id, sub.parent.id + @ecookbook.reload + assert_equal 3, @ecookbook.children.size + end + + def test_subproject_invalid + sub = Project.find(2) + sub.parent = @ecookbook_sub1 + assert !sub.save + end + + def test_subproject_invalid_2 + sub = @ecookbook + sub.parent = Project.find(2) + assert !sub.save + end + + def test_rolled_up_trackers + parent = Project.find(1) + child = parent.children.find(3) + + assert_equal [1, 2], parent.tracker_ids + assert_equal [2, 3], child.tracker_ids + + 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 +end diff --git a/groups/test/unit/query_test.rb b/groups/test/unit/query_test.rb new file mode 100644 index 000000000..d291018fb --- /dev/null +++ b/groups/test/unit/query_test.rb @@ -0,0 +1,74 @@ +# 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 < Test::Unit::TestCase + fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries + + 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 = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + assert_equal 1, issues.length + assert_equal Issue.find(3), issues.first + 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_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 diff --git a/groups/test/unit/repository_bazaar_test.rb b/groups/test/unit/repository_bazaar_test.rb new file mode 100644 index 000000000..b7a3cf98e --- /dev/null +++ b/groups/test/unit/repository_bazaar_test.rb @@ -0,0 +1,88 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/repository_cvs_test.rb b/groups/test/unit/repository_cvs_test.rb new file mode 100644 index 000000000..b14d9d964 --- /dev/null +++ b/groups/test/unit/repository_cvs_test.rb @@ -0,0 +1,60 @@ +# 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 < Test::Unit::TestCase + fixtures :projects + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository' + REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_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 + else + puts "CVS test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/repository_darcs_test.rb b/groups/test/unit/repository_darcs_test.rb new file mode 100644 index 000000000..1c8c1b8dd --- /dev/null +++ b/groups/test/unit/repository_darcs_test.rb @@ -0,0 +1,55 @@ +# 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 < Test::Unit::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 + else + puts "Darcs test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/repository_git_test.rb b/groups/test/unit/repository_git_test.rb new file mode 100644 index 000000000..c7bd84a6e --- /dev/null +++ b/groups/test/unit/repository_git_test.rb @@ -0,0 +1,56 @@ +# 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 < Test::Unit::TestCase + fixtures :projects + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository' + REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_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 6, @repository.changesets.count + assert_equal 11, @repository.changes.count + assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find(:first, :order => 'id ASC').comments + end + + def test_fetch_changesets_incremental + @repository.fetch_changesets + # Remove the 3 latest changesets + @repository.changesets.find(:all, :order => 'id DESC', :limit => 3).each(&:destroy) + @repository.reload + assert_equal 3, @repository.changesets.count + + @repository.fetch_changesets + assert_equal 6, @repository.changesets.count + end + else + puts "Git test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/repository_mercurial_test.rb b/groups/test/unit/repository_mercurial_test.rb new file mode 100644 index 000000000..21ddf1e3a --- /dev/null +++ b/groups/test/unit/repository_mercurial_test.rb @@ -0,0 +1,55 @@ +# 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 < Test::Unit::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 + else + puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/repository_subversion_test.rb b/groups/test/unit/repository_subversion_test.rb new file mode 100644 index 000000000..7a1c9df4a --- /dev/null +++ b/groups/test/unit/repository_subversion_test.rb @@ -0,0 +1,55 @@ +# 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 < Test::Unit::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 8, @repository.changesets.count + assert_equal 16, @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 8, @repository.changesets.count + end + else + puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/repository_test.rb b/groups/test/unit/repository_test.rb new file mode 100644 index 000000000..7764ee04a --- /dev/null +++ b/groups/test/unit/repository_test.rb @@ -0,0 +1,110 @@ +# 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 < Test::Unit::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_scan_changesets_for_issue_ids + # 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 ', :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 +end diff --git a/groups/test/unit/role_test.rb b/groups/test/unit/role_test.rb new file mode 100644 index 000000000..5e0d16753 --- /dev/null +++ b/groups/test/unit/role_test.rb @@ -0,0 +1,33 @@ +# 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 < Test::Unit::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 + assert target.workflows.copy(source) + target.reload + assert_equal 90, target.workflows.size + end +end diff --git a/groups/test/unit/setting_test.rb b/groups/test/unit/setting_test.rb new file mode 100644 index 000000000..34d07c193 --- /dev/null +++ b/groups/test/unit/setting_test.rb @@ -0,0 +1,45 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/time_entry_test.rb b/groups/test/unit/time_entry_test.rb new file mode 100644 index 000000000..f86e42eab --- /dev/null +++ b/groups/test/unit/time_entry_test.rb @@ -0,0 +1,46 @@ +# 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 < Test::Unit::TestCase + fixtures :issues, :projects, :users, :time_entries + + def test_hours_format + assertions = { "2" => 2.0, + "21.1" => 21.1, + "2,1" => 2.1, + "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 + end + end +end diff --git a/groups/test/unit/token_test.rb b/groups/test/unit/token_test.rb new file mode 100644 index 000000000..5a34e0ad3 --- /dev/null +++ b/groups/test/unit/token_test.rb @@ -0,0 +1,29 @@ +# 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 TokenTest < Test::Unit::TestCase + fixtures :tokens + + def test_create + token = Token.new + token.save + assert_equal 40, token.value.length + assert !token.expired? + end +end diff --git a/groups/test/unit/tracker_test.rb b/groups/test/unit/tracker_test.rb new file mode 100644 index 000000000..406bdd6db --- /dev/null +++ b/groups/test/unit/tracker_test.rb @@ -0,0 +1,33 @@ +# 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 < Test::Unit::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 + assert target.workflows.copy(source) + target.reload + assert_equal 89, target.workflows.size + end +end diff --git a/groups/test/unit/user_preference_test.rb b/groups/test/unit/user_preference_test.rb new file mode 100644 index 000000000..cf6787b17 --- /dev/null +++ b/groups/test/unit/user_preference_test.rb @@ -0,0 +1,43 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/user_test.rb b/groups/test/unit/user_test.rb new file mode 100644 index 000000000..3209f261a --- /dev/null +++ b/groups/test/unit/user_test.rb @@ -0,0 +1,155 @@ +# 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 < Test::Unit::TestCase + fixtures :users, :members, :projects + + def setup + @admin = User.find(1) + @jsmith = User.find(2) + @dlopper = User.find(3) + 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_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_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.name + Setting.user_format = :username + assert_equal 'jsmith', @jsmith.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_role_for_project + # user with a role + role = @jsmith.role_for_project(Project.find(1)) + assert_kind_of Role, role + assert_equal "Manager", role.name + + # user with no role + assert !@dlopper.role_for_project(Project.find(2)).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 +end diff --git a/groups/test/unit/watcher_test.rb b/groups/test/unit/watcher_test.rb new file mode 100644 index 000000000..9566e6a7c --- /dev/null +++ b/groups/test/unit/watcher_test.rb @@ -0,0 +1,69 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/wiki_content_test.rb b/groups/test/unit/wiki_content_test.rb new file mode 100644 index 000000000..a8c28ae21 --- /dev/null +++ b/groups/test/unit/wiki_content_test.rb @@ -0,0 +1,60 @@ +# 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 < Test::Unit::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_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_fetch_history + assert !@page.content.versions.empty? + @page.content.versions.each do |version| + assert_kind_of String, version.text + end + end +end diff --git a/groups/test/unit/wiki_page_test.rb b/groups/test/unit/wiki_page_test.rb new file mode 100644 index 000000000..bb8111176 --- /dev/null +++ b/groups/test/unit/wiki_page_test.rb @@ -0,0 +1,59 @@ +# 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 < Test::Unit::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_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 +end diff --git a/groups/test/unit/wiki_redirect_test.rb b/groups/test/unit/wiki_redirect_test.rb new file mode 100644 index 000000000..12f6b7d89 --- /dev/null +++ b/groups/test/unit/wiki_redirect_test.rb @@ -0,0 +1,73 @@ +# 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 < Test::Unit::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 diff --git a/groups/test/unit/wiki_test.rb b/groups/test/unit/wiki_test.rb new file mode 100644 index 000000000..23d4f442c --- /dev/null +++ b/groups/test/unit/wiki_test.rb @@ -0,0 +1,44 @@ +# 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 < Test::Unit::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 diff --git a/groups/vendor/plugins/actionwebservice/CHANGELOG b/groups/vendor/plugins/actionwebservice/CHANGELOG new file mode 100644 index 000000000..bb5280356 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/CHANGELOG @@ -0,0 +1,265 @@ +*SVN* + +* Documentation for ActionWebService::API::Base. Closes #7275. [zackchandler] + +* Allow action_web_service to handle various HTTP methods including GET. Closes #7011. [zackchandler] + +* Ensure that DispatcherError is being thrown when a malformed request is received. [Kent Sibilev] + +* Added support for decimal types. Closes #6676. [Kent Sibilev] + +* Removed deprecated end_form_tag helper. [Kent Sibilev] + +* Removed deprecated @request and @response usages. [Kent Sibilev] + +* Removed invocation of deprecated before_action and around_action filter methods. Corresponding before_invocation and after_invocation methods should be used instead. #6275 [Kent Sibilev] + +* Provide access to the underlying SOAP driver. #6212 [bmilekic, Kent Sibilev] + +* Deprecation: update docs. #5998 [jakob@mentalized.net, Kevin Clark] + +* ActionWebService WSDL generation ignores HTTP_X_FORWARDED_HOST [Paul Butcher ] + +* Tighten rescue clauses. #5985 [james@grayproductions.net] + +* Fixed XMLRPC multicall when one of the called methods returns a struct object. [Kent Sibilev] + +* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar] + +* Fix invoke_layered since api_method didn't declare :expects. Closes #4720. [Kevin Ballard , Kent Sibilev] + +* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.] + +* Replace Ruby's deprecated append_features in favor of included. [Marcel Molina Jr.] + +* Fix test database name typo. [Marcel Molina Jr.] + +*1.1.2* (April 9th, 2006) + +* Rely on Active Record 1.14.2 + + +*1.1.1* (April 6th, 2006) + +* Do not convert driver options to strings (#4499) + + +*1.1.0* (March 27th, 2006) + +* Make ActiveWebService::Struct type reloadable + +* Fix scaffolding action when one of the members of a structural type has date or time type + +* Remove extra index hash when generating scaffold html for parameters of structural type #4374 [joe@mjg2.com] + +* Fix Scaffold Fails with Struct as a Parameter #4363 [joe@mjg2.com] + +* Fix soap type registration of multidimensional arrays (#4232) + +* Fix that marshaler couldn't handle ActiveRecord models defined in a different namespace (#2392). + +* Fix that marshaler couldn't handle structs with members of ActiveRecord type (#1889). + +* Fix that marshaler couldn't handle nil values for inner structs (#3576). + +* Fix that changes to ActiveWebService::API::Base required restarting of the server (#2390). + +* Fix scaffolding for signatures with :date, :time and :base64 types (#3321, #2769, #2078). + +* Fix for incorrect casting of TrueClass/FalseClass instances (#2633, #3421). + +* Fix for incompatibility problems with SOAP4R 1.5.5 (#2553) [Kent Sibilev] + + +*1.0.0* (December 13th, 2005) + +* Become part of Rails 1.0 + +*0.9.4* (December 7th, 2005) + +* Update from LGPL to MIT license as per Minero Aoki's permission. [Marcel Molina Jr.] + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Fix that XML-RPC date/time values did not have well-defined behaviour (#2516, #2534). This fix has one caveat, in that we can't support pre-1970 dates from XML-RPC clients. + +*0.9.3* (November 7th, 2005) + +* Upgraded to Action Pack 1.11.0 and Active Record 1.13.0 + + +*0.9.2* (October 26th, 2005) + +* Upgraded to Action Pack 1.10.2 and Active Record 1.12.2 + + +*0.9.1* (October 19th, 2005) + +* Upgraded to Action Pack 1.10.1 and Active Record 1.12.1 + + +*0.9.0* (October 16th, 2005) + +* Fix invalid XML request generation bug in test_invoke [Ken Barker] + +* Add XML-RPC 'system.multicall' support #1941 [jbonnar] + +* Fix duplicate XSD entries for custom types shared across delegated/layered services #1729 [Tyler Kovacs] + +* Allow multiple invocations in the same test method #1720 [dkhawk] + +* Added ActionWebService::API::Base.soap_client and ActionWebService::API::Base.xmlrpc_client helper methods to create the internal clients for an API, useful for testing from ./script/console + +* ActionWebService now always returns UTF-8 responses. + + +*0.8.1* (11 July, 2005) + +* Fix scaffolding for Action Pack controller changes + + +*0.8.0* (6 July, 2005) + +* Fix WSDL generation by aliasing #inherited instead of trying to overwrite it, or the WSDL action may end up not being defined in the controller + +* Add ActionController::Base.wsdl_namespace option, to allow overriding of the namespace used in generated WSDL and SOAP messages. This is equivalent to the [WebService(Namespace = "Value")] attribute in .NET. + +* Add workaround for Ruby 1.8.3's SOAP4R changing the return value of SOAP::Mapping::Registry#find_mapped_soap_class #1414 [Shugo Maeda] + +* Fix moduled controller URLs in WSDL, and add unit test to verify the generated URL #1428 + +* Fix scaffolding template paths, it was broken on Win32 + +* Fix that functional testing of :layered controllers failed when using the SOAP protocol + +* Allow invocation filters in :direct controllers as well, as they have access to more information regarding the web service request than ActionPack filters + +* Add support for a :base64 signature type #1272 [Shugo Maeda] + +* Fix that boolean fields were not rendered correctly in scaffolding + +* Fix that scaffolding was not working for :delegated dispatching + +* Add support for structured types as input parameters to scaffolding, this should let one test the blogging APIs using scaffolding as well + +* Fix that generated WSDL was not using relative_url_root for base URI #1210 [Shugo Maeda] + +* Use UTF-8 encoding by default for SOAP responses, but if an encoding is supplied by caller, use that for the response #1211 [Shugo Maeda, NAKAMURA Hiroshi] + +* If the WSDL was retrieved over HTTPS, use HTTPS URLs in the WSDL too + +* Fix that casting change in 0.7.0 would convert nil values to the default value for the type instead of leaving it as nil + + +*0.7.1* (20th April, 2005) + +* Depend on Active Record 1.10.1 and Action Pack 1.8.1 + + +*0.7.0* (19th April, 2005) + +* When casting structured types, don't try to send obj.name= unless obj responds to it, causes casting to be less likely to fail for XML-RPC + +* Add scaffolding via ActionController::Base.web_service_scaffold for quick testing using a web browser + +* ActionWebService::API::Base#api_methods now returns a hash containing ActionWebService::API::Method objects instead of hashes. However, ActionWebService::API::Method defines a #[]() backwards compatibility method so any existing code utilizing this will still work. + +* The :layered dispatching mode can now be used with SOAP as well, allowing you to support SOAP and XML-RPC clients for APIs like the metaWeblog API + +* Remove ActiveRecordSoapMarshallable workaround, see #912 for details + +* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC) + +* Ensure return value is properly cast as well, fixes XML-RPC interoperability with Ecto and possibly other clients + +* Include backtraces in 500 error responses for failed request parsing, and remove "rescue nil" statements obscuring real errors for XML-RPC + +* Perform casting of struct members even if the structure is already of the correct type, so that the type we specify for the struct member is always the type of the value seen by the API implementation + + +*0.6.2* (27th March, 2005) + +* Allow method declarations for direct dispatching to declare parameters as well. We treat an arity of < 0 or > 0 as an indication that we should send through parameters. Closes #939. + + +*0.6.1* (22th March, 2005) + +* Fix that method response QNames mismatched with that declared in the WSDL, makes SOAP::WSDLDriverFactory work against AWS again + +* Fix that @request.env was being modified, instead, dup the value gotten from env + +* Fix XML-RPC example to use :layered mode, so it works again + +* Support casting '0' or 0 into false, and '1' or 1 into true, when expecting a boolean value + +* Fix that SOAP fault response fault code values were not QName's #804 + + +*0.6.0* (7th March, 2005) + +* Add action_controller/test_invoke, used for integrating AWS with the Rails testing infrastructure + +* Allow passing through options to the SOAP RPC driver for the SOAP client + +* Make the SOAP WS marshaler use #columns to decide which fields to marshal as well, avoids providing attributes brought in by associations + +* Add ActionWebService::API::Base.allow_active_record_expects option, with a default of false. Setting this to true will allow specifying ActiveRecord::Base model classes in :expects. API writers should take care to validate the received ActiveRecord model objects when turning it on, and/or have an authentication mechanism in place to reduce the security risk. + +* Improve error message reporting. Bugs in either AWS or the web service itself will send back a protocol-specific error report message if possible, otherwise, provide as much detail as possible. + +* Removed type checking of received parameters, and perform casting for XML-RPC if possible, but fallback to the received parameters if casting fails, closes #677 + +* Refactored SOAP and XML-RPC marshaling and encoding into a small library devoted exclusively to protocol specifics, also cleaned up the SOAP marshaling approach, so that array and custom type marshaling should be a bit faster. + +* Add namespaced XML-RPC method name support, closes #678 + +* Replace '::' with '..' in fully qualified type names for marshaling and WSDL. This improves interoperability with .NET, and closes #676. + + +*0.5.0* (24th February, 2005) + + * lib/action_service/dispatcher*: replace "router" fragments with + one file for Action Controllers, moves dispatching work out of + the container + * lib/*,test/*,examples/*: rename project to + ActionWebService. prefix all generic "service" type names with web_. + update all using code as well as the RDoc. + * lib/action_service/router/wsdl.rb: ensure that #wsdl is + defined in the final container class, or the new ActionPack + filtering will exclude it + * lib/action_service/struct.rb,test/struct_test.rb: create a + default #initialize on inherit that accepts a Hash containing + the default member values + * lib/action_service/api/action_controller.rb: add support and + tests for #client_api in controller + * test/router_wsdl_test.rb: add tests to ensure declared + service names don't contain ':', as ':' causes interoperability + issues + * lib/*, test/*: rename "interface" concept to "api", and change all + related uses to reflect this change. update all uses of Inflector + to call the method on String instead. + * test/api_test.rb: add test to ensure API definition not + instantiatable + * lib/action_service/invocation.rb: change @invocation_params to + @method_params + * lib/*: update RDoc + * lib/action_service/struct.rb: update to support base types + * lib/action_service/support/signature.rb: support the notion of + "base types" in signatures, with well-known unambiguous names such as :int, + :bool, etc, which map to the correct Ruby class. accept the same names + used by ActiveRecord as well as longer versions of each, as aliases. + * examples/*: update for seperate API definition updates + * lib/action_service/*, test/*: extensive refactoring: define API methods in + a seperate class, and specify it wherever used with 'service_api'. + this makes writing a client API for accessing defined API methods + with ActionWebService really easy. + * lib/action_service/container.rb: fix a bug in default call + handling for direct dispatching, and add ActionController filter + support for direct dispatching. + * test/router_action_controller_test.rb: add tests to ensure + ActionController filters are actually called. + * test/protocol_soap_test.rb: add more tests for direct dispatching. + +0.3.0 + + * First public release diff --git a/groups/vendor/plugins/actionwebservice/MIT-LICENSE b/groups/vendor/plugins/actionwebservice/MIT-LICENSE new file mode 100644 index 000000000..528941e84 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2005 Leon Breedt + +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. + diff --git a/groups/vendor/plugins/actionwebservice/README b/groups/vendor/plugins/actionwebservice/README new file mode 100644 index 000000000..78b91f081 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/README @@ -0,0 +1,364 @@ += Action Web Service -- Serving APIs on rails + +Action Web Service provides a way to publish interoperable web service APIs with +Rails without spending a lot of time delving into protocol details. + + +== Features + +* SOAP RPC protocol support +* Dynamic WSDL generation for APIs +* XML-RPC protocol support +* Clients that use the same API definitions as the server for + easy interoperability with other Action Web Service based applications +* Type signature hints to improve interoperability with static languages +* Active Record model class support in signatures + + +== Defining your APIs + +You specify the methods you want to make available as API methods in an +ActionWebService::API::Base derivative, and then specify this API +definition class wherever you want to use that API. + +The implementation of the methods is done separately from the API +specification. + + +==== Method name inflection + +Action Web Service will camelcase the method names according to Rails Inflector +rules for the API visible to public callers. What this means, for example, +is that the method names in generated WSDL will be camelcased, and callers will +have to supply the camelcased name in their requests for the request to +succeed. + +If you do not desire this behaviour, you can turn it off with the +ActionWebService::API::Base +inflect_names+ option. + + +==== Inflection examples + + :add => Add + :find_all => FindAll + + +==== Disabling inflection + + class PersonAPI < ActionWebService::API::Base + inflect_names false + end + + +==== API definition example + + class PersonAPI < ActionWebService::API::Base + api_method :add, :expects => [:string, :string, :bool], :returns => [:int] + api_method :remove, :expects => [:int], :returns => [:bool] + end + +==== API usage example + + class PersonController < ActionController::Base + web_service_api PersonAPI + + def add + end + + def remove + end + end + + +== Publishing your APIs + +Action Web Service uses Action Pack to process protocol requests. There are two +modes of dispatching protocol requests, _Direct_, and _Delegated_. + + +=== Direct dispatching + +This is the default mode. In this mode, public controller instance methods +implement the API methods, and parameters are passed through to the methods in +accordance with the API specification. + +The return value of the method is sent back as the return value to the +caller. + +In this mode, a special api action is generated in the target +controller to unwrap the protocol request, forward it on to the relevant method +and send back the wrapped return value. This action must not be +overridden. + +==== Direct dispatching example + + class PersonController < ApplicationController + web_service_api PersonAPI + + def add + end + + def remove + end + end + + class PersonAPI < ActionWebService::API::Base + ... + end + + +For this example, protocol requests for +Add+ and +Remove+ methods sent to +/person/api will be routed to the controller methods +add+ and +remove+. + + +=== Delegated dispatching + +This mode can be turned on by setting the +web_service_dispatching_mode+ option +in a controller to :delegated. + +In this mode, the controller contains one or more web service objects (objects +that implement an ActionWebService::API::Base definition). These web service +objects are each mapped onto one controller action only. + +==== Delegated dispatching example + + class ApiController < ApplicationController + web_service_dispatching_mode :delegated + + web_service :person, PersonService.new + end + + class PersonService < ActionWebService::Base + web_service_api PersonAPI + + def add + end + + def remove + end + end + + class PersonAPI < ActionWebService::API::Base + ... + end + + +For this example, all protocol requests for +PersonService+ are +sent to the /api/person action. + +The /api/person action is generated when the +web_service+ +method is called. This action must not be overridden. + +Other controller actions (actions that aren't the target of a +web_service+ call) +are ignored for ActionWebService purposes, and can do normal action tasks. + + +=== Layered dispatching + +This mode can be turned on by setting the +web_service_dispatching_mode+ option +in a controller to :layered. + +This mode is similar to _delegated_ mode, in that multiple web service objects +can be attached to one controller, however, all protocol requests are sent to a +single endpoint. + +Use this mode when you want to share code between XML-RPC and SOAP clients, +for APIs where the XML-RPC method names have prefixes. An example of such +a method name would be blogger.newPost. + + +==== Layered dispatching example + + + class ApiController < ApplicationController + web_service_dispatching_mode :layered + + web_service :mt, MovableTypeService.new + web_service :blogger, BloggerService.new + web_service :metaWeblog, MetaWeblogService.new + end + + class MovableTypeService < ActionWebService::Base + ... + end + + class BloggerService < ActionWebService::Base + ... + end + + class MetaWeblogService < ActionWebService::API::Base + ... + end + + +For this example, an XML-RPC call for a method with a name like +mt.getCategories will be sent to the getCategories +method on the :mt service. + + +== Customizing WSDL generation + +You can customize the names used for the SOAP bindings in the generated +WSDL by using the wsdl_service_name option in a controller: + + class WsController < ApplicationController + wsdl_service_name 'MyApp' + end + +You can also customize the namespace used in the generated WSDL for +custom types and message definition types: + + class WsController < ApplicationController + wsdl_namespace 'http://my.company.com/app/wsapi' + end + +The default namespace used is 'urn:ActionWebService', if you don't supply +one. + + +== ActionWebService and UTF-8 + +If you're going to be sending back strings containing non-ASCII UTF-8 +characters using the :string data type, you need to make sure that +Ruby is using UTF-8 as the default encoding for its strings. + +The default in Ruby is to use US-ASCII encoding for strings, which causes a string +validation check in the Ruby SOAP library to fail and your string to be sent +back as a Base-64 value, which may confuse clients that expected strings +because of the WSDL. + +Two ways of setting the default string encoding are: + +* Start Ruby using the -Ku command-line option to the Ruby executable +* Set the $KCODE flag in config/environment.rb to the + string 'UTF8' + + +== Testing your APIs + + +=== Functional testing + +You can perform testing of your APIs by creating a functional test for the +controller dispatching the API, and calling #invoke in the test case to +perform the invocation. + +Example: + + class PersonApiControllerTest < Test::Unit::TestCase + def setup + @controller = PersonController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_add + result = invoke :remove, 1 + assert_equal true, result + end + end + +This example invokes the API method test, defined on +the PersonController, and returns the result. + + +=== Scaffolding + +You can also test your APIs with a web browser by attaching scaffolding +to the controller. + +Example: + + class PersonController + web_service_scaffold :invocation + end + +This creates an action named invocation on the PersonController. + +Navigating to this action lets you select the method to invoke, supply the parameters, +and view the result of the invocation. + + +== Using the client support + +Action Web Service includes client classes that can use the same API +definition as the server. The advantage of this approach is that your client +will have the same support for Active Record and structured types as the +server, and can just use them directly, and rely on the marshaling to Do The +Right Thing. + +*Note*: The client support is intended for communication between Ruby on Rails +applications that both use Action Web Service. It may work with other servers, but +that is not its intended use, and interoperability can't be guaranteed, especially +not for .NET web services. + +Web services protocol specifications are complex, and Action Web Service client +support can only be guaranteed to work with a subset. + + +==== Factory created client example + + class BlogManagerController < ApplicationController + web_client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger' + end + + class SearchingController < ApplicationController + web_client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch' + end + +See ActionWebService::API::ActionController::ClassMethods for more details. + +==== Manually created client example + + class PersonAPI < ActionWebService::API::Base + api_method :find_all, :returns => [[Person]] + end + + soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...") + persons = soap_client.find_all + + class BloggerAPI < ActionWebService::API::Base + inflect_names false + api_method :getRecentPosts, :returns => [[Blog::Post]] + end + + blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger") + posts = blog.getRecentPosts + + +See ActionWebService::Client::Soap and ActionWebService::Client::XmlRpc for more details. + +== Dependencies + +Action Web Service requires that the Action Pack and Active Record are either +available to be required immediately or are accessible as GEMs. + +It also requires a version of Ruby that includes SOAP support in the standard +library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended; this +is the version tested against. + + +== Download + +The latest Action Web Service version can be downloaded from +http://rubyforge.org/projects/actionservice + + +== Installation + +You can install Action Web Service with the following command. + + % [sudo] ruby setup.rb + + +== License + +Action Web Service is released under the MIT license. + + +== Support + +The Ruby on Rails mailing list + +Or, to contact the author, send mail to bitserf@gmail.com + diff --git a/groups/vendor/plugins/actionwebservice/Rakefile b/groups/vendor/plugins/actionwebservice/Rakefile new file mode 100644 index 000000000..ad2ad223e --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/Rakefile @@ -0,0 +1,172 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require 'fileutils' +require File.join(File.dirname(__FILE__), 'lib', 'action_web_service', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'actionwebservice' +PKG_VERSION = ActionWebService::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "aws" +RUBY_FORGE_USER = "webster132" + +desc "Default Task" +task :default => [ :test ] + + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << "test" + t.test_files = Dir['test/*_test.rb'] + t.verbose = true +} + +SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions)) + +desc 'Build the MySQL test database' +task :build_database do + %x( mysqladmin create actionwebservice_unittest ) + %x( mysql actionwebservice_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} ) +end + + +# Generate the RDoc documentation +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Action Web Service -- Web services for Action Pack" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.options << '--charset' << 'utf-8' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('CHANGELOG') + rdoc.rdoc_files.include('lib/action_web_service.rb') + rdoc.rdoc_files.include('lib/action_web_service/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/api/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/client/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/container/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/dispatcher/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/protocol/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/support/*.rb') +} + + +# Create compressed packages +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.summary = "Web service support for Action Pack." + s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack} + s.version = PKG_VERSION + + s.author = "Leon Breedt" + s.email = "bitserf@gmail.com" + s.rubyforge_project = "aws" + s.homepage = "http://www.rubyonrails.org" + + s.add_dependency('actionpack', '= 1.13.5' + PKG_BUILD) + s.add_dependency('activerecord', '= 1.15.5' + PKG_BUILD) + + s.has_rdoc = true + s.requirements << 'none' + s.require_path = 'lib' + s.autorequire = 'action_web_service' + + s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ] + s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) } +end +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + + +# Publish beta gem +desc "Publish the API documentation" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` +end + +# Publish documentation +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/aws", "doc").upload +end + + +def each_source_file(*args) + prefix, includes, excludes, open_file = args + prefix ||= File.dirname(__FILE__) + open_file = true if open_file.nil? + includes ||= %w[lib\/action_web_service\.rb$ lib\/action_web_service\/.*\.rb$] + excludes ||= %w[lib\/action_web_service\/vendor] + Find.find(prefix) do |file_name| + next if file_name =~ /\.svn/ + file_name.gsub!(/^\.\//, '') + continue = false + includes.each do |inc| + if file_name.match(/#{inc}/) + continue = true + break + end + end + next unless continue + excludes.each do |exc| + if file_name.match(/#{exc}/) + continue = false + break + end + end + next unless continue + if open_file + File.open(file_name) do |f| + yield file_name, f + end + else + yield file_name + end + end +end + +desc "Count lines of the AWS source code" +task :lines do + total_lines = total_loc = 0 + puts "Per File:" + each_source_file do |file_name, f| + file_lines = file_loc = 0 + while line = f.gets + file_lines += 1 + next if line =~ /^\s*$/ + next if line =~ /^\s*#/ + file_loc += 1 + end + puts " #{file_name}: Lines #{file_lines}, LOC #{file_loc}" + total_lines += file_lines + total_loc += file_loc + end + puts "Total:" + puts " Lines #{total_lines}, LOC #{total_loc}" +end + +desc "Publish the release files to RubyForge." +task :release => [ :package ] do + require 'rubyforge' + + packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" } + + rubyforge = RubyForge.new + rubyforge.login + rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages) +end diff --git a/groups/vendor/plugins/actionwebservice/TODO b/groups/vendor/plugins/actionwebservice/TODO new file mode 100644 index 000000000..7c022c14c --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/TODO @@ -0,0 +1,32 @@ += Post-1.0 + - Document/Literal SOAP support + - URL-based dispatching, URL identifies method + + - Add :rest dispatching mode, a.l.a. Backpack API. Clean up dispatching + in general. Support vanilla XML-format as a "Rails" protocol? + XML::Simple deserialization into params? + + web_service_dispatching_mode :rest + + def method1(params) + end + + def method2(params) + end + + + /ws/method1 + + /ws/method2 + + + - Allow locking down a controller to only accept messages for a particular + protocol. This will allow us to generate fully conformant error messages + in cases where we currently fudge it if we don't know the protocol. + + - Allow AWS user to participate in typecasting, so they can centralize + workarounds for buggy input in one place + += Refactoring + - Don't have clean way to go from SOAP Class object to the xsd:NAME type + string -- NaHi possibly looking at remedying this situation diff --git a/groups/vendor/plugins/actionwebservice/init.rb b/groups/vendor/plugins/actionwebservice/init.rb new file mode 100644 index 000000000..ade118c0f --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/init.rb @@ -0,0 +1,7 @@ +require 'action_web_service' + +# These need to be in the load path for action_web_service to work +Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"] + +# AWS Test helpers +require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] =~ /^test/ diff --git a/groups/vendor/plugins/actionwebservice/install.rb b/groups/vendor/plugins/actionwebservice/install.rb new file mode 100644 index 000000000..da08bf5f9 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by way of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the actual gruntwork +Dir.chdir("lib") + +Find.find("action_web_service", "action_web_service.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service.rb new file mode 100644 index 000000000..0632dd1ec --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service.rb @@ -0,0 +1,66 @@ +#-- +# Copyright (C) 2005 Leon Breedt +# +# 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. +#++ + +begin + require 'active_support' + require 'action_controller' + require 'active_record' +rescue LoadError + require 'rubygems' + gem 'activesupport', '>= 1.0.2' + gem 'actionpack', '>= 1.6.0' + gem 'activerecord', '>= 1.9.0' +end + +$:.unshift(File.dirname(__FILE__) + "/action_web_service/vendor/") + +require 'action_web_service/support/class_inheritable_options' +require 'action_web_service/support/signature_types' +require 'action_web_service/base' +require 'action_web_service/client' +require 'action_web_service/invocation' +require 'action_web_service/api' +require 'action_web_service/casting' +require 'action_web_service/struct' +require 'action_web_service/container' +require 'action_web_service/protocol' +require 'action_web_service/dispatcher' +require 'action_web_service/scaffolding' + +ActionWebService::Base.class_eval do + include ActionWebService::Container::Direct + include ActionWebService::Invocation +end + +ActionController::Base.class_eval do + include ActionWebService::Protocol::Discovery + include ActionWebService::Protocol::Soap + include ActionWebService::Protocol::XmlRpc + include ActionWebService::Container::Direct + include ActionWebService::Container::Delegated + include ActionWebService::Container::ActionController + include ActionWebService::Invocation + include ActionWebService::Dispatcher + include ActionWebService::Dispatcher::ActionController + include ActionWebService::Scaffolding +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/api.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/api.rb new file mode 100644 index 000000000..d16dc420d --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/api.rb @@ -0,0 +1,297 @@ +module ActionWebService # :nodoc: + module API # :nodoc: + # A web service API class specifies the methods that will be available for + # invocation for an API. It also contains metadata such as the method type + # signature hints. + # + # It is not intended to be instantiated. + # + # It is attached to web service implementation classes like + # ActionWebService::Base and ActionController::Base derivatives by using + # container.web_service_api, where container is an + # ActionController::Base or a ActionWebService::Base. + # + # See ActionWebService::Container::Direct::ClassMethods for an example + # of use. + class Base + # Whether to transform the public API method names into camel-cased names + class_inheritable_option :inflect_names, true + + # By default only HTTP POST requests are processed + class_inheritable_option :allowed_http_methods, [ :post ] + + # Whether to allow ActiveRecord::Base models in :expects. + # The default is +false+; you should be aware of the security implications + # of allowing this, and ensure that you don't allow remote callers to + # easily overwrite data they should not have access to. + class_inheritable_option :allow_active_record_expects, false + + # If present, the name of a method to call when the remote caller + # tried to call a nonexistent method. Semantically equivalent to + # +method_missing+. + class_inheritable_option :default_api_method + + # Disallow instantiation + private_class_method :new, :allocate + + class << self + include ActionWebService::SignatureTypes + + # API methods have a +name+, which must be the Ruby method name to use when + # performing the invocation on the web service object. + # + # The signatures for the method input parameters and return value can + # by specified in +options+. + # + # A signature is an array of one or more parameter specifiers. + # A parameter specifier can be one of the following: + # + # * A symbol or string representing one of the Action Web Service base types. + # See ActionWebService::SignatureTypes for a canonical list of the base types. + # * The Class object of the parameter type + # * A single-element Array containing one of the two preceding items. This + # will cause Action Web Service to treat the parameter at that position + # as an array containing only values of the given type. + # * A Hash containing as key the name of the parameter, and as value + # one of the three preceding items + # + # If no method input parameter or method return value signatures are given, + # the method is assumed to take no parameters and/or return no values of + # interest, and any values that are received by the server will be + # discarded and ignored. + # + # Valid options: + # [:expects] Signature for the method input parameters + # [:returns] Signature for the method return value + # [:expects_and_returns] Signature for both input parameters and return value + def api_method(name, options={}) + unless options.is_a?(Hash) + raise(ActionWebServiceError, "Expected a Hash for options") + end + validate_options([:expects, :returns, :expects_and_returns], options.keys) + if options[:expects_and_returns] + expects = options[:expects_and_returns] + returns = options[:expects_and_returns] + else + expects = options[:expects] + returns = options[:returns] + end + expects = canonical_signature(expects) + returns = canonical_signature(returns) + if expects + expects.each do |type| + type = type.element_type if type.is_a?(ArrayType) + if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects + raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects") + end + end + end + name = name.to_sym + public_name = public_api_method_name(name) + method = Method.new(name, public_name, expects, returns) + write_inheritable_hash("api_methods", name => method) + write_inheritable_hash("api_public_method_names", public_name => name) + end + + # Whether the given method name is a service method on this API + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # end + # + # ProjectsApi.has_api_method?('GetCount') #=> false + # ProjectsApi.has_api_method?(:getCount) #=> true + def has_api_method?(name) + api_methods.has_key?(name) + end + + # Whether the given public method name has a corresponding service method + # on this API + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # end + # + # ProjectsApi.has_api_method?(:getCount) #=> false + # ProjectsApi.has_api_method?('GetCount') #=> true + def has_public_api_method?(public_name) + api_public_method_names.has_key?(public_name) + end + + # The corresponding public method name for the given service method name + # + # ProjectsApi.public_api_method_name('GetCount') #=> "GetCount" + # ProjectsApi.public_api_method_name(:getCount) #=> "GetCount" + def public_api_method_name(name) + if inflect_names + name.to_s.camelize + else + name.to_s + end + end + + # The corresponding service method name for the given public method name + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # end + # + # ProjectsApi.api_method_name('GetCount') #=> :getCount + def api_method_name(public_name) + api_public_method_names[public_name] + end + + # A Hash containing all service methods on this API, and their + # associated metadata. + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # api_method :getCompletedCount, :returns => [:int] + # end + # + # ProjectsApi.api_methods #=> + # {:getCount=>#, + # :getCompletedCount=>#} + # ProjectsApi.api_methods[:getCount].public_name #=> "GetCount" + def api_methods + read_inheritable_attribute("api_methods") || {} + end + + # The Method instance for the given public API method name, if any + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # api_method :getCompletedCount, :returns => [:int] + # end + # + # ProjectsApi.public_api_method_instance('GetCount') #=> <# + # ProjectsApi.public_api_method_instance(:getCount) #=> nil + def public_api_method_instance(public_method_name) + api_method_instance(api_method_name(public_method_name)) + end + + # The Method instance for the given API method name, if any + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # api_method :getCompletedCount, :returns => [:int] + # end + # + # ProjectsApi.api_method_instance(:getCount) #=> + # ProjectsApi.api_method_instance('GetCount') #=> + def api_method_instance(method_name) + api_methods[method_name] + end + + # The Method instance for the default API method, if any + def default_api_method_instance + return nil unless name = default_api_method + instance = read_inheritable_attribute("default_api_method_instance") + if instance && instance.name == name + return instance + end + instance = Method.new(name, public_api_method_name(name), nil, nil) + write_inheritable_attribute("default_api_method_instance", instance) + instance + end + + private + def api_public_method_names + read_inheritable_attribute("api_public_method_names") || {} + end + + def validate_options(valid_option_keys, supplied_option_keys) + unknown_option_keys = supplied_option_keys - valid_option_keys + unless unknown_option_keys.empty? + raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}") + end + end + end + end + + # Represents an API method and its associated metadata, and provides functionality + # to assist in commonly performed API method tasks. + class Method + attr :name + attr :public_name + attr :expects + attr :returns + + def initialize(name, public_name, expects, returns) + @name = name + @public_name = public_name + @expects = expects + @returns = returns + @caster = ActionWebService::Casting::BaseCaster.new(self) + end + + # The list of parameter names for this method + def param_names + return [] unless @expects + @expects.map{ |type| type.name } + end + + # Casts a set of Ruby values into the expected Ruby values + def cast_expects(params) + @caster.cast_expects(params) + end + + # Cast a Ruby return value into the expected Ruby value + def cast_returns(return_value) + @caster.cast_returns(return_value) + end + + # Returns the index of the first expected parameter + # with the given name + def expects_index_of(param_name) + return -1 if @expects.nil? + (0..(@expects.length-1)).each do |i| + return i if @expects[i].name.to_s == param_name.to_s + end + -1 + end + + # Returns a hash keyed by parameter name for the given + # parameter list + def expects_to_hash(params) + return {} if @expects.nil? + h = {} + @expects.zip(params){ |type, param| h[type.name] = param } + h + end + + # Backwards compatibility with previous API + def [](sig_type) + case sig_type + when :expects + @expects.map{|x| compat_signature_entry(x)} + when :returns + @returns.map{|x| compat_signature_entry(x)} + end + end + + # String representation of this method + def to_s + fqn = "" + fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ") + fqn << "#{@public_name}(" + fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects + fqn << ")" + fqn + end + + private + def compat_signature_entry(entry) + if entry.array? + [compat_signature_entry(entry.element_type)] + else + if entry.spec.is_a?(Hash) + {entry.spec.keys.first => entry.type_class} + else + entry.type_class + end + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/base.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/base.rb new file mode 100644 index 000000000..6282061d8 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/base.rb @@ -0,0 +1,38 @@ +module ActionWebService # :nodoc: + class ActionWebServiceError < StandardError # :nodoc: + end + + # An Action Web Service object implements a specified API. + # + # Used by controllers operating in _Delegated_ dispatching mode. + # + # ==== Example + # + # class PersonService < ActionWebService::Base + # web_service_api PersonAPI + # + # def find_person(criteria) + # Person.find(:all) [...] + # end + # + # def delete_person(id) + # Person.find_by_id(id).destroy + # end + # end + # + # class PersonAPI < ActionWebService::API::Base + # api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]] + # api_method :delete_person, :expects => [:int] + # end + # + # class SearchCriteria < ActionWebService::Struct + # member :firstname, :string + # member :lastname, :string + # member :email, :string + # end + class Base + # Whether to report exceptions back to the caller in the protocol's exception + # format + class_inheritable_option :web_service_exception_reporting, true + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/casting.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/casting.rb new file mode 100644 index 000000000..71f422eae --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/casting.rb @@ -0,0 +1,138 @@ +require 'time' +require 'date' +require 'xmlrpc/datetime' + +module ActionWebService # :nodoc: + module Casting # :nodoc: + class CastingError < ActionWebServiceError # :nodoc: + end + + # Performs casting of arbitrary values into the correct types for the signature + class BaseCaster # :nodoc: + def initialize(api_method) + @api_method = api_method + end + + # Coerces the parameters in +params+ (an Enumerable) into the types + # this method expects + def cast_expects(params) + self.class.cast_expects(@api_method, params) + end + + # Coerces the given +return_value+ into the type returned by this + # method + def cast_returns(return_value) + self.class.cast_returns(@api_method, return_value) + end + + class << self + include ActionWebService::SignatureTypes + + def cast_expects(api_method, params) # :nodoc: + return [] if api_method.expects.nil? + api_method.expects.zip(params).map{ |type, param| cast(param, type) } + end + + def cast_returns(api_method, return_value) # :nodoc: + return nil if api_method.returns.nil? + cast(return_value, api_method.returns[0]) + end + + def cast(value, signature_type) # :nodoc: + return value if signature_type.nil? # signature.length != params.length + return nil if value.nil? + # XMLRPC protocol doesn't support nil values. It uses false instead. + # It should never happen for SOAP. + if signature_type.structured? && value.equal?(false) + return nil + end + unless signature_type.array? || signature_type.structured? + return value if canonical_type(value.class) == signature_type.type + end + if signature_type.array? + unless value.respond_to?(:entries) && !value.is_a?(String) + raise CastingError, "Don't know how to cast #{value.class} into #{signature_type.type.inspect}" + end + value.entries.map do |entry| + cast(entry, signature_type.element_type) + end + elsif signature_type.structured? + cast_to_structured_type(value, signature_type) + elsif !signature_type.custom? + cast_base_type(value, signature_type) + end + end + + def cast_base_type(value, signature_type) # :nodoc: + # This is a work-around for the fact that XML-RPC special-cases DateTime values into its own DateTime type + # in order to support iso8601 dates. This doesn't work too well for us, so we'll convert it into a Time, + # with the caveat that we won't be able to handle pre-1970 dates that are sent to us. + # + # See http://dev.rubyonrails.com/ticket/2516 + value = value.to_time if value.is_a?(XMLRPC::DateTime) + + case signature_type.type + when :int + Integer(value) + when :string + value.to_s + when :base64 + if value.is_a?(ActionWebService::Base64) + value + else + ActionWebService::Base64.new(value.to_s) + end + when :bool + return false if value.nil? + return value if value == true || value == false + case value.to_s.downcase + when '1', 'true', 'y', 'yes' + true + when '0', 'false', 'n', 'no' + false + else + raise CastingError, "Don't know how to cast #{value.class} into Boolean" + end + when :float + Float(value) + when :decimal + BigDecimal(value.to_s) + when :time + value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash) + value.kind_of?(Time) ? value : Time.parse(value.to_s) + when :date + value = "%s/%s/%s" % value.values_at(*%w[2 3 1]) if value.kind_of?(Hash) + value.kind_of?(Date) ? value : Date.parse(value.to_s) + when :datetime + value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash) + value.kind_of?(DateTime) ? value : DateTime.parse(value.to_s) + end + end + + def cast_to_structured_type(value, signature_type) # :nodoc: + obj = nil + obj = value if canonical_type(value.class) == canonical_type(signature_type.type) + obj ||= signature_type.type_class.new + if value.respond_to?(:each_pair) + klass = signature_type.type_class + value.each_pair do |name, val| + type = klass.respond_to?(:member_type) ? klass.member_type(name) : nil + val = cast(val, type) if type + # See http://dev.rubyonrails.com/ticket/3567 + val = val.to_time if val.is_a?(XMLRPC::DateTime) + obj.__send__("#{name}=", val) if obj.respond_to?(name) + end + elsif value.respond_to?(:attributes) + signature_type.each_member do |name, type| + val = value.__send__(name) + obj.__send__("#{name}=", cast(val, type)) if obj.respond_to?(name) + end + else + raise CastingError, "Don't know how to cast #{value.class} to #{signature_type.type_class}" + end + obj + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/client.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client.rb new file mode 100644 index 000000000..2a1e33054 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client.rb @@ -0,0 +1,3 @@ +require 'action_web_service/client/base' +require 'action_web_service/client/soap_client' +require 'action_web_service/client/xmlrpc_client' diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb new file mode 100644 index 000000000..9dada7bf9 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb @@ -0,0 +1,28 @@ +module ActionWebService # :nodoc: + module Client # :nodoc: + class ClientError < StandardError # :nodoc: + end + + class Base # :nodoc: + def initialize(api, endpoint_uri) + @api = api + @endpoint_uri = endpoint_uri + end + + def method_missing(name, *args) # :nodoc: + call_name = method_name(name) + return super(name, *args) if call_name.nil? + self.perform_invocation(call_name, args) + end + + private + def method_name(name) + if @api.has_api_method?(name.to_sym) + name.to_s + elsif @api.has_public_api_method?(name.to_s) + @api.api_method_name(name.to_s).to_s + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb new file mode 100644 index 000000000..ebabd8ea8 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb @@ -0,0 +1,113 @@ +require 'soap/rpc/driver' +require 'uri' + +module ActionWebService # :nodoc: + module Client # :nodoc: + + # Implements SOAP client support (using RPC encoding for the messages). + # + # ==== Example Usage + # + # class PersonAPI < ActionWebService::API::Base + # api_method :find_all, :returns => [[Person]] + # end + # + # soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...") + # persons = soap_client.find_all + # + class Soap < Base + # provides access to the underlying soap driver + attr_reader :driver + + # Creates a new web service client using the SOAP RPC protocol. + # + # +api+ must be an ActionWebService::API::Base derivative, and + # +endpoint_uri+ must point at the relevant URL to which protocol requests + # will be sent with HTTP POST. + # + # Valid options: + # [:namespace] If the remote server has used a custom namespace to + # declare its custom types, you can specify it here. This would + # be the namespace declared with a [WebService(Namespace = "http://namespace")] attribute + # in .NET, for example. + # [:driver_options] If you want to supply any custom SOAP RPC driver + # options, you can provide them as a Hash here + # + # The :driver_options option can be used to configure the backend SOAP + # RPC driver. An example of configuring the SOAP backend to do + # client-certificate authenticated SSL connections to the server: + # + # opts = {} + # opts['protocol.http.ssl_config.verify_mode'] = 'OpenSSL::SSL::VERIFY_PEER' + # opts['protocol.http.ssl_config.client_cert'] = client_cert_file_path + # opts['protocol.http.ssl_config.client_key'] = client_key_file_path + # opts['protocol.http.ssl_config.ca_file'] = ca_cert_file_path + # client = ActionWebService::Client::Soap.new(api, 'https://some/service', :driver_options => opts) + def initialize(api, endpoint_uri, options={}) + super(api, endpoint_uri) + @namespace = options[:namespace] || 'urn:ActionWebService' + @driver_options = options[:driver_options] || {} + @protocol = ActionWebService::Protocol::Soap::SoapProtocol.new @namespace + @soap_action_base = options[:soap_action_base] + @soap_action_base ||= URI.parse(endpoint_uri).path + @driver = create_soap_rpc_driver(api, endpoint_uri) + @driver_options.each do |name, value| + @driver.options[name.to_s] = value + end + end + + protected + def perform_invocation(method_name, args) + method = @api.api_methods[method_name.to_sym] + args = method.cast_expects(args.dup) rescue args + return_value = @driver.send(method_name, *args) + method.cast_returns(return_value.dup) rescue return_value + end + + def soap_action(method_name) + "#{@soap_action_base}/#{method_name}" + end + + private + def create_soap_rpc_driver(api, endpoint_uri) + @protocol.register_api(api) + driver = SoapDriver.new(endpoint_uri, nil) + driver.mapping_registry = @protocol.marshaler.registry + api.api_methods.each do |name, method| + qname = XSD::QName.new(@namespace, method.public_name) + action = soap_action(method.public_name) + expects = method.expects + returns = method.returns + param_def = [] + if expects + expects.each do |type| + type_binding = @protocol.marshaler.lookup_type(type) + if SOAP::Version >= "1.5.5" + param_def << ['in', type.name.to_s, [type_binding.type.type_class.to_s]] + else + param_def << ['in', type.name, type_binding.mapping] + end + end + end + if returns + type_binding = @protocol.marshaler.lookup_type(returns[0]) + if SOAP::Version >= "1.5.5" + param_def << ['retval', 'return', [type_binding.type.type_class.to_s]] + else + param_def << ['retval', 'return', type_binding.mapping] + end + end + driver.add_method(qname, action, method.name.to_s, param_def) + end + driver + end + + class SoapDriver < SOAP::RPC::Driver # :nodoc: + def add_method(qname, soapaction, name, param_def) + @proxy.add_rpc_method(qname, soapaction, name, param_def) + add_rpc_method_interface(name, param_def) + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb new file mode 100644 index 000000000..42b5c5d4f --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb @@ -0,0 +1,58 @@ +require 'uri' +require 'xmlrpc/client' + +module ActionWebService # :nodoc: + module Client # :nodoc: + + # Implements XML-RPC client support + # + # ==== Example Usage + # + # class BloggerAPI < ActionWebService::API::Base + # inflect_names false + # api_method :getRecentPosts, :returns => [[Blog::Post]] + # end + # + # blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger") + # posts = blog.getRecentPosts + class XmlRpc < Base + + # Creates a new web service client using the XML-RPC protocol. + # + # +api+ must be an ActionWebService::API::Base derivative, and + # +endpoint_uri+ must point at the relevant URL to which protocol requests + # will be sent with HTTP POST. + # + # Valid options: + # [:handler_name] If the remote server defines its services inside special + # handler (the Blogger API uses a "blogger" handler name for example), + # provide it here, or your method calls will fail + def initialize(api, endpoint_uri, options={}) + @api = api + @handler_name = options[:handler_name] + @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new + @client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout]) + end + + protected + def perform_invocation(method_name, args) + method = @api.api_methods[method_name.to_sym] + if method.expects && method.expects.length != args.length + raise(ArgumentError, "#{method.public_name}: wrong number of arguments (#{args.length} for #{method.expects.length})") + end + args = method.cast_expects(args.dup) rescue args + if method.expects + method.expects.each_with_index{ |type, i| args[i] = @protocol.value_to_xmlrpc_wire_format(args[i], type) } + end + ok, return_value = @client.call2(public_name(method_name), *args) + return (method.cast_returns(return_value.dup) rescue return_value) if ok + raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}") + end + + def public_name(method_name) + public_name = @api.public_api_method_name(method_name) + @handler_name ? "#{@handler_name}.#{public_name}" : public_name + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/container.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container.rb new file mode 100644 index 000000000..13d9d8ab5 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container.rb @@ -0,0 +1,3 @@ +require 'action_web_service/container/direct_container' +require 'action_web_service/container/delegated_container' +require 'action_web_service/container/action_controller_container' diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb new file mode 100644 index 000000000..bbc28083c --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb @@ -0,0 +1,93 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module ActionController # :nodoc: + def self.included(base) # :nodoc: + class << base + include ClassMethods + alias_method_chain :inherited, :api + alias_method_chain :web_service_api, :require + end + end + + module ClassMethods + # Creates a client for accessing remote web services, using the + # given +protocol+ to communicate with the +endpoint_uri+. + # + # ==== Example + # + # class MyController < ActionController::Base + # web_client_api :blogger, :xmlrpc, "http://blogger.com/myblog/api/RPC2", :handler_name => 'blogger' + # end + # + # In this example, a protected method named blogger will + # now exist on the controller, and calling it will return the + # XML-RPC client object for working with that remote service. + # + # +options+ is the set of protocol client specific options (see + # a protocol client class for details). + # + # If your API definition does not exist on the load path with the + # correct rules for it to be found using +name+, you can pass in + # the API definition class via +options+, using a key of :api + def web_client_api(name, protocol, endpoint_uri, options={}) + unless method_defined?(name) + api_klass = options.delete(:api) || require_web_service_api(name) + class_eval do + define_method(name) do + create_web_service_client(api_klass, protocol, endpoint_uri, options) + end + protected name + end + end + end + + def web_service_api_with_require(definition=nil) # :nodoc: + return web_service_api_without_require if definition.nil? + case definition + when String, Symbol + klass = require_web_service_api(definition) + else + klass = definition + end + web_service_api_without_require(klass) + end + + def require_web_service_api(name) # :nodoc: + case name + when String, Symbol + file_name = name.to_s.underscore + "_api" + class_name = file_name.camelize + class_names = [class_name, class_name.sub(/Api$/, 'API')] + begin + require_dependency(file_name) + rescue LoadError => load_error + requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1] + msg = requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}" + raise LoadError.new(msg).copy_blame!(load_error) + end + klass = nil + class_names.each do |name| + klass = name.constantize rescue nil + break unless klass.nil? + end + unless klass + raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found") + end + klass + else + raise(ArgumentError, "expected String or Symbol argument") + end + end + + private + def inherited_with_api(child) + inherited_without_api(child) + begin child.web_service_api(child.controller_path) + rescue MissingSourceFile => e + raise unless e.is_missing?("apis/#{child.controller_path}_api") + end + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb new file mode 100644 index 000000000..5477f8d10 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb @@ -0,0 +1,86 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module Delegated # :nodoc: + class ContainerError < ActionWebServiceError # :nodoc: + end + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + base.send(:include, ActionWebService::Container::Delegated::InstanceMethods) + end + + module ClassMethods + # Declares a web service that will provide access to the API of the given + # +object+. +object+ must be an ActionWebService::Base derivative. + # + # Web service object creation can either be _immediate_, where the object + # instance is given at class definition time, or _deferred_, where + # object instantiation is delayed until request time. + # + # ==== Immediate web service object example + # + # class ApiController < ApplicationController + # web_service_dispatching_mode :delegated + # + # web_service :person, PersonService.new + # end + # + # For deferred instantiation, a block should be given instead of an + # object instance. This block will be executed in controller instance + # context, so it can rely on controller instance variables being present. + # + # ==== Deferred web service object example + # + # class ApiController < ApplicationController + # web_service_dispatching_mode :delegated + # + # web_service(:person) { PersonService.new(request.env) } + # end + def web_service(name, object=nil, &block) + if (object && block_given?) || (object.nil? && block.nil?) + raise(ContainerError, "either service, or a block must be given") + end + name = name.to_sym + if block_given? + info = { name => { :block => block } } + else + info = { name => { :object => object } } + end + write_inheritable_hash("web_services", info) + call_web_service_definition_callbacks(self, name, info) + end + + # Whether this service contains a service with the given +name+ + def has_web_service?(name) + web_services.has_key?(name.to_sym) + end + + def web_services # :nodoc: + read_inheritable_attribute("web_services") || {} + end + + def add_web_service_definition_callback(&block) # :nodoc: + write_inheritable_array("web_service_definition_callbacks", [block]) + end + + private + def call_web_service_definition_callbacks(container_class, web_service_name, service_info) + (read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block| + block.call(container_class, web_service_name, service_info) + end + end + end + + module InstanceMethods # :nodoc: + def web_service_object(web_service_name) + info = self.class.web_services[web_service_name.to_sym] + unless info + raise(ContainerError, "no such web service '#{web_service_name}'") + end + service = info[:block] + service ? self.instance_eval(&service) : info[:object] + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb new file mode 100644 index 000000000..8818d8f45 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb @@ -0,0 +1,69 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module Direct # :nodoc: + class ContainerError < ActionWebServiceError # :nodoc: + end + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + end + + module ClassMethods + # Attaches ActionWebService API +definition+ to the calling class. + # + # Action Controllers can have a default associated API, removing the need + # to call this method if you follow the Action Web Service naming conventions. + # + # A controller with a class name of GoogleSearchController will + # implicitly load app/apis/google_search_api.rb, and expect the + # API definition class to be named GoogleSearchAPI or + # GoogleSearchApi. + # + # ==== Service class example + # + # class MyService < ActionWebService::Base + # web_service_api MyAPI + # end + # + # class MyAPI < ActionWebService::API::Base + # ... + # end + # + # ==== Controller class example + # + # class MyController < ActionController::Base + # web_service_api MyAPI + # end + # + # class MyAPI < ActionWebService::API::Base + # ... + # end + def web_service_api(definition=nil) + if definition.nil? + read_inheritable_attribute("web_service_api") + else + if definition.is_a?(Symbol) + raise(ContainerError, "symbols can only be used for #web_service_api inside of a controller") + end + unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base) + raise(ContainerError, "#{definition.to_s} is not a valid API definition") + end + write_inheritable_attribute("web_service_api", definition) + call_web_service_api_callbacks(self, definition) + end + end + + def add_web_service_api_callback(&block) # :nodoc: + write_inheritable_array("web_service_api_callbacks", [block]) + end + + private + def call_web_service_api_callbacks(container_class, definition) + (read_inheritable_attribute("web_service_api_callbacks") || []).each do |block| + block.call(container_class, definition) + end + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb new file mode 100644 index 000000000..601d83137 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb @@ -0,0 +1,2 @@ +require 'action_web_service/dispatcher/abstract' +require 'action_web_service/dispatcher/action_controller_dispatcher' diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb new file mode 100644 index 000000000..cb94d649e --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb @@ -0,0 +1,207 @@ +require 'benchmark' + +module ActionWebService # :nodoc: + module Dispatcher # :nodoc: + class DispatcherError < ActionWebService::ActionWebServiceError # :nodoc: + def initialize(*args) + super + set_backtrace(caller) + end + end + + def self.included(base) # :nodoc: + base.class_inheritable_option(:web_service_dispatching_mode, :direct) + base.class_inheritable_option(:web_service_exception_reporting, true) + base.send(:include, ActionWebService::Dispatcher::InstanceMethods) + end + + module InstanceMethods # :nodoc: + private + def invoke_web_service_request(protocol_request) + invocation = web_service_invocation(protocol_request) + if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol) + xmlrpc_multicall_invoke(invocation) + else + web_service_invoke(invocation) + end + end + + def web_service_direct_invoke(invocation) + @method_params = invocation.method_ordered_params + arity = method(invocation.api_method.name).arity rescue 0 + if arity < 0 || arity > 0 + params = @method_params + else + params = [] + end + web_service_filtered_invoke(invocation, params) + end + + def web_service_delegated_invoke(invocation) + web_service_filtered_invoke(invocation, invocation.method_ordered_params) + end + + def web_service_filtered_invoke(invocation, params) + cancellation_reason = nil + return_value = invocation.service.perform_invocation(invocation.api_method.name, params) do |x| + cancellation_reason = x + end + if cancellation_reason + raise(DispatcherError, "request canceled: #{cancellation_reason}") + end + return_value + end + + def web_service_invoke(invocation) + case web_service_dispatching_mode + when :direct + return_value = web_service_direct_invoke(invocation) + when :delegated, :layered + return_value = web_service_delegated_invoke(invocation) + end + web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value) + end + + def xmlrpc_multicall_invoke(invocations) + responses = [] + invocations.each do |invocation| + if invocation.is_a?(Hash) + responses << [invocation, nil] + next + end + begin + case web_service_dispatching_mode + when :direct + return_value = web_service_direct_invoke(invocation) + when :delegated, :layered + return_value = web_service_delegated_invoke(invocation) + end + api_method = invocation.api_method + if invocation.api.has_api_method?(api_method.name) + response_type = (api_method.returns ? api_method.returns[0] : nil) + return_value = api_method.cast_returns(return_value) + else + response_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0) + end + responses << [return_value, response_type] + rescue Exception => e + responses << [{ 'faultCode' => 3, 'faultString' => e.message }, nil] + end + end + invocation = invocations[0] + invocation.protocol.encode_multicall_response(responses, invocation.protocol_options) + end + + def web_service_invocation(request, level = 0) + public_method_name = request.method_name + invocation = Invocation.new + invocation.protocol = request.protocol + invocation.protocol_options = request.protocol_options + invocation.service_name = request.service_name + if web_service_dispatching_mode == :layered + case invocation.protocol + when Protocol::Soap::SoapProtocol + soap_action = request.protocol_options[:soap_action] + if soap_action && soap_action =~ /^\/\w+\/(\w+)\// + invocation.service_name = $1 + end + when Protocol::XmlRpc::XmlRpcProtocol + if request.method_name =~ /^([^\.]+)\.(.*)$/ + public_method_name = $2 + invocation.service_name = $1 + end + end + end + if invocation.protocol.is_a? Protocol::XmlRpc::XmlRpcProtocol + if public_method_name == 'multicall' && invocation.service_name == 'system' + if level > 0 + raise(DispatcherError, "Recursive system.multicall invocations not allowed") + end + multicall = request.method_params.dup + unless multicall.is_a?(Array) && multicall[0].is_a?(Array) + raise(DispatcherError, "Malformed multicall (expected array of Hash elements)") + end + multicall = multicall[0] + return multicall.map do |item| + raise(DispatcherError, "Multicall elements must be Hash") unless item.is_a?(Hash) + raise(DispatcherError, "Multicall elements must contain a 'methodName' key") unless item.has_key?('methodName') + method_name = item['methodName'] + params = item.has_key?('params') ? item['params'] : [] + multicall_request = request.dup + multicall_request.method_name = method_name + multicall_request.method_params = params + begin + web_service_invocation(multicall_request, level + 1) + rescue Exception => e + {'faultCode' => 4, 'faultMessage' => e.message} + end + end + end + end + case web_service_dispatching_mode + when :direct + invocation.api = self.class.web_service_api + invocation.service = self + when :delegated, :layered + invocation.service = web_service_object(invocation.service_name) + invocation.api = invocation.service.class.web_service_api + end + if invocation.api.nil? + raise(DispatcherError, "no API attached to #{invocation.service.class}") + end + invocation.protocol.register_api(invocation.api) + request.api = invocation.api + if invocation.api.has_public_api_method?(public_method_name) + invocation.api_method = invocation.api.public_api_method_instance(public_method_name) + else + if invocation.api.default_api_method.nil? + raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}") + else + invocation.api_method = invocation.api.default_api_method_instance + end + end + if invocation.service.nil? + raise(DispatcherError, "no service available for service name #{invocation.service_name}") + end + unless invocation.service.respond_to?(invocation.api_method.name) + raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method.name})") + end + request.api_method = invocation.api_method + begin + invocation.method_ordered_params = invocation.api_method.cast_expects(request.method_params.dup) + rescue + logger.warn "Casting of method parameters failed" unless logger.nil? + invocation.method_ordered_params = request.method_params + end + request.method_params = invocation.method_ordered_params + invocation.method_named_params = {} + invocation.api_method.param_names.inject(0) do |m, n| + invocation.method_named_params[n] = invocation.method_ordered_params[m] + m + 1 + end + invocation + end + + def web_service_create_response(protocol, protocol_options, api, api_method, return_value) + if api.has_api_method?(api_method.name) + return_type = api_method.returns ? api_method.returns[0] : nil + return_value = api_method.cast_returns(return_value) + else + return_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0) + end + protocol.encode_response(api_method.public_name + 'Response', return_value, return_type, protocol_options) + end + + class Invocation # :nodoc: + attr_accessor :protocol + attr_accessor :protocol_options + attr_accessor :service_name + attr_accessor :api + attr_accessor :api_method + attr_accessor :method_ordered_params + attr_accessor :method_named_params + attr_accessor :service + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb new file mode 100644 index 000000000..f9995197a --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb @@ -0,0 +1,379 @@ +require 'benchmark' +require 'builder/xmlmarkup' + +module ActionWebService # :nodoc: + module Dispatcher # :nodoc: + module ActionController # :nodoc: + def self.included(base) # :nodoc: + class << base + include ClassMethods + alias_method_chain :inherited, :action_controller + end + base.class_eval do + alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke + end + base.add_web_service_api_callback do |klass, api| + if klass.web_service_dispatching_mode == :direct + klass.class_eval 'def api; dispatch_web_service_request; end' + end + end + base.add_web_service_definition_callback do |klass, name, info| + if klass.web_service_dispatching_mode == :delegated + klass.class_eval "def #{name}; dispatch_web_service_request; end" + elsif klass.web_service_dispatching_mode == :layered + klass.class_eval 'def api; dispatch_web_service_request; end' + end + end + base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods) + end + + module ClassMethods # :nodoc: + def inherited_with_action_controller(child) + inherited_without_action_controller(child) + child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlAction) + end + end + + module InstanceMethods # :nodoc: + private + def dispatch_web_service_request + method = request.method.to_s.upcase + allowed_methods = self.class.web_service_api ? (self.class.web_service_api.allowed_http_methods || []) : [ :post ] + allowed_methods = allowed_methods.map{|m| m.to_s.upcase } + if !allowed_methods.include?(method) + render :text => "#{method} not supported", :status=>500 + return + end + exception = nil + begin + ws_request = discover_web_service_request(request) + rescue Exception => e + exception = e + end + if ws_request + ws_response = nil + exception = nil + bm = Benchmark.measure do + begin + ws_response = invoke_web_service_request(ws_request) + rescue Exception => e + exception = e + end + end + log_request(ws_request, request.raw_post) + if exception + log_error(exception) unless logger.nil? + send_web_service_error_response(ws_request, exception) + else + send_web_service_response(ws_response, bm.real) + end + else + exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message") + log_error(exception) unless logger.nil? + send_web_service_error_response(ws_request, exception) + end + rescue Exception => e + log_error(e) unless logger.nil? + send_web_service_error_response(ws_request, e) + end + + def send_web_service_response(ws_response, elapsed=nil) + log_response(ws_response, elapsed) + options = { :type => ws_response.content_type, :disposition => 'inline' } + send_data(ws_response.body, options) + end + + def send_web_service_error_response(ws_request, exception) + if ws_request + unless self.class.web_service_exception_reporting + exception = DispatcherError.new("Internal server error (exception raised)") + end + api_method = ws_request.api_method + public_method_name = api_method ? api_method.public_name : ws_request.method_name + return_type = ActionWebService::SignatureTypes.canonical_signature_entry(Exception, 0) + ws_response = ws_request.protocol.encode_response(public_method_name + 'Response', exception, return_type, ws_request.protocol_options) + send_web_service_response(ws_response) + else + if self.class.web_service_exception_reporting + message = exception.message + backtrace = "\nBacktrace:\n#{exception.backtrace.join("\n")}" + else + message = "Exception raised" + backtrace = "" + end + render :text => "Internal protocol error: #{message}#{backtrace}", :status => 500 + end + end + + def web_service_direct_invoke(invocation) + invocation.method_named_params.each do |name, value| + params[name] = value + end + web_service_direct_invoke_without_controller(invocation) + end + + def log_request(ws_request, body) + unless logger.nil? + name = ws_request.method_name + api_method = ws_request.api_method + params = ws_request.method_params + if api_method && api_method.expects + params = api_method.expects.zip(params).map{ |type, param| "#{type.name}=>#{param.inspect}" } + else + params = params.map{ |param| param.inspect } + end + service = ws_request.service_name + logger.debug("\nWeb Service Request: #{name}(#{params.join(", ")}) Entrypoint: #{service}") + logger.debug(indent(body)) + end + end + + def log_response(ws_response, elapsed=nil) + unless logger.nil? + elapsed = (elapsed ? " (%f):" % elapsed : ":") + logger.debug("\nWeb Service Response" + elapsed + " => #{ws_response.return_value.inspect}") + logger.debug(indent(ws_response.body)) + end + end + + def indent(body) + body.split(/\n/).map{|x| " #{x}"}.join("\n") + end + end + + module WsdlAction # :nodoc: + XsdNs = 'http://www.w3.org/2001/XMLSchema' + WsdlNs = 'http://schemas.xmlsoap.org/wsdl/' + SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/' + SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/' + SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http' + + def wsdl + case request.method + when :get + begin + options = { :type => 'text/xml', :disposition => 'inline' } + send_data(to_wsdl, options) + rescue Exception => e + log_error(e) unless logger.nil? + end + when :post + render :text => 'POST not supported', :status => 500 + end + end + + private + def base_uri + host = request.host_with_port + relative_url_root = request.relative_url_root + scheme = request.ssl? ? 'https' : 'http' + '%s://%s%s/%s/' % [scheme, host, relative_url_root, self.class.controller_path] + end + + def to_wsdl + xml = '' + dispatching_mode = web_service_dispatching_mode + global_service_name = wsdl_service_name + namespace = wsdl_namespace || 'urn:ActionWebService' + soap_action_base = "/#{controller_name}" + + marshaler = ActionWebService::Protocol::Soap::SoapMarshaler.new(namespace) + apis = {} + case dispatching_mode + when :direct + api = self.class.web_service_api + web_service_name = controller_class_name.sub(/Controller$/, '').underscore + apis[web_service_name] = [api, register_api(api, marshaler)] + when :delegated, :layered + self.class.web_services.each do |web_service_name, info| + service = web_service_object(web_service_name) + api = service.class.web_service_api + apis[web_service_name] = [api, register_api(api, marshaler)] + end + end + custom_types = [] + apis.values.each do |api, bindings| + bindings.each do |b| + custom_types << b unless custom_types.include?(b) + end + end + + xm = Builder::XmlMarkup.new(:target => xml, :indent => 2) + xm.instruct! + xm.definitions('name' => wsdl_service_name, + 'targetNamespace' => namespace, + 'xmlns:typens' => namespace, + 'xmlns:xsd' => XsdNs, + 'xmlns:soap' => SoapNs, + 'xmlns:soapenc' => SoapEncodingNs, + 'xmlns:wsdl' => WsdlNs, + 'xmlns' => WsdlNs) do + # Generate XSD + if custom_types.size > 0 + xm.types do + xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do + custom_types.each do |binding| + case + when binding.type.array? + xm.xsd(:complexType, 'name' => binding.type_name) do + xm.xsd(:complexContent) do + xm.xsd(:restriction, 'base' => 'soapenc:Array') do + xm.xsd(:attribute, 'ref' => 'soapenc:arrayType', + 'wsdl:arrayType' => binding.element_binding.qualified_type_name('typens') + '[]') + end + end + end + when binding.type.structured? + xm.xsd(:complexType, 'name' => binding.type_name) do + xm.xsd(:all) do + binding.type.each_member do |name, type| + b = marshaler.register_type(type) + xm.xsd(:element, 'name' => name, 'type' => b.qualified_type_name('typens')) + end + end + end + end + end + end + end + end + + # APIs + apis.each do |api_name, values| + api = values[0] + api.api_methods.each do |name, method| + gen = lambda do |msg_name, direction| + xm.message('name' => message_name_for(api_name, msg_name)) do + sym = nil + if direction == :out + returns = method.returns + if returns + binding = marshaler.register_type(returns[0]) + xm.part('name' => 'return', 'type' => binding.qualified_type_name('typens')) + end + else + expects = method.expects + expects.each do |type| + binding = marshaler.register_type(type) + xm.part('name' => type.name, 'type' => binding.qualified_type_name('typens')) + end if expects + end + end + end + public_name = method.public_name + gen.call(public_name, :in) + gen.call("#{public_name}Response", :out) + end + + # Port + port_name = port_name_for(global_service_name, api_name) + xm.portType('name' => port_name) do + api.api_methods.each do |name, method| + xm.operation('name' => method.public_name) do + xm.input('message' => "typens:" + message_name_for(api_name, method.public_name)) + xm.output('message' => "typens:" + message_name_for(api_name, "#{method.public_name}Response")) + end + end + end + + # Bind it + binding_name = binding_name_for(global_service_name, api_name) + xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do + xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport) + api.api_methods.each do |name, method| + xm.operation('name' => method.public_name) do + case web_service_dispatching_mode + when :direct + soap_action = soap_action_base + "/api/" + method.public_name + when :delegated, :layered + soap_action = soap_action_base \ + + "/" + api_name.to_s \ + + "/" + method.public_name + end + xm.soap(:operation, 'soapAction' => soap_action) + xm.input do + xm.soap(:body, + 'use' => 'encoded', + 'namespace' => namespace, + 'encodingStyle' => SoapEncodingNs) + end + xm.output do + xm.soap(:body, + 'use' => 'encoded', + 'namespace' => namespace, + 'encodingStyle' => SoapEncodingNs) + end + end + end + end + end + + # Define it + xm.service('name' => "#{global_service_name}Service") do + apis.each do |api_name, values| + port_name = port_name_for(global_service_name, api_name) + binding_name = binding_name_for(global_service_name, api_name) + case web_service_dispatching_mode + when :direct, :layered + binding_target = 'api' + when :delegated + binding_target = api_name.to_s + end + xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do + xm.soap(:address, 'location' => "#{base_uri}#{binding_target}") + end + end + end + end + end + + def port_name_for(global_service, service) + "#{global_service}#{service.to_s.camelize}Port" + end + + def binding_name_for(global_service, service) + "#{global_service}#{service.to_s.camelize}Binding" + end + + def message_name_for(api_name, message_name) + mode = web_service_dispatching_mode + if mode == :layered || mode == :delegated + api_name.to_s + '-' + message_name + else + message_name + end + end + + def register_api(api, marshaler) + bindings = {} + traverse_custom_types(api, marshaler, bindings) do |binding| + bindings[binding] = nil unless bindings.has_key?(binding) + element_binding = binding.element_binding + bindings[element_binding] = nil if element_binding && !bindings.has_key?(element_binding) + end + bindings.keys + end + + def traverse_custom_types(api, marshaler, bindings, &block) + api.api_methods.each do |name, method| + expects, returns = method.expects, method.returns + expects.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if expects + returns.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if returns + end + end + + def traverse_type(marshaler, type, bindings, &block) + binding = marshaler.register_type(type) + return if bindings.has_key?(binding) + bindings[binding] = nil + yield binding + if type.array? + yield marshaler.register_type(type.element_type) + type = type.element_type + end + type.each_member{ |name, type| traverse_type(marshaler, type, bindings, &block) } if type.structured? + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb new file mode 100644 index 000000000..2a9121ee2 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb @@ -0,0 +1,202 @@ +module ActionWebService # :nodoc: + module Invocation # :nodoc: + class InvocationError < ActionWebService::ActionWebServiceError # :nodoc: + end + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + base.send(:include, ActionWebService::Invocation::InstanceMethods) + end + + # Invocation interceptors provide a means to execute custom code before + # and after method invocations on ActionWebService::Base objects. + # + # When running in _Direct_ dispatching mode, ActionController filters + # should be used for this functionality instead. + # + # The semantics of invocation interceptors are the same as ActionController + # filters, and accept the same parameters and options. + # + # A _before_ interceptor can also cancel execution by returning +false+, + # or returning a [false, "cancel reason"] array if it wishes to supply + # a reason for canceling the request. + # + # === Example + # + # class CustomService < ActionWebService::Base + # before_invocation :intercept_add, :only => [:add] + # + # def add(a, b) + # a + b + # end + # + # private + # def intercept_add + # return [false, "permission denied"] # cancel it + # end + # end + # + # Options: + # [:except] A list of methods for which the interceptor will NOT be called + # [:only] A list of methods for which the interceptor WILL be called + module ClassMethods + # Appends the given +interceptors+ to be called + # _before_ method invocation. + def append_before_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + append_interceptors_to_chain("before", interceptors) + end + + # Prepends the given +interceptors+ to be called + # _before_ method invocation. + def prepend_before_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + prepend_interceptors_to_chain("before", interceptors) + end + + alias :before_invocation :append_before_invocation + + # Appends the given +interceptors+ to be called + # _after_ method invocation. + def append_after_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + append_interceptors_to_chain("after", interceptors) + end + + # Prepends the given +interceptors+ to be called + # _after_ method invocation. + def prepend_after_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + prepend_interceptors_to_chain("after", interceptors) + end + + alias :after_invocation :append_after_invocation + + def before_invocation_interceptors # :nodoc: + read_inheritable_attribute("before_invocation_interceptors") + end + + def after_invocation_interceptors # :nodoc: + read_inheritable_attribute("after_invocation_interceptors") + end + + def included_intercepted_methods # :nodoc: + read_inheritable_attribute("included_intercepted_methods") || {} + end + + def excluded_intercepted_methods # :nodoc: + read_inheritable_attribute("excluded_intercepted_methods") || {} + end + + private + def append_interceptors_to_chain(condition, interceptors) + write_inheritable_array("#{condition}_invocation_interceptors", interceptors) + end + + def prepend_interceptors_to_chain(condition, interceptors) + interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors") + write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors) + end + + def extract_conditions!(interceptors) + return nil unless interceptors.last.is_a? Hash + interceptors.pop + end + + def add_interception_conditions(interceptors, conditions) + return unless conditions + included, excluded = conditions[:only], conditions[:except] + write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included + write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded + end + + def condition_hash(interceptors, *methods) + interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})} + end + end + + module InstanceMethods # :nodoc: + def self.included(base) + base.class_eval do + alias_method_chain :perform_invocation, :interception + end + end + + def perform_invocation_with_interception(method_name, params, &block) + return if before_invocation(method_name, params, &block) == false + return_value = perform_invocation_without_interception(method_name, params) + after_invocation(method_name, params, return_value) + return_value + end + + def perform_invocation(method_name, params) + send(method_name, *params) + end + + def before_invocation(name, args, &block) + call_interceptors(self.class.before_invocation_interceptors, [name, args], &block) + end + + def after_invocation(name, args, result) + call_interceptors(self.class.after_invocation_interceptors, [name, args, result]) + end + + private + + def call_interceptors(interceptors, interceptor_args, &block) + if interceptors and not interceptors.empty? + interceptors.each do |interceptor| + next if method_exempted?(interceptor, interceptor_args[0].to_s) + result = case + when interceptor.is_a?(Symbol) + self.send(interceptor, *interceptor_args) + when interceptor_block?(interceptor) + interceptor.call(self, *interceptor_args) + when interceptor_class?(interceptor) + interceptor.intercept(self, *interceptor_args) + else + raise( + InvocationError, + "Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method" + ) + end + reason = nil + if result.is_a?(Array) + reason = result[1] if result[1] + result = result[0] + end + if result == false + block.call(reason) if block && reason + return false + end + end + end + end + + def interceptor_block?(interceptor) + interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1) + end + + def interceptor_class?(interceptor) + interceptor.respond_to?("intercept") + end + + def method_exempted?(interceptor, method_name) + case + when self.class.included_intercepted_methods[interceptor] + !self.class.included_intercepted_methods[interceptor].include?(method_name) + when self.class.excluded_intercepted_methods[interceptor] + self.class.excluded_intercepted_methods[interceptor].include?(method_name) + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb new file mode 100644 index 000000000..053e9cb4b --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb @@ -0,0 +1,4 @@ +require 'action_web_service/protocol/abstract' +require 'action_web_service/protocol/discovery' +require 'action_web_service/protocol/soap_protocol' +require 'action_web_service/protocol/xmlrpc_protocol' diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb new file mode 100644 index 000000000..fff5f622c --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb @@ -0,0 +1,112 @@ +module ActionWebService # :nodoc: + module Protocol # :nodoc: + class ProtocolError < ActionWebServiceError # :nodoc: + end + + class AbstractProtocol # :nodoc: + def setup(controller) + end + + def decode_action_pack_request(action_pack_request) + end + + def encode_action_pack_request(service_name, public_method_name, raw_body, options={}) + klass = options[:request_class] || SimpleActionPackRequest + request = klass.new + request.request_parameters['action'] = service_name.to_s + request.env['RAW_POST_DATA'] = raw_body + request.env['REQUEST_METHOD'] = 'POST' + request.env['HTTP_CONTENT_TYPE'] = 'text/xml' + request + end + + def decode_request(raw_request, service_name, protocol_options={}) + end + + def encode_request(method_name, params, param_types) + end + + def decode_response(raw_response) + end + + def encode_response(method_name, return_value, return_type, protocol_options={}) + end + + def protocol_client(api, protocol_name, endpoint_uri, options) + end + + def register_api(api) + end + end + + class Request # :nodoc: + attr :protocol + attr_accessor :method_name + attr_accessor :method_params + attr :service_name + attr_accessor :api + attr_accessor :api_method + attr :protocol_options + + def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil, protocol_options=nil) + @protocol = protocol + @method_name = method_name + @method_params = method_params + @service_name = service_name + @api = api + @api_method = api_method + @protocol_options = protocol_options || {} + end + end + + class Response # :nodoc: + attr :body + attr :content_type + attr :return_value + + def initialize(body, content_type, return_value) + @body = body + @content_type = content_type + @return_value = return_value + end + end + + class SimpleActionPackRequest < ActionController::AbstractRequest # :nodoc: + def initialize + @env = {} + @qparams = {} + @rparams = {} + @cookies = {} + reset_session + end + + def query_parameters + @qparams + end + + def request_parameters + @rparams + end + + def env + @env + end + + def host + '' + end + + def cookies + @cookies + end + + def session + @session + end + + def reset_session + @session = {} + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb new file mode 100644 index 000000000..3d4e0818d --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb @@ -0,0 +1,37 @@ +module ActionWebService # :nodoc: + module Protocol # :nodoc: + module Discovery # :nodoc: + def self.included(base) + base.extend(ClassMethods) + base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods) + end + + module ClassMethods # :nodoc: + def register_protocol(klass) + write_inheritable_array("web_service_protocols", [klass]) + end + end + + module InstanceMethods # :nodoc: + private + def discover_web_service_request(action_pack_request) + (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| + protocol = protocol.create(self) + request = protocol.decode_action_pack_request(action_pack_request) + return request unless request.nil? + end + nil + end + + def create_web_service_client(api, protocol_name, endpoint_uri, options) + (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| + protocol = protocol.create(self) + client = protocol.protocol_client(api, protocol_name, endpoint_uri, options) + return client unless client.nil? + end + nil + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb new file mode 100644 index 000000000..1bce496a7 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb @@ -0,0 +1,176 @@ +require 'action_web_service/protocol/soap_protocol/marshaler' +require 'soap/streamHandler' +require 'action_web_service/client/soap_client' + +module ActionWebService # :nodoc: + module API # :nodoc: + class Base # :nodoc: + def self.soap_client(endpoint_uri, options={}) + ActionWebService::Client::Soap.new self, endpoint_uri, options + end + end + end + + module Protocol # :nodoc: + module Soap # :nodoc: + def self.included(base) + base.register_protocol(SoapProtocol) + base.class_inheritable_option(:wsdl_service_name) + base.class_inheritable_option(:wsdl_namespace) + end + + class SoapProtocol < AbstractProtocol # :nodoc: + AWSEncoding = 'UTF-8' + XSDEncoding = 'UTF8' + + attr :marshaler + + def initialize(namespace=nil) + namespace ||= 'urn:ActionWebService' + @marshaler = SoapMarshaler.new namespace + end + + def self.create(controller) + SoapProtocol.new(controller.wsdl_namespace) + end + + def decode_action_pack_request(action_pack_request) + return nil unless soap_action = has_valid_soap_action?(action_pack_request) + service_name = action_pack_request.parameters['action'] + input_encoding = parse_charset(action_pack_request.env['HTTP_CONTENT_TYPE']) + protocol_options = { + :soap_action => soap_action, + :charset => input_encoding + } + decode_request(action_pack_request.raw_post, service_name, protocol_options) + end + + def encode_action_pack_request(service_name, public_method_name, raw_body, options={}) + request = super + request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name] + request + end + + def decode_request(raw_request, service_name, protocol_options={}) + envelope = SOAP::Processor.unmarshal(raw_request, :charset => protocol_options[:charset]) + unless envelope + raise ProtocolError, "Failed to parse SOAP request message" + end + request = envelope.body.request + method_name = request.elename.name + params = request.collect{ |k, v| marshaler.soap_to_ruby(request[k]) } + Request.new(self, method_name, params, service_name, nil, nil, protocol_options) + end + + def encode_request(method_name, params, param_types) + param_types.each{ |type| marshaler.register_type(type) } if param_types + qname = XSD::QName.new(marshaler.namespace, method_name) + param_def = [] + if param_types + params = param_types.zip(params).map do |type, param| + param_def << ['in', type.name, marshaler.lookup_type(type).mapping] + [type.name, marshaler.ruby_to_soap(param)] + end + else + params = [] + end + request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def) + request.set_param(params) + envelope = create_soap_envelope(request) + SOAP::Processor.marshal(envelope) + end + + def decode_response(raw_response) + envelope = SOAP::Processor.unmarshal(raw_response) + unless envelope + raise ProtocolError, "Failed to parse SOAP request message" + end + method_name = envelope.body.request.elename.name + return_value = envelope.body.response + return_value = marshaler.soap_to_ruby(return_value) unless return_value.nil? + [method_name, return_value] + end + + def encode_response(method_name, return_value, return_type, protocol_options={}) + if return_type + return_binding = marshaler.register_type(return_type) + marshaler.annotate_arrays(return_binding, return_value) + end + qname = XSD::QName.new(marshaler.namespace, method_name) + if return_value.nil? + response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) + else + if return_value.is_a?(Exception) + detail = SOAP::Mapping::SOAPException.new(return_value) + response = SOAP::SOAPFault.new( + SOAP::SOAPQName.new('%s:%s' % [SOAP::SOAPNamespaceTag, 'Server']), + SOAP::SOAPString.new(return_value.to_s), + SOAP::SOAPString.new(self.class.name), + marshaler.ruby_to_soap(detail)) + else + if return_type + param_def = [['retval', 'return', marshaler.lookup_type(return_type).mapping]] + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = marshaler.ruby_to_soap(return_value) + else + response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) + end + end + end + envelope = create_soap_envelope(response) + + # FIXME: This is not thread-safe, but StringFactory_ in SOAP4R only + # reads target encoding from the XSD::Charset.encoding variable. + # This is required to ensure $KCODE strings are converted + # correctly to UTF-8 for any values of $KCODE. + previous_encoding = XSD::Charset.encoding + XSD::Charset.encoding = XSDEncoding + response_body = SOAP::Processor.marshal(envelope, :charset => AWSEncoding) + XSD::Charset.encoding = previous_encoding + + Response.new(response_body, "text/xml; charset=#{AWSEncoding}", return_value) + end + + def protocol_client(api, protocol_name, endpoint_uri, options={}) + return nil unless protocol_name == :soap + ActionWebService::Client::Soap.new(api, endpoint_uri, options) + end + + def register_api(api) + api.api_methods.each do |name, method| + method.expects.each{ |type| marshaler.register_type(type) } if method.expects + method.returns.each{ |type| marshaler.register_type(type) } if method.returns + end + end + + private + def has_valid_soap_action?(request) + return nil unless request.method == :post + soap_action = request.env['HTTP_SOAPACTION'] + return nil unless soap_action + soap_action = soap_action.dup + soap_action.gsub!(/^"/, '') + soap_action.gsub!(/"$/, '') + soap_action.strip! + return nil if soap_action.empty? + soap_action + end + + def create_soap_envelope(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + SOAP::SOAPEnvelope.new(header, body) + end + + def parse_charset(content_type) + return AWSEncoding if content_type.nil? + if /^text\/xml(?:\s*;\s*charset=([^"]+|"[^"]+"))$/i =~ content_type + $1 + else + AWSEncoding + end + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb new file mode 100644 index 000000000..187339627 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb @@ -0,0 +1,235 @@ +require 'soap/mapping' + +module ActionWebService + module Protocol + module Soap + # Workaround for SOAP4R return values changing + class Registry < SOAP::Mapping::Registry + if SOAP::Version >= "1.5.4" + def find_mapped_soap_class(obj_class) + return @map.instance_eval { @obj2soap[obj_class][0] } + end + + def find_mapped_obj_class(soap_class) + return @map.instance_eval { @soap2obj[soap_class][0] } + end + end + end + + class SoapMarshaler + attr :namespace + attr :registry + + def initialize(namespace=nil) + @namespace = namespace || 'urn:ActionWebService' + @registry = Registry.new + @type2binding = {} + register_static_factories + end + + def soap_to_ruby(obj) + SOAP::Mapping.soap2obj(obj, @registry) + end + + def ruby_to_soap(obj) + soap = SOAP::Mapping.obj2soap(obj, @registry) + soap.elename = XSD::QName.new if SOAP::Version >= "1.5.5" && soap.elename == XSD::QName::EMPTY + soap + end + + def register_type(type) + return @type2binding[type] if @type2binding.has_key?(type) + + if type.array? + array_mapping = @registry.find_mapped_soap_class(Array) + qname = XSD::QName.new(@namespace, soap_type_name(type.element_type.type_class.name) + 'Array') + element_type_binding = register_type(type.element_type) + @type2binding[type] = SoapBinding.new(self, qname, type, array_mapping, element_type_binding) + elsif (mapping = @registry.find_mapped_soap_class(type.type_class) rescue nil) + qname = mapping[2] ? mapping[2][:type] : nil + qname ||= soap_base_type_name(mapping[0]) + @type2binding[type] = SoapBinding.new(self, qname, type, mapping) + else + qname = XSD::QName.new(@namespace, soap_type_name(type.type_class.name)) + @registry.add(type.type_class, + SOAP::SOAPStruct, + typed_struct_factory(type.type_class), + { :type => qname }) + mapping = @registry.find_mapped_soap_class(type.type_class) + @type2binding[type] = SoapBinding.new(self, qname, type, mapping) + end + + if type.structured? + type.each_member do |m_name, m_type| + register_type(m_type) + end + end + + @type2binding[type] + end + alias :lookup_type :register_type + + def annotate_arrays(binding, value) + if value.nil? + return + elsif binding.type.array? + mark_typed_array(value, binding.element_binding.qname) + if binding.element_binding.type.custom? + value.each do |element| + annotate_arrays(binding.element_binding, element) + end + end + elsif binding.type.structured? + binding.type.each_member do |name, type| + member_binding = register_type(type) + member_value = value.respond_to?('[]') ? value[name] : value.send(name) + annotate_arrays(member_binding, member_value) if type.custom? + end + end + end + + private + def typed_struct_factory(type_class) + if Object.const_defined?('ActiveRecord') + if type_class.ancestors.include?(ActiveRecord::Base) + qname = XSD::QName.new(@namespace, soap_type_name(type_class.name)) + type_class.instance_variable_set('@qname', qname) + return SoapActiveRecordStructFactory.new + end + end + SOAP::Mapping::Registry::TypedStructFactory + end + + def mark_typed_array(array, qname) + (class << array; self; end).class_eval do + define_method(:arytype) do + qname + end + end + end + + def soap_base_type_name(type) + xsd_type = type.ancestors.find{ |c| c.const_defined? 'Type' } + xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type + end + + def soap_type_name(type_name) + type_name.gsub(/::/, '..') + end + + def register_static_factories + @registry.add(ActionWebService::Base64, SOAP::SOAPBase64, SoapBase64Factory.new, nil) + mapping = @registry.find_mapped_soap_class(ActionWebService::Base64) + @type2binding[ActionWebService::Base64] = + SoapBinding.new(self, SOAP::SOAPBase64::Type, ActionWebService::Base64, mapping) + @registry.add(Array, SOAP::SOAPArray, SoapTypedArrayFactory.new, nil) + @registry.add(::BigDecimal, SOAP::SOAPDouble, SOAP::Mapping::Registry::BasetypeFactory, {:derived_class => true}) + end + end + + class SoapBinding + attr :qname + attr :type + attr :mapping + attr :element_binding + + def initialize(marshaler, qname, type, mapping, element_binding=nil) + @marshaler = marshaler + @qname = qname + @type = type + @mapping = mapping + @element_binding = element_binding + end + + def type_name + @type.custom? ? @qname.name : nil + end + + def qualified_type_name(ns=nil) + if @type.custom? + "#{ns ? ns : @qname.namespace}:#{@qname.name}" + else + ns = XSD::NS.new + ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) + ns.assign(SOAP::EncodingNamespace, "soapenc") + xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')} + return ns.name(XSD::AnyTypeName) unless xsd_klass + ns.name(xsd_klass.const_get('Type')) + end + end + + def eql?(other) + @qname == other.qname + end + alias :== :eql? + + def hash + @qname.hash + end + end + + class SoapActiveRecordStructFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.is_a?(ActiveRecord::Base) + return nil + end + soap_obj = soap_class.new(obj.class.instance_variable_get('@qname')) + obj.class.columns.each do |column| + key = column.name.to_s + value = obj.send(key) + soap_obj[key] = SOAP::Mapping._obj2soap(value, map) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + unless node.type == obj_class.instance_variable_get('@qname') + return false + end + obj = obj_class.new + node.each do |key, value| + obj[key] = value.data + end + obj.instance_variable_set('@new_record', false) + return true, obj + end + end + + class SoapTypedArrayFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.respond_to?(:arytype) + return nil + end + soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype) + mark_marshalled_obj(obj, soap_obj) + obj.each do |item| + child = SOAP::Mapping._obj2soap(item, map) + soap_obj.add(child) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + return false + end + end + + class SoapBase64Factory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.is_a?(ActionWebService::Base64) + return nil + end + return soap_class.new(obj) + end + + def soap2obj(obj_class, node, info, map) + unless node.type == SOAP::SOAPBase64::Type + return false + end + return true, obj_class.new(node.string) + end + end + + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb new file mode 100644 index 000000000..dfa4afc67 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb @@ -0,0 +1,122 @@ +require 'xmlrpc/marshal' +require 'action_web_service/client/xmlrpc_client' + +module XMLRPC # :nodoc: + class FaultException # :nodoc: + alias :message :faultString + end + + class Create + def wrong_type(value) + if BigDecimal === value + [true, value.to_f] + else + false + end + end + end +end + +module ActionWebService # :nodoc: + module API # :nodoc: + class Base # :nodoc: + def self.xmlrpc_client(endpoint_uri, options={}) + ActionWebService::Client::XmlRpc.new self, endpoint_uri, options + end + end + end + + module Protocol # :nodoc: + module XmlRpc # :nodoc: + def self.included(base) + base.register_protocol(XmlRpcProtocol) + end + + class XmlRpcProtocol < AbstractProtocol # :nodoc: + def self.create(controller) + XmlRpcProtocol.new + end + + def decode_action_pack_request(action_pack_request) + service_name = action_pack_request.parameters['action'] + decode_request(action_pack_request.raw_post, service_name) + end + + def decode_request(raw_request, service_name) + method_name, params = XMLRPC::Marshal.load_call(raw_request) + Request.new(self, method_name, params, service_name) + rescue + return nil + end + + def encode_request(method_name, params, param_types) + if param_types + params = params.dup + param_types.each_with_index{ |type, i| params[i] = value_to_xmlrpc_wire_format(params[i], type) } + end + XMLRPC::Marshal.dump_call(method_name, *params) + end + + def decode_response(raw_response) + [nil, XMLRPC::Marshal.load_response(raw_response)] + end + + def encode_response(method_name, return_value, return_type, protocol_options={}) + if return_value && return_type + return_value = value_to_xmlrpc_wire_format(return_value, return_type) + end + return_value = false if return_value.nil? + raw_response = XMLRPC::Marshal.dump_response(return_value) + Response.new(raw_response, 'text/xml', return_value) + end + + def encode_multicall_response(responses, protocol_options={}) + result = responses.map do |return_value, return_type| + if return_value && return_type + return_value = value_to_xmlrpc_wire_format(return_value, return_type) + return_value = [return_value] unless return_value.nil? + end + return_value = false if return_value.nil? + return_value + end + raw_response = XMLRPC::Marshal.dump_response(result) + Response.new(raw_response, 'text/xml', result) + end + + def protocol_client(api, protocol_name, endpoint_uri, options={}) + return nil unless protocol_name == :xmlrpc + ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options) + end + + def value_to_xmlrpc_wire_format(value, value_type) + if value_type.array? + value.map{ |val| value_to_xmlrpc_wire_format(val, value_type.element_type) } + else + if value.is_a?(ActionWebService::Struct) + struct = {} + value.class.members.each do |name, type| + member_value = value[name] + next if member_value.nil? + struct[name.to_s] = value_to_xmlrpc_wire_format(member_value, type) + end + struct + elsif value.is_a?(ActiveRecord::Base) + struct = {} + value.attributes.each do |key, member_value| + next if member_value.nil? + struct[key.to_s] = member_value + end + struct + elsif value.is_a?(ActionWebService::Base64) + XMLRPC::Base64.new(value) + elsif value.is_a?(Exception) && !value.is_a?(XMLRPC::FaultException) + XMLRPC::FaultException.new(2, value.message) + else + value + end + end + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb new file mode 100644 index 000000000..f94a7ee91 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb @@ -0,0 +1,283 @@ +require 'benchmark' +require 'pathname' + +module ActionWebService + module Scaffolding # :nodoc: + class ScaffoldingError < ActionWebServiceError # :nodoc: + end + + def self.included(base) + base.extend(ClassMethods) + end + + # Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The + # generated scaffold actions have default views to let you enter the method parameters and view the + # results. + # + # Example: + # + # class ApiController < ActionController + # web_service_scaffold :invoke + # end + # + # This example generates an +invoke+ action in the +ApiController+ that you can navigate to from + # your browser, select the API method, enter its parameters, and perform the invocation. + # + # If you want to customize the default views, create the following views in "app/views": + # + # * action_name/methods.erb + # * action_name/parameters.erb + # * action_name/result.erb + # * action_name/layout.erb + # + # Where action_name is the name of the action you gave to ClassMethods#web_service_scaffold. + # + # You can use the default views in RAILS_DIR/lib/action_web_service/templates/scaffolds as + # a guide. + module ClassMethods + # Generates web service invocation scaffolding for the current controller. The given action name + # can then be used as the entry point for invoking API methods from a web browser. + def web_service_scaffold(action_name) + add_template_helper(Helpers) + module_eval <<-"end_eval", __FILE__, __LINE__ + 1 + def #{action_name} + if request.method == :get + setup_invocation_assigns + render_invocation_scaffold 'methods' + end + end + + def #{action_name}_method_params + if request.method == :get + setup_invocation_assigns + render_invocation_scaffold 'parameters' + end + end + + def #{action_name}_submit + if request.method == :post + setup_invocation_assigns + protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap + case protocol_name + when :soap + @protocol = Protocol::Soap::SoapProtocol.create(self) + when :xmlrpc + @protocol = Protocol::XmlRpc::XmlRpcProtocol.create(self) + end + bm = Benchmark.measure do + @protocol.register_api(@scaffold_service.api) + post_params = params['method_params'] ? params['method_params'].dup : nil + params = [] + @scaffold_method.expects.each_with_index do |spec, i| + params << post_params[i.to_s] + end if @scaffold_method.expects + params = @scaffold_method.cast_expects(params) + method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name) + @method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects) + new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml) + prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name) + self.request = new_request + if @scaffold_container.dispatching_mode != :direct + request.parameters['action'] = @scaffold_service.name + end + dispatch_web_service_request + @method_response_xml = response.body + method_name, obj = @protocol.decode_response(@method_response_xml) + return if handle_invocation_exception(obj) + @method_return_value = @scaffold_method.cast_returns(obj) + end + @method_elapsed = bm.real + add_instance_variables_to_assigns + reset_invocation_response + render_invocation_scaffold 'result' + end + end + + private + def setup_invocation_assigns + @scaffold_class = self.class + @scaffold_action_name = "#{action_name}" + @scaffold_container = WebServiceModel::Container.new(self) + if params['service'] && params['method'] + @scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] } + @scaffold_method = @scaffold_service.api_methods[params['method']] + end + add_instance_variables_to_assigns + end + + def render_invocation_scaffold(action) + customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}" + default_template = scaffold_path(action) + if template_exists?(customized_template) + content = @template.render :file => customized_template + else + content = @template.render :file => default_template + end + @template.instance_variable_set("@content_for_layout", content) + if self.active_layout.nil? + render :file => scaffold_path("layout") + else + render :file => self.active_layout + end + end + + def scaffold_path(template_name) + File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".erb" + end + + def reset_invocation_response + erase_render_results + response.headers = ::ActionController::AbstractResponse::DEFAULT_HEADERS.merge("cookie" => []) + end + + def public_method_name(service_name, method_name) + if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) + service_name + '.' + method_name + else + method_name + end + end + + def prepare_request(new_request, service_name, method_name) + new_request.parameters.update(request.parameters) + request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) } + if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol) + new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}" + end + end + + def handle_invocation_exception(obj) + exception = nil + if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception) + exception = obj.detail.cause + elsif obj.is_a?(XMLRPC::FaultException) + exception = obj + end + return unless exception + reset_invocation_response + rescue_action(exception) + true + end + end_eval + end + end + + module Helpers # :nodoc: + def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false) + if type.array? + return content_tag('em', "Typed array input fields not supported yet (#{type.name})") + end + if type.structured? + return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured + parameters = "" + type.each_member do |member_name, member_type| + label = method_parameter_label(member_name, member_type) + nested_content = method_parameter_input_fields( + method, + member_type, + "#{field_name_base}[#{idx}][#{member_name}]", + idx, + true) + if member_type.custom? + parameters << content_tag('li', label) + parameters << content_tag('ul', nested_content) + else + parameters << content_tag('li', label + ' ' + nested_content) + end + end + content_tag('ul', parameters) + else + # If the data source was structured previously we already have the index set + field_name_base = "#{field_name_base}[#{idx}]" unless was_structured + + case type.type + when :int + text_field_tag "#{field_name_base}" + when :string + text_field_tag "#{field_name_base}" + when :base64 + text_area_tag "#{field_name_base}", nil, :size => "40x5" + when :bool + radio_button_tag("#{field_name_base}", "true") + " True" + + radio_button_tag("#{field_name_base}", "false") + "False" + when :float + text_field_tag "#{field_name_base}" + when :time, :datetime + time = Time.now + i = 0 + %w|year month day hour minute second|.map do |name| + i += 1 + send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true) + end.join + when :date + date = Date.today + i = 0 + %w|year month day|.map do |name| + i += 1 + send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true) + end.join + end + end + end + + def method_parameter_label(name, type) + name.to_s.capitalize + ' (' + type.human_name(false) + ')' + end + + def service_method_list(service) + action = @scaffold_action_name + '_method_params' + methods = service.api_methods_full.map do |desc, name| + content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name)) + end + content_tag("ul", methods.join("\n")) + end + end + + module WebServiceModel # :nodoc: + class Container # :nodoc: + attr :services + attr :dispatching_mode + + def initialize(real_container) + @real_container = real_container + @dispatching_mode = @real_container.class.web_service_dispatching_mode + @services = [] + if @dispatching_mode == :direct + @services << Service.new(@real_container.controller_name, @real_container) + else + @real_container.class.web_services.each do |name, obj| + @services << Service.new(name, @real_container.instance_eval{ web_service_object(name) }) + end + end + end + end + + class Service # :nodoc: + attr :name + attr :object + attr :api + attr :api_methods + attr :api_methods_full + + def initialize(name, real_service) + @name = name.to_s + @object = real_service + @api = @object.class.web_service_api + if @api.nil? + raise ScaffoldingError, "No web service API attached to #{object.class}" + end + @api_methods = {} + @api_methods_full = [] + @api.api_methods.each do |name, method| + @api_methods[method.public_name.to_s] = method + @api_methods_full << [method.to_s, method.public_name.to_s] + end + end + + def to_s + self.name.camelize + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/struct.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/struct.rb new file mode 100644 index 000000000..00eafc169 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/struct.rb @@ -0,0 +1,64 @@ +module ActionWebService + # To send structured types across the wire, derive from ActionWebService::Struct, + # and use +member+ to declare structure members. + # + # ActionWebService::Struct should be used in method signatures when you want to accept or return + # structured types that have no Active Record model class representations, or you don't + # want to expose your entire Active Record model to remote callers. + # + # === Example + # + # class Person < ActionWebService::Struct + # member :id, :int + # member :firstnames, [:string] + # member :lastname, :string + # member :email, :string + # end + # person = Person.new(:id => 5, :firstname => 'john', :lastname => 'doe') + # + # Active Record model classes are already implicitly supported in method + # signatures. + class Struct + # If a Hash is given as argument to an ActionWebService::Struct constructor, + # it can contain initial values for the structure member. + def initialize(values={}) + if values.is_a?(Hash) + values.map{|k,v| __send__('%s=' % k.to_s, v)} + end + end + + # The member with the given name + def [](name) + send(name.to_s) + end + + # Iterates through each member + def each_pair(&block) + self.class.members.each do |name, type| + yield name, self.__send__(name) + end + end + + class << self + # Creates a structure member with the specified +name+ and +type+. Generates + # accessor methods for reading and writing the member value. + def member(name, type) + name = name.to_sym + type = ActionWebService::SignatureTypes.canonical_signature_entry({ name => type }, 0) + write_inheritable_hash("struct_members", name => type) + class_eval <<-END + def #{name}; @#{name}; end + def #{name}=(value); @#{name} = value; end + END + end + + def members # :nodoc: + read_inheritable_attribute("struct_members") || {} + end + + def member_type(name) # :nodoc: + members[name.to_sym] + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb new file mode 100644 index 000000000..4d1c2ed47 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb @@ -0,0 +1,26 @@ +class Class # :nodoc: + def class_inheritable_option(sym, default_value=nil) + write_inheritable_attribute sym, default_value + class_eval <<-EOS + def self.#{sym}(value=nil) + if !value.nil? + write_inheritable_attribute(:#{sym}, value) + else + read_inheritable_attribute(:#{sym}) + end + end + + def self.#{sym}=(value) + write_inheritable_attribute(:#{sym}, value) + end + + def #{sym} + self.class.#{sym} + end + + def #{sym}=(value) + self.class.#{sym} = value + end + EOS + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb new file mode 100644 index 000000000..66c86bf6d --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb @@ -0,0 +1,226 @@ +module ActionWebService # :nodoc: + # Action Web Service supports the following base types in a signature: + # + # [:int] Represents an integer value, will be cast to an integer using Integer(value) + # [:string] Represents a string value, will be cast to an string using the to_s method on an object + # [:base64] Represents a Base 64 value, will contain the binary bytes of a Base 64 value sent by the caller + # [:bool] Represents a boolean value, whatever is passed will be cast to boolean (true, '1', 'true', 'y', 'yes' are taken to represent true; false, '0', 'false', 'n', 'no' and nil represent false) + # [:float] Represents a floating point value, will be cast to a float using Float(value) + # [:time] Represents a timestamp, will be cast to a Time object + # [:datetime] Represents a timestamp, will be cast to a DateTime object + # [:date] Represents a date, will be cast to a Date object + # + # For structured types, you'll need to pass in the Class objects of + # ActionWebService::Struct and ActiveRecord::Base derivatives. + module SignatureTypes + def canonical_signature(signature) # :nodoc: + return nil if signature.nil? + unless signature.is_a?(Array) + raise(ActionWebServiceError, "Expected signature to be an Array") + end + i = -1 + signature.map{ |spec| canonical_signature_entry(spec, i += 1) } + end + + def canonical_signature_entry(spec, i) # :nodoc: + orig_spec = spec + name = "param#{i}" + if spec.is_a?(Hash) + name, spec = spec.keys.first, spec.values.first + end + type = spec + if spec.is_a?(Array) + ArrayType.new(orig_spec, canonical_signature_entry(spec[0], 0), name) + else + type = canonical_type(type) + if type.is_a?(Symbol) + BaseType.new(orig_spec, type, name) + else + StructuredType.new(orig_spec, type, name) + end + end + end + + def canonical_type(type) # :nodoc: + type_name = symbol_name(type) || class_to_type_name(type) + type = type_name || type + return canonical_type_name(type) if type.is_a?(Symbol) + type + end + + def canonical_type_name(name) # :nodoc: + name = name.to_sym + case name + when :int, :integer, :fixnum, :bignum + :int + when :string, :text + :string + when :base64, :binary + :base64 + when :bool, :boolean + :bool + when :float, :double + :float + when :decimal + :decimal + when :time, :timestamp + :time + when :datetime + :datetime + when :date + :date + else + raise(TypeError, "#{name} is not a valid base type") + end + end + + def canonical_type_class(type) # :nodoc: + type = canonical_type(type) + type.is_a?(Symbol) ? type_name_to_class(type) : type + end + + def symbol_name(name) # :nodoc: + return name.to_sym if name.is_a?(Symbol) || name.is_a?(String) + nil + end + + def class_to_type_name(klass) # :nodoc: + klass = klass.class unless klass.is_a?(Class) + if derived_from?(Integer, klass) || derived_from?(Fixnum, klass) || derived_from?(Bignum, klass) + :int + elsif klass == String + :string + elsif klass == Base64 + :base64 + elsif klass == TrueClass || klass == FalseClass + :bool + elsif derived_from?(Float, klass) || derived_from?(Precision, klass) || derived_from?(Numeric, klass) + :float + elsif klass == Time + :time + elsif klass == DateTime + :datetime + elsif klass == Date + :date + else + nil + end + end + + def type_name_to_class(name) # :nodoc: + case canonical_type_name(name) + when :int + Integer + when :string + String + when :base64 + Base64 + when :bool + TrueClass + when :float + Float + when :decimal + BigDecimal + when :time + Time + when :date + Date + when :datetime + DateTime + else + nil + end + end + + def derived_from?(ancestor, child) # :nodoc: + child.ancestors.include?(ancestor) + end + + module_function :type_name_to_class + module_function :class_to_type_name + module_function :symbol_name + module_function :canonical_type_class + module_function :canonical_type_name + module_function :canonical_type + module_function :canonical_signature_entry + module_function :canonical_signature + module_function :derived_from? + end + + class BaseType # :nodoc: + include SignatureTypes + + attr :spec + attr :type + attr :type_class + attr :name + + def initialize(spec, type, name) + @spec = spec + @type = canonical_type(type) + @type_class = canonical_type_class(@type) + @name = name + end + + def custom? + false + end + + def array? + false + end + + def structured? + false + end + + def human_name(show_name=true) + type_type = array? ? element_type.type.to_s : self.type.to_s + str = array? ? (type_type + '[]') : type_type + show_name ? (str + " " + name.to_s) : str + end + end + + class ArrayType < BaseType # :nodoc: + attr :element_type + + def initialize(spec, element_type, name) + super(spec, Array, name) + @element_type = element_type + end + + def custom? + true + end + + def array? + true + end + end + + class StructuredType < BaseType # :nodoc: + def each_member + if @type_class.respond_to?(:members) + @type_class.members.each do |name, type| + yield name, type + end + elsif @type_class.respond_to?(:columns) + i = -1 + @type_class.columns.each do |column| + yield column.name, canonical_signature_entry(column.type, i += 1) + end + end + end + + def custom? + true + end + + def structured? + true + end + end + + class Base64 < String # :nodoc: + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb new file mode 100644 index 000000000..167613f68 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb @@ -0,0 +1,65 @@ + + + <%= @scaffold_class.wsdl_service_name %> Web Service + + + + +<%= @content_for_layout %> + + + diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb new file mode 100644 index 000000000..60dfe23f0 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb @@ -0,0 +1,6 @@ +<% @scaffold_container.services.each do |service| %> + +

    API Methods for <%= service %>

    + <%= service_method_list(service) %> + +<% end %> diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.erb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.erb new file mode 100644 index 000000000..767284e0d --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.erb @@ -0,0 +1,29 @@ +

    Method Invocation Details for <%= @scaffold_service %>#<%= @scaffold_method.public_name %>

    + +<% form_tag(:action => @scaffold_action_name + '_submit') do -%> +<%= hidden_field_tag "service", @scaffold_service.name %> +<%= hidden_field_tag "method", @scaffold_method.public_name %> + +

    +
    +<%= select_tag 'protocol', options_for_select([['SOAP', 'soap'], ['XML-RPC', 'xmlrpc']], params['protocol']) %> +

    + +<% if @scaffold_method.expects %> + +Method Parameters:
    +<% @scaffold_method.expects.each_with_index do |type, i| %> +

    +
    + <%= method_parameter_input_fields(@scaffold_method, type, "method_params", i) %> +

    +<% end %> + +<% end %> + +<%= submit_tag "Invoke" %> +<% end -%> + +

    +<%= link_to "Back", :action => @scaffold_action_name %> +

    diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.erb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.erb new file mode 100644 index 000000000..5317688fc --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.erb @@ -0,0 +1,30 @@ +

    Method Invocation Result for <%= @scaffold_service %>#<%= @scaffold_method.public_name %>

    + +

    +Invocation took <%= '%f' % @method_elapsed %> seconds +

    + +

    +Return Value:
    +

    +<%= h @method_return_value.inspect %>
    +
    +

    + +

    +Request XML:
    +

    +<%= h @method_request_xml %>
    +
    +

    + +

    +Response XML:
    +

    +<%= h @method_response_xml %>
    +
    +

    + +

    +<%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %> +

    diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml b/groups/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb new file mode 100644 index 000000000..7e714c941 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb @@ -0,0 +1,110 @@ +require 'test/unit' + +module Test # :nodoc: + module Unit # :nodoc: + class TestCase # :nodoc: + private + # invoke the specified API method + def invoke_direct(method_name, *args) + prepare_request('api', 'api', method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + alias_method :invoke, :invoke_direct + + # invoke the specified API method on the specified service + def invoke_delegated(service_name, method_name, *args) + prepare_request(service_name.to_s, service_name, method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + + # invoke the specified layered API method on the correct service + def invoke_layered(service_name, method_name, *args) + prepare_request('api', service_name, method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + + # ---------------------- internal --------------------------- + + def prepare_request(action, service_name, api_method_name, *args) + @request.recycle! + @request.request_parameters['action'] = action + @request.env['REQUEST_METHOD'] = 'POST' + @request.env['HTTP_CONTENT_TYPE'] = 'text/xml' + @request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args) + case protocol + when ActionWebService::Protocol::Soap::SoapProtocol + soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}" + @request.env['HTTP_SOAPACTION'] = soap_action + when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + @request.env.delete('HTTP_SOAPACTION') + end + end + + def encode_rpc_call(service_name, api_method_name, *args) + case @controller.web_service_dispatching_mode + when :direct + api = @controller.class.web_service_api + when :delegated, :layered + api = @controller.web_service_object(service_name.to_sym).class.web_service_api + end + protocol.register_api(api) + method = api.api_methods[api_method_name.to_sym] + raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" if method && method.expects && args.length != method.expects.length + protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects) + end + + def decode_rpc_response + public_method_name, return_value = protocol.decode_response(@response.body) + exception = is_exception?(return_value) + raise exception if exception + return_value + end + + def public_method_name(service_name, api_method_name) + public_name = service_api(service_name).public_api_method_name(api_method_name) + if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) + '%s.%s' % [service_name.to_s, public_name] + else + public_name + end + end + + def service_api(service_name) + case @controller.web_service_dispatching_mode + when :direct + @controller.class.web_service_api + when :delegated, :layered + @controller.web_service_object(service_name.to_sym).class.web_service_api + end + end + + def protocol + if @protocol.nil? + @protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) + else + case @protocol + when :xmlrpc + @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller) + when :soap + @protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) + else + @protocol + end + end + end + + def is_exception?(obj) + case protocol + when :soap, ActionWebService::Protocol::Soap::SoapProtocol + (obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \ + obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil + when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + obj.is_a?(XMLRPC::FaultException) ? obj : nil + end + end + end + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/action_web_service/version.rb b/groups/vendor/plugins/actionwebservice/lib/action_web_service/version.rb new file mode 100644 index 000000000..a1b3d5929 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/action_web_service/version.rb @@ -0,0 +1,9 @@ +module ActionWebService + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 2 + TINY = 5 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/groups/vendor/plugins/actionwebservice/lib/actionwebservice.rb b/groups/vendor/plugins/actionwebservice/lib/actionwebservice.rb new file mode 100644 index 000000000..25e3aa8e8 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/lib/actionwebservice.rb @@ -0,0 +1 @@ +require 'action_web_service' diff --git a/groups/vendor/plugins/actionwebservice/setup.rb b/groups/vendor/plugins/actionwebservice/setup.rb new file mode 100644 index 000000000..aeef0d106 --- /dev/null +++ b/groups/vendor/plugins/actionwebservice/setup.rb @@ -0,0 +1,1379 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 Minero Aoki +# +# 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. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. + +# + +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 + +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 SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigItem + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default.dup.freeze + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value + @value + end + + def eval(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 < ConfigItem + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ val + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end +end + +class PathItem < ConfigItem + 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 < ConfigItem + def config_type + 'program' + end +end + +class SelectItem < ConfigItem + def initialize(name, template, default, desc) + super + @ok = template.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 PackageSelectionItem < ConfigItem + 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 ConfigTable_class + + def initialize(items) + @items = items + @table = {} + items.each do |i| + @table[i.name] = i + end + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + end + + include Enumerable + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or raise ArgumentError, "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 new + dup() + end + + def savefile + '.config' + end + + def load + begin + t = dup() + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + t[k] = v.strip + end + t + rescue Errno::ENOENT + setup_rb_error $!.message + "#{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 + end + } + end + + def [](key) + lookup(key).eval(self) + end + + def []=(key, val) + lookup(key).set val + end + +end + +c = ::Config::CONFIG + +rubypath = c['bindir'] + '/' + c['ruby_install_name'] + +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 + _stdruby = c['rubylibdir'] + _siteruby = c['sitedir'] + _siterubyver = c['sitelibdir'] + _siterubyverarch = c['sitearchdir'] +elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = c['sitedir'] + _siterubyver = "$siteruby/#{version}" + _siterubyverarch = "$siterubyver/#{c['arch']}" +else + # V < 1.4.4 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + _siterubyver = _siteruby + _siterubyverarch = "$siterubyver/#{c['arch']}" +end +libdir = '-* dummy libdir *-' +stdruby = '-* dummy rubylibdir *-' +siteruby = '-* dummy site_ruby *-' +siterubyver = '-* dummy site_ruby version *-' +parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')\ + .sub(/\A#{Regexp.quote(libdir)}/, '$libdir')\ + .sub(/\A#{Regexp.quote(stdruby)}/, '$stdruby')\ + .sub(/\A#{Regexp.quote(siteruby)}/, '$siteruby')\ + .sub(/\A#{Regexp.quote(siterubyver)}/, '$siterubyver') +} +libdir = parameterize.call(c['libdir']) +stdruby = parameterize.call(_stdruby) +siteruby = parameterize.call(_siteruby) +siterubyver = parameterize.call(_siterubyver) +siterubyverarch = parameterize.call(_siterubyverarch) + +if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] +else + makeprog = 'make' +end + +common_conf = [ + 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', 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 man pages'), + PathItem.new('stdruby', 'path', stdruby, + 'the directory for standard ruby libraries'), + 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') +] +class ConfigTable_class # open again + ALIASES = { + 'std-ruby' => 'stdruby', + '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' + } +end +multipackage_conf = [ + 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') +] +if multipackage_install? + ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf) +else + ConfigTable = ConfigTable_class.new(common_conf) +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.map {|i| i.name } + end + + def config?(name) + ConfigTable.key?(name) + end + + def bool_config?(name) + ConfigTable.lookup(name).config_type == 'bool' + end + + def path_config?(name) + ConfigTable.lookup(name).config_type == 'path' + end + + def value_config?(name) + case ConfigTable.lookup(name).config_type + when 'bool', 'path' + true + else + false + end + end + + def add_config(item) + ConfigTable.add item + end + + def add_bool_config(name, default, desc) + ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + ConfigTable.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + ConfigTable.lookup(name).default = default + end + + def remove_config(name) + ConfigTable.remove(name) + end + +end + + +# +# File Operations +# + +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 case + 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(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(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 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(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('rubyprog') + ' ' + str + end + + def make(task = '') + command config('makeprog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + + +# +# Main Installer +# + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + setup_rb_error "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + 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.3.1' + Copyright = 'Copyright (c) 2000-2004 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' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable.savefile) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @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 + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + 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' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--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 parsearg_no_options + unless ARGV.empty? + setup_rb_error "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or setup_rb_error "config: unknown option #{i}" + name, value = *m.to_a[1,2] + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['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} " + out.puts " ruby #{File.basename $0} [] []" + + 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, '-h,--help', 'print this message' + out.printf fmt, '-v,--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:' + ConfigTable.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', '$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_show + ConfigTable.each do |i| + printf "%-20s %s\n", i.name, i.value + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@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 'Included packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_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 + + # + # 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_clean + rm_f ConfigTable.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable.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 @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + first = r.gets + return unless File.basename(config('rubypath')) == 'ruby' + return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby' + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + File.open(tmpfile, 'wb') {|w| + w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath')) + w.write r.read + } + move_file tmpfile, File.basename(path) + } + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.rb\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + 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 mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + Dir.open(dir) {|d| + ents = d.select {|fname| /\.#{::Config::CONFIG['DLEXT']}\z/ =~ fname } + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + return ents + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $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)], '') + all_dirs_in(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 + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/groups/vendor/plugins/acts_as_event/init.rb b/groups/vendor/plugins/acts_as_event/init.rb new file mode 100644 index 000000000..91051510a --- /dev/null +++ b/groups/vendor/plugins/acts_as_event/init.rb @@ -0,0 +1,2 @@ +require File.dirname(__FILE__) + '/lib/acts_as_event' +ActiveRecord::Base.send(:include, Redmine::Acts::Event) diff --git a/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb b/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb new file mode 100644 index 000000000..d7f437a5e --- /dev/null +++ b/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb @@ -0,0 +1,75 @@ +# 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) + options[:datetime] ||= :created_on + options[:title] ||= :title + options[:description] ||= :description + options[:author] ||= :author + options[:url] ||= {:controller => 'welcome'} + options[:type] ||= self.name.underscore.dasherize + cattr_accessor :event_options + self.event_options = 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 diff --git a/groups/vendor/plugins/acts_as_list/README b/groups/vendor/plugins/acts_as_list/README new file mode 100644 index 000000000..36ae3188e --- /dev/null +++ b/groups/vendor/plugins/acts_as_list/README @@ -0,0 +1,23 @@ +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 diff --git a/groups/vendor/plugins/acts_as_list/init.rb b/groups/vendor/plugins/acts_as_list/init.rb new file mode 100644 index 000000000..eb87e8790 --- /dev/null +++ b/groups/vendor/plugins/acts_as_list/init.rb @@ -0,0 +1,3 @@ +$:.unshift "#{File.dirname(__FILE__)}/lib" +require 'active_record/acts/list' +ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List } diff --git a/groups/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb b/groups/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb new file mode 100644 index 000000000..00d86928d --- /dev/null +++ b/groups/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb @@ -0,0 +1,256 @@ +module ActiveRecord + module Acts #:nodoc: + module List #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list. + # The class that has this specified needs to have a +position+ column defined as an integer on + # the mapped database table. + # + # Todo list example: + # + # class TodoList < ActiveRecord::Base + # has_many :todo_items, :order => "position" + # end + # + # class TodoItem < ActiveRecord::Base + # belongs_to :todo_list + # acts_as_list :scope => :todo_list + # end + # + # todo_list.first.move_to_bottom + # todo_list.last.move_higher + module ClassMethods + # Configuration options are: + # + # * +column+ - specifies the column name to use for keeping the position integer (default: +position+) + # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id + # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible + # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. + # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' + def acts_as_list(options = {}) + configuration = { :column => "position", :scope => "1 = 1" } + configuration.update(options) if options.is_a?(Hash) + + configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ + + if configuration[:scope].is_a?(Symbol) + scope_condition_method = %( + def scope_condition + if #{configuration[:scope].to_s}.nil? + "#{configuration[:scope].to_s} IS NULL" + else + "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" + end + end + ) + else + scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" + end + + class_eval <<-EOV + include ActiveRecord::Acts::List::InstanceMethods + + def acts_as_list_class + ::#{self.name} + end + + def position_column + '#{configuration[:column]}' + end + + #{scope_condition_method} + + before_destroy :remove_from_list + before_create :add_to_list_bottom + EOV + end + end + + # All the methods available to a record that has had acts_as_list specified. Each method works + # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter + # lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is + # the first in the list of all chapters. + module InstanceMethods + # Insert the item at the given position (defaults to the top position of 1). + def insert_at(position = 1) + insert_at_position(position) + end + + # Swap positions with the next lower item, if one exists. + def move_lower + return unless lower_item + + acts_as_list_class.transaction do + lower_item.decrement_position + increment_position + end + end + + # Swap positions with the next higher item, if one exists. + def move_higher + return unless higher_item + + acts_as_list_class.transaction do + higher_item.increment_position + decrement_position + end + end + + # Move to the bottom of the list. If the item is already in the list, the items below it have their + # position adjusted accordingly. + def move_to_bottom + return unless in_list? + acts_as_list_class.transaction do + decrement_positions_on_lower_items + assume_bottom_position + end + end + + # Move to the top of the list. If the item is already in the list, the items above it have their + # position adjusted accordingly. + def move_to_top + return unless in_list? + acts_as_list_class.transaction do + increment_positions_on_higher_items + assume_top_position + end + end + + # 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 (position_column) of all items in the list. + def increment_positions_on_all_items + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" + ) + end + + def insert_at_position(position) + remove_from_list + increment_positions_on_lower_items(position) + self.update_attribute(position_column, position) + end + end + end + end +end diff --git a/groups/vendor/plugins/acts_as_list/test/list_test.rb b/groups/vendor/plugins/acts_as_list/test/list_test.rb new file mode 100644 index 000000000..e89cb8e12 --- /dev/null +++ b/groups/vendor/plugins/acts_as_list/test/list_test.rb @@ -0,0 +1,332 @@ +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 diff --git a/groups/vendor/plugins/acts_as_searchable/init.rb b/groups/vendor/plugins/acts_as_searchable/init.rb new file mode 100644 index 000000000..063721756 --- /dev/null +++ b/groups/vendor/plugins/acts_as_searchable/init.rb @@ -0,0 +1,2 @@ +require File.dirname(__FILE__) + '/lib/acts_as_searchable' +ActiveRecord::Base.send(:include, Redmine::Acts::Searchable) diff --git a/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb new file mode 100644 index 000000000..dff76b913 --- /dev/null +++ b/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -0,0 +1,110 @@ +# 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 + 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 + + if searchable_options[:project_key] + elsif column_names.include?('project_id') + searchable_options[:project_key] = "#{table_name}.project_id" + else + raise 'No project key defined.' + end + + if searchable_options[:date_column] + elsif column_names.include?('created_on') + searchable_options[:date_column] = "#{table_name}.created_on" + else + raise 'No date column defined defined.' + end + + # 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 + def search(tokens, project, options={}) + tokens = [] << tokens unless tokens.is_a?(Array) + find_options = {:include => searchable_options[:include]} + find_options[:limit] = options[:limit] if options[:limit] + find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC') + columns = searchable_options[:columns] + columns.slice!(1..-1) 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 ') + + if options[:offset] + sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" + end + find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] + + results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do + find(:all, find_options) + end + if searchable_options[:with] && !options[:titles_only] + searchable_options[:with].each do |model, assoc| + results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc} + end + results.uniq! + end + results + end + end + end + end + end +end diff --git a/groups/vendor/plugins/acts_as_tree/README b/groups/vendor/plugins/acts_as_tree/README new file mode 100644 index 000000000..a6cc6a904 --- /dev/null +++ b/groups/vendor/plugins/acts_as_tree/README @@ -0,0 +1,26 @@ +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 diff --git a/groups/vendor/plugins/acts_as_tree/Rakefile b/groups/vendor/plugins/acts_as_tree/Rakefile new file mode 100644 index 000000000..da091d9dd --- /dev/null +++ b/groups/vendor/plugins/acts_as_tree/Rakefile @@ -0,0 +1,22 @@ +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 diff --git a/groups/vendor/plugins/acts_as_tree/init.rb b/groups/vendor/plugins/acts_as_tree/init.rb new file mode 100644 index 000000000..0901ddb4a --- /dev/null +++ b/groups/vendor/plugins/acts_as_tree/init.rb @@ -0,0 +1 @@ +ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree diff --git a/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb b/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb new file mode 100644 index 000000000..1f00e90a9 --- /dev/null +++ b/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module Acts + module Tree + def self.included(base) + base.extend(ClassMethods) + end + + # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children + # association. This requires that you have a foreign key column, which by default is called +parent_id+. + # + # class Category < ActiveRecord::Base + # acts_as_tree :order => "name" + # end + # + # Example: + # root + # \_ child1 + # \_ subchild1 + # \_ subchild2 + # + # root = Category.create("name" => "root") + # child1 = root.children.create("name" => "child1") + # subchild1 = child1.children.create("name" => "subchild1") + # + # root.parent # => nil + # child1.parent # => root + # root.children # => [child1] + # root.children.first.children.first # => subchild1 + # + # In addition to the parent and children associations, the following instance methods are added to the class + # after calling acts_as_tree: + # * siblings - Returns all the children of the parent, excluding the current node ([subchild2] when called on subchild1) + # * self_and_siblings - Returns all the children of the parent, including the current node ([subchild1, subchild2] when called on subchild1) + # * ancestors - Returns all the ancestors of the current node ([child1, root] when called on subchild2) + # * root - Returns the root of the current node (root when called on subchild2) + module ClassMethods + # Configuration options are: + # + # * foreign_key - specifies the column name to use for tracking of the tree (default: +parent_id+) + # * order - makes it possible to sort the children according to this SQL snippet. + # * counter_cache - keeps a count in a +children_count+ column if set to +true+ (default: +false+). + def acts_as_tree(options = {}) + configuration = { :foreign_key => "parent_id", :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 => :destroy + + 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 the root node of the tree. + def root + node = self + node = node.parent while node.parent + node + end + + # Returns all siblings of the current node. + # + # subchild1.siblings # => [subchild2] + def siblings + self_and_siblings - [self] + end + + # Returns all siblings and a reference to the current node. + # + # subchild1.self_and_siblings # => [subchild1, subchild2] + def self_and_siblings + parent ? parent.children : self.class.roots + end + end + end + end +end diff --git a/groups/vendor/plugins/acts_as_tree/test/abstract_unit.rb b/groups/vendor/plugins/acts_as_tree/test/abstract_unit.rb new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb b/groups/vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb new file mode 100644 index 000000000..018c58e1f --- /dev/null +++ b/groups/vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb @@ -0,0 +1,219 @@ +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 diff --git a/groups/vendor/plugins/acts_as_tree/test/database.yml b/groups/vendor/plugins/acts_as_tree/test/database.yml new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/acts_as_tree/test/fixtures/mixin.rb b/groups/vendor/plugins/acts_as_tree/test/fixtures/mixin.rb new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/acts_as_tree/test/fixtures/mixins.yml b/groups/vendor/plugins/acts_as_tree/test/fixtures/mixins.yml new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/acts_as_tree/test/schema.rb b/groups/vendor/plugins/acts_as_tree/test/schema.rb new file mode 100644 index 000000000..e69de29bb diff --git a/groups/vendor/plugins/acts_as_versioned/CHANGELOG b/groups/vendor/plugins/acts_as_versioned/CHANGELOG new file mode 100644 index 000000000..a5d339cc7 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/CHANGELOG @@ -0,0 +1,74 @@ +*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 diff --git a/groups/vendor/plugins/acts_as_versioned/MIT-LICENSE b/groups/vendor/plugins/acts_as_versioned/MIT-LICENSE new file mode 100644 index 000000000..5851fdae1 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/MIT-LICENSE @@ -0,0 +1,20 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/README b/groups/vendor/plugins/acts_as_versioned/README new file mode 100644 index 000000000..8961f0522 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/README @@ -0,0 +1,28 @@ += 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 in the wild. \ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS b/groups/vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS new file mode 100644 index 000000000..a6e55b841 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS @@ -0,0 +1,41 @@ +== 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//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 diff --git a/groups/vendor/plugins/acts_as_versioned/Rakefile b/groups/vendor/plugins/acts_as_versioned/Rakefile new file mode 100644 index 000000000..3ae69e961 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/Rakefile @@ -0,0 +1,182 @@ +require 'rubygems' + +Gem::manage_gems + +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/testtask' +require 'rake/contrib/rubyforgepublisher' + +PKG_NAME = 'acts_as_versioned' +PKG_VERSION = '0.3.1' +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +PROD_HOST = "technoweenie@bidwell.textdrive.com" +RUBY_FORGE_PROJECT = 'ar-versioned' +RUBY_FORGE_USER = 'technoweenie' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the calculations plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the calculations plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models" + rdoc.options << '--line-numbers --inline-source' + rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS') + rdoc.rdoc_files.include('lib/**/*.rb') +end + +spec = Gem::Specification.new do |s| + s.name = PKG_NAME + s.version = PKG_VERSION + s.platform = Gem::Platform::RUBY + s.summary = "Simple versioning with active record models" + s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS) + s.files.delete "acts_as_versioned_plugin.sqlite.db" + s.files.delete "acts_as_versioned_plugin.sqlite3.db" + s.files.delete "test/debug.log" + s.require_path = 'lib' + s.autorequire = 'acts_as_versioned' + s.has_rdoc = true + s.test_files = Dir['test/**/*_test.rb'] + s.add_dependency 'activerecord', '>= 1.10.1' + s.add_dependency 'activesupport', '>= 1.1.1' + s.author = "Rick Olson" + s.email = "technoweenie@gmail.com" + s.homepage = "http://techno-weenie.net" +end + +Rake::GemPackageTask.new(spec) do |pkg| + pkg.need_tar = true +end + +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload +end + +desc 'Publish the gem and API docs' +task :publish => [:pdoc, :rubyforge_upload] + +desc "Publish the release files to RubyForge." +task :rubyforge_upload => :package do + files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" } + + if RUBY_FORGE_PROJECT then + require 'net/http' + require 'open-uri' + + project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/" + project_data = open(project_uri) { |data| data.read } + group_id = project_data[/[?&]group_id=(\d+)/, 1] + raise "Couldn't get group id" unless group_id + + # This echos password to shell which is a bit sucky + if ENV["RUBY_FORGE_PASSWORD"] + password = ENV["RUBY_FORGE_PASSWORD"] + else + print "#{RUBY_FORGE_USER}@rubyforge.org's password: " + password = STDIN.gets.chomp + end + + login_response = Net::HTTP.start("rubyforge.org", 80) do |http| + data = [ + "login=1", + "form_loginname=#{RUBY_FORGE_USER}", + "form_pw=#{password}" + ].join("&") + http.post("/account/login.php", data) + end + + cookie = login_response["set-cookie"] + raise "Login failed" unless cookie + headers = { "Cookie" => cookie } + + release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}" + release_data = open(release_uri, headers) { |data| data.read } + package_id = release_data[/[?&]package_id=(\d+)/, 1] + raise "Couldn't get package id" unless package_id + + first_file = true + release_id = "" + + files.each do |filename| + basename = File.basename(filename) + file_ext = File.extname(filename) + file_data = File.open(filename, "rb") { |file| file.read } + + puts "Releasing #{basename}..." + + release_response = Net::HTTP.start("rubyforge.org", 80) do |http| + release_date = Time.now.strftime("%Y-%m-%d %H:%M") + type_map = { + ".zip" => "3000", + ".tgz" => "3110", + ".gz" => "3110", + ".gem" => "1400" + }; type_map.default = "9999" + type = type_map[file_ext] + boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor" + + query_hash = if first_file then + { + "group_id" => group_id, + "package_id" => package_id, + "release_name" => PKG_FILE_NAME, + "release_date" => release_date, + "type_id" => type, + "processor_id" => "8000", # Any + "release_notes" => "", + "release_changes" => "", + "preformatted" => "1", + "submit" => "1" + } + else + { + "group_id" => group_id, + "release_id" => release_id, + "package_id" => package_id, + "step2" => "1", + "type_id" => type, + "processor_id" => "8000", # Any + "submit" => "Add This File" + } + end + + query = "?" + query_hash.map do |(name, value)| + [name, URI.encode(value)].join("=") + end.join("&") + + data = [ + "--" + boundary, + "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"", + "Content-Type: application/octet-stream", + "Content-Transfer-Encoding: binary", + "", file_data, "" + ].join("\x0D\x0A") + + release_headers = headers.merge( + "Content-Type" => "multipart/form-data; boundary=#{boundary}" + ) + + target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php" + http.post(target + query, data, release_headers) + end + + if first_file then + release_id = release_response.body[/release_id=(\d+)/, 1] + raise("Couldn't get release id") unless release_id + end + + first_file = false + end + end +end \ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/init.rb b/groups/vendor/plugins/acts_as_versioned/init.rb new file mode 100644 index 000000000..5937bbc7c --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/init.rb @@ -0,0 +1 @@ +require 'acts_as_versioned' \ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb b/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb new file mode 100644 index 000000000..5e6f6e636 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb @@ -0,0 +1,511 @@ +# 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 optimisic 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' + # + # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options + module Versioned + CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes] + def self.included(base) # :nodoc: + base.extend ClassMethods + end + + module ClassMethods + # == Configuration options + # + # * class_name - versioned model class name (default: PageVersion in the above example) + # * table_name - versioned model table name (default: page_versions in the above example) + # * foreign_key - foreign key used to relate the versioned model to the original model (default: page_id in the above example) + # * inheritance_column - name of the column to save the model's inheritance_column value for STI. (default: versioned_type) + # * version_column - name of the column in the model that keeps the version number (default: version) + # * sequence_name - name of the custom sequence to be used by the versioned model. + # * limit - number of revisions to keep, defaults to unlimited + # * if - 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 :if option + # !expired? + # end + # end + # + # * if_changed - 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 + # + # * extend - 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_changed_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, :changed_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}", + :order => 'version', + :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 + before_save :set_new_version + after_create :save_version_on_create + after_update :save_version + after_save :clear_old_versions + after_save :clear_changed_attributes + + unless options[:if_changed].nil? + self.track_changed_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 + end + + 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 + + # 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 acts_as_versioned. + # 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 + + # Finds a specific version of this model. + def find_version(version) + return version if version.is_a?(self.class.versioned_class) + return nil if version.is_a?(ActiveRecord::Base) + find_versions(:conditions => ['version = ?', version], :limit => 1).first + end + + # Finds versions of this model. Takes an options hash like find + def find_versions(options = {}) + versions.find(:all, options) + 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 = find_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_changed_attributes || (changed_attributes && changed_attributes.length > 0)) : + (changed_attributes && changed_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.attributes[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 version_condition_met? and changed?. + 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_changed_attributes + self.changed_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.changed_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 + + private + CALLBACKS.each do |attr_name| + alias_method "orig_#{attr_name}".to_sym, attr_name + end + + module ClassMethods + # Finds a specific version of a specific row of this model + def find_version(id, version) + find_versions(id, + :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version], + :limit => 1).first + end + + # Finds versions of a specific model. Takes an options hash like find + 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 + 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 + 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 attr_name, :empty_callback + end + end + result = block.call + class_eval do + CALLBACKS.each do |attr_name| + alias_method attr_name, "orig_#{attr_name}".to_sym + end + end + result + 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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb b/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb new file mode 100644 index 000000000..1740db8dc --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb @@ -0,0 +1,40 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') + +require 'test/unit' +require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb')) +require 'active_record/fixtures' + +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") +ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite']) + +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/" +$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path) + +class Test::Unit::TestCase #:nodoc: + def create_fixtures(*table_names) + if block_given? + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } + else + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) + end + end + + # 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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/database.yml b/groups/vendor/plugins/acts_as_versioned/test/database.yml new file mode 100644 index 000000000..506e6bd37 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/database.yml @@ -0,0 +1,18 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/authors.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/authors.yml new file mode 100644 index 000000000..bd7a5aed6 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/authors.yml @@ -0,0 +1,6 @@ +caged: + id: 1 + name: caged +mly: + id: 2 + name: mly \ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb b/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb new file mode 100644 index 000000000..cb9b93057 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb @@ -0,0 +1,3 @@ +class Landmark < ActiveRecord::Base + acts_as_versioned :if_changed => [ :name, :longitude, :latitude ] +end diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml new file mode 100644 index 000000000..2dbd54ed2 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml @@ -0,0 +1,7 @@ +washington: + id: 1 + landmark_id: 1 + version: 1 + name: Washington, D.C. + latitude: 38.895 + longitude: -77.036667 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml new file mode 100644 index 000000000..46d96176a --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml @@ -0,0 +1,6 @@ +washington: + id: 1 + name: Washington, D.C. + latitude: 38.895 + longitude: -77.036667 + version: 1 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml new file mode 100644 index 000000000..318e776cb --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml @@ -0,0 +1,10 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml new file mode 100644 index 000000000..5c978e629 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml @@ -0,0 +1,27 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb b/groups/vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb new file mode 100644 index 000000000..9512b5e82 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb @@ -0,0 +1,13 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/page.rb b/groups/vendor/plugins/acts_as_versioned/test/fixtures/page.rb new file mode 100644 index 000000000..f133e351a --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/page.rb @@ -0,0 +1,43 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml new file mode 100644 index 000000000..ef565fa4f --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml @@ -0,0 +1,16 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/pages.yml b/groups/vendor/plugins/acts_as_versioned/test/fixtures/pages.yml new file mode 100644 index 000000000..07ac51f97 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/pages.yml @@ -0,0 +1,7 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb b/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb new file mode 100644 index 000000000..3c38f2fcf --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb @@ -0,0 +1,6 @@ +class Widget < ActiveRecord::Base + acts_as_versioned :sequence_name => 'widgets_seq', :association_options => { + :dependent => nil, :order => 'version desc' + } + non_versioned_columns << 'foo' +end \ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb b/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb new file mode 100644 index 000000000..d85e95883 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb @@ -0,0 +1,32 @@ +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 + ActiveRecord::Base.connection.initialize_schema_information + ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0" + + 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' + assert_equal 1, t.versions.size + + # 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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/schema.rb b/groups/vendor/plugins/acts_as_versioned/test/schema.rb new file mode 100644 index 000000000..7d5153d07 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/schema.rb @@ -0,0 +1,68 @@ +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 diff --git a/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb b/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb new file mode 100644 index 000000000..c1e1a4b98 --- /dev/null +++ b/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb @@ -0,0 +1,313 @@ +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 + + 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_changed_attributes_default_value + assert !Page.track_changed_attributes + assert LockedPage.track_changed_attributes + assert SpecialLockedPage.track_changed_attributes + end + + def test_version_order + assert_equal 23, pages(:welcome).versions.first.version + assert_equal 24, pages(:welcome).versions.last.version + assert_equal 23, pages(:welcome).find_versions.first.version + assert_equal 24, pages(:welcome).find_versions.last.version + end + + def test_track_changed_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).find_versions(:conditions => ['title LIKE ?', '%weblog%']).length + assert_equal 2, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%web%']).length + assert_equal 0, locked_pages(:thinking).find_versions(:conditions => ['title LIKE ?', '%web%']).length + assert_equal 2, locked_pages(:welcome).find_versions.length + end + + def test_with_sequence + assert_equal 'widgets_seq', Widget.versioned_class.sequence_name + Widget.create :name => 'new widget' + Widget.create :name => 'new widget' + 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_nil 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_unchanged_attributes + landmarks(:washington).attributes = landmarks(:washington).attributes + assert !landmarks(:washington).changed? + end + + def test_unchanged_string_attributes + landmarks(:washington).attributes = landmarks(:washington).attributes.inject({}) { |params, (key, value)| params.update key => value.to_s } + assert !landmarks(:washington).changed? + end +end diff --git a/groups/vendor/plugins/acts_as_watchable/init.rb b/groups/vendor/plugins/acts_as_watchable/init.rb new file mode 100644 index 000000000..f39cc7d18 --- /dev/null +++ b/groups/vendor/plugins/acts_as_watchable/init.rb @@ -0,0 +1,3 @@ +# Include hook code here +require File.dirname(__FILE__) + '/lib/acts_as_watchable' +ActiveRecord::Base.send(:include, Redmine::Acts::Watchable) diff --git a/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb b/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb new file mode 100644 index 000000000..53e4455cf --- /dev/null +++ b/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb @@ -0,0 +1,53 @@ +# 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 + end + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + def add_watcher(user) + self.watchers << Watcher.new(:user => user) + end + + 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 + + def watched_by?(user) + !self.watchers.find(:first, + :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil? + end + + def watcher_recipients + self.watchers.collect { |w| w.user.mail if w.user.active? }.compact + end + + module ClassMethods + def watched_by(user) + find(:all, + :include => :watchers, + :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]) + end + end + end + end + end +end \ No newline at end of file diff --git a/groups/vendor/plugins/classic_pagination/CHANGELOG b/groups/vendor/plugins/classic_pagination/CHANGELOG new file mode 100644 index 000000000..d7d11f129 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/CHANGELOG @@ -0,0 +1,152 @@ +* Exported the changelog of Pagination code for historical reference. + +* Imported some patches from Rails Trac (others closed as "wontfix"): + #8176, #7325, #7028, #4113. Documentation is much cleaner now and there + are some new unobtrusive features! + +* Extracted Pagination from Rails trunk (r6795) + +# +# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb +# +# Generated by Trac 0.10.3 +# 05/20/07 23:48:02 +# + +09/03/06 23:28:54 david [4953] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Docs and deprecation + +08/07/06 12:40:14 bitsweat [4715] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Deprecate direct usage of @params. Update ActionView::Base for + instance var deprecation. + +06/21/06 02:16:11 rick [4476] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Fix indent in pagination documentation. Closes #4990. [Kevin Clark] + +04/25/06 17:42:48 marcel [4268] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Remove all remaining references to @params in the documentation. + +03/16/06 06:38:08 rick [3899] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + trivial documentation patch for #pagination_links [Francois + Beausoleil] closes #4258 + +02/20/06 03:15:22 david [3620] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/test/activerecord/pagination_test.rb (modified) + * trunk/activerecord/CHANGELOG (modified) + * trunk/activerecord/lib/active_record/base.rb (modified) + * trunk/activerecord/test/base_test.rb (modified) + Added :count option to pagination that'll make it possible for the + ActiveRecord::Base.count call to using something else than * for the + count. Especially important for count queries using DISTINCT #3839 + [skaes]. Added :select option to Base.count that'll allow you to + select something else than * to be counted on. Especially important + for count queries using DISTINCT (closes #3839) [skaes]. + +02/09/06 09:17:40 nzkoz [3553] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/test/active_record_unit.rb (added) + * trunk/actionpack/test/activerecord (added) + * trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added) + * trunk/actionpack/test/activerecord/pagination_test.rb (added) + * trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted) + * trunk/actionpack/test/fixtures/companies.yml (added) + * trunk/actionpack/test/fixtures/company.rb (added) + * trunk/actionpack/test/fixtures/db_definitions (added) + * trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added) + * trunk/actionpack/test/fixtures/developer.rb (added) + * trunk/actionpack/test/fixtures/developers_projects.yml (added) + * trunk/actionpack/test/fixtures/developers.yml (added) + * trunk/actionpack/test/fixtures/project.rb (added) + * trunk/actionpack/test/fixtures/projects.yml (added) + * trunk/actionpack/test/fixtures/replies.yml (added) + * trunk/actionpack/test/fixtures/reply.rb (added) + * trunk/actionpack/test/fixtures/topic.rb (added) + * trunk/actionpack/test/fixtures/topics.yml (added) + * Fix pagination problems when using include + * Introduce Unit Tests for pagination + * Allow count to work with :include by using count distinct. + + [Kevin Clark & Jeremy Hopple] + +11/05/05 02:10:29 bitsweat [2878] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Update paginator docs. Closes #2744. + +10/16/05 15:42:03 minam [2649] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Update/clean up AP documentation (rdoc) + +08/31/05 00:13:10 ulysses [2078] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Add option to specify the singular name used by pagination. Closes + #1960 + +08/23/05 14:24:15 minam [2041] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Add support for :include with pagination (subject to existing + constraints for :include with :limit and :offset) #1478 + [michael@schubert.cx] + +07/15/05 20:27:38 david [1839] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + More pagination speed #1334 [Stefan Kaes] + +07/14/05 08:02:01 david [1832] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + * trunk/actionpack/test/controller/addresses_render_test.rb (modified) + Made pagination faster #1334 [Stefan Kaes] + +04/13/05 05:40:22 david [1159] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/activerecord/lib/active_record/base.rb (modified) + Fixed pagination to work with joins #1034 [scott@sigkill.org] + +04/02/05 09:11:17 david [1067] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_controller/scaffolding.rb (modified) + * trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified) + * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified) + * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified) + Added pagination for scaffolding (10 items per page) #964 + [mortonda@dgrmm.net] + +03/31/05 14:46:11 david [1048] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Improved the message display on the exception handler pages #963 + [Johan Sorensen] + +03/27/05 00:04:07 david [1017] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Fixed that pagination_helper would ignore :params #947 [Sebastian + Kanthak] + +03/22/05 13:09:44 david [976] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Fixed documentation and prepared for 0.11.0 release + +03/21/05 14:35:36 david [967] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Tweaked the documentation + +03/20/05 23:12:05 david [949] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller.rb (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (added) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added) + * trunk/activesupport/lib/active_support/core_ext/kernel.rb (added) + Added pagination support through both a controller and helper add-on + #817 [Sam Stephenson] diff --git a/groups/vendor/plugins/classic_pagination/README b/groups/vendor/plugins/classic_pagination/README new file mode 100644 index 000000000..e94904974 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/README @@ -0,0 +1,18 @@ +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. diff --git a/groups/vendor/plugins/classic_pagination/Rakefile b/groups/vendor/plugins/classic_pagination/Rakefile new file mode 100644 index 000000000..c7e374b56 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the classic_pagination plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the classic_pagination plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Pagination' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/groups/vendor/plugins/classic_pagination/init.rb b/groups/vendor/plugins/classic_pagination/init.rb new file mode 100644 index 000000000..25e552f2a --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/init.rb @@ -0,0 +1,33 @@ +#-- +# 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 diff --git a/groups/vendor/plugins/classic_pagination/install.rb b/groups/vendor/plugins/classic_pagination/install.rb new file mode 100644 index 000000000..adf746f8b --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/install.rb @@ -0,0 +1 @@ +puts "\n\n" + File.read(File.dirname(__FILE__) + '/README') diff --git a/groups/vendor/plugins/classic_pagination/lib/pagination.rb b/groups/vendor/plugins/classic_pagination/lib/pagination.rb new file mode 100644 index 000000000..b6e9cf4bc --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/lib/pagination.rb @@ -0,0 +1,405 @@ +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 @people + # 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 + # @person_pages Paginator instance. The current page is determined + # by the params[:page] 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 @person_pages + # and @people 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 @people 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] ||= Inflector.singularize(collection_id.to_s) + options[:class_name] ||= 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: + # :singular_name:: the singular name to use, if it can't be inferred by singularizing the collection name + # :class_name:: the class name to use, if it can't be inferred by + # camelizing the singular name + # :per_page:: the maximum number of items to include in a + # single page. Defaults to 10 + # :conditions:: optional conditions passed to Model.find(:all, *params) and + # Model.count + # :order:: optional order parameter passed to Model.find(:all, *params) + # :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) + # :joins:: optional joins parameter passed to Model.find(:all, *params) + # and Model.count + # :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) + # and Model.count + # :include:: optional eager loading parameter passed to Model.find(:all, *params) + # and Model.count + # :select:: :select parameter passed to Model.find(:all, *params) + # + # :count:: parameter passed as :select option to Model.count(*params) + # + # :group:: :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 :actions option). + # + # +options+ are the same as PaginationHelper#paginate, with the addition + # of: + # :actions:: 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 diff --git a/groups/vendor/plugins/classic_pagination/lib/pagination_helper.rb b/groups/vendor/plugins/classic_pagination/lib/pagination_helper.rb new file mode 100644 index 000000000..069d77566 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/lib/pagination_helper.rb @@ -0,0 +1,135 @@ +module ActionView + module Helpers + # Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally + # also build your links manually using ActionView::Helpers::AssetHelper#link_to like so: + # + # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %> + # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %> + module PaginationHelper + unless const_defined?(:DEFAULT_OPTIONS) + DEFAULT_OPTIONS = { + :name => :page, + :window_size => 2, + :always_show_anchors => true, + :link_to_current_page => false, + :params => {} + } + end + + # Creates a basic HTML link bar for the given +paginator+. Links will be created + # for the next and/or previous page and for a number of other pages around the current + # pages position. The +html_options+ hash is passed to +link_to+ when the links are created. + # + # ==== Options + # :name:: the routing name for this paginator + # (defaults to +page+) + # :prefix:: prefix for pagination links + # (i.e. Older Pages: 1 2 3 4) + # :suffix:: suffix for pagination links + # (i.e. 1 2 3 4 <- Older Pages) + # :window_size:: the number of pages to show around + # the current page (defaults to 2) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # :params:: any additional routing parameters + # for page URLs + # + # ==== Examples + # # We'll assume we have a paginator setup in @person_pages... + # + # pagination_links(@person_pages) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :link_to_current_page => true) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :always_show_anchors => false) + # # => 1 2 3 + # + # pagination_links(@person_pages, :window_size => 1) + # # => 1 2 ... 10 + # + # pagination_links(@person_pages, :params => { :viewer => "flash" }) + # # => 1 2 3 ... + # # 10 + def pagination_links(paginator, options={}, html_options={}) + name = options[:name] || DEFAULT_OPTIONS[:name] + params = (options[:params] || DEFAULT_OPTIONS[:params]).clone + + prefix = options[:prefix] || '' + suffix = options[:suffix] || '' + + pagination_links_each(paginator, options, prefix, suffix) do |n| + params[name] = n + link_to(n.to_s, params, html_options) + end + end + + # Iterate through the pages of a given +paginator+, invoking a + # block for each page number that needs to be rendered as a link. + # + # ==== Options + # :window_size:: the number of pages to show around + # the current page (defaults to +2+) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # + # ==== Example + # # Turn paginated links into an Ajax call + # pagination_links_each(paginator, page_options) do |link| + # options = { :url => {:action => 'list'}, :update => 'results' } + # html_options = { :href => url_for(:action => 'list') } + # + # link_to_remote(link.to_s, options, html_options) + # end + def pagination_links_each(paginator, options, prefix = nil, suffix = nil) + options = DEFAULT_OPTIONS.merge(options) + link_to_current_page = options[:link_to_current_page] + always_show_anchors = options[:always_show_anchors] + + current_page = paginator.current_page + window_pages = current_page.window(options[:window_size]).pages + return if window_pages.length <= 1 unless link_to_current_page + + first, last = paginator.first, paginator.last + + html = '' + + html << prefix if prefix + + if always_show_anchors and not (wp_first = window_pages[0]).first? + html << yield(first.number) + html << ' ... ' if wp_first.number - first.number > 1 + html << ' ' + end + + window_pages.each do |page| + if current_page == page && !link_to_current_page + html << page.number.to_s + else + html << yield(page.number) + end + html << ' ' + end + + if always_show_anchors and not (wp_last = window_pages[-1]).last? + html << ' ... ' if last.number - wp_last.number > 1 + html << yield(last.number) + end + + html << suffix if suffix + + html + end + + end # PaginationHelper + end # Helpers +end # ActionView diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/companies.yml b/groups/vendor/plugins/classic_pagination/test/fixtures/companies.yml new file mode 100644 index 000000000..707f72abc --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/companies.yml @@ -0,0 +1,24 @@ +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 diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/company.rb b/groups/vendor/plugins/classic_pagination/test/fixtures/company.rb new file mode 100644 index 000000000..0d1c29b90 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/company.rb @@ -0,0 +1,9 @@ +class Company < ActiveRecord::Base + attr_protected :rating + set_sequence_name :companies_nonstd_seq + + validates_presence_of :name + def validate + errors.add('rating', 'rating should not be 2') if rating == 2 + end +end \ No newline at end of file diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/developer.rb b/groups/vendor/plugins/classic_pagination/test/fixtures/developer.rb new file mode 100644 index 000000000..f5e5b901f --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/developer.rb @@ -0,0 +1,7 @@ +class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects +end + +class DeVeLoPeR < ActiveRecord::Base + set_table_name "developers" +end diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/developers.yml b/groups/vendor/plugins/classic_pagination/test/fixtures/developers.yml new file mode 100644 index 000000000..308bf75de --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/developers.yml @@ -0,0 +1,21 @@ +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 diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml b/groups/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml new file mode 100644 index 000000000..cee359c7c --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml @@ -0,0 +1,13 @@ +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 diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/project.rb b/groups/vendor/plugins/classic_pagination/test/fixtures/project.rb new file mode 100644 index 000000000..2b53d39ed --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + has_and_belongs_to_many :developers, :uniq => true +end diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/projects.yml b/groups/vendor/plugins/classic_pagination/test/fixtures/projects.yml new file mode 100644 index 000000000..02800c782 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +action_controller: + id: 2 + name: Active Controller + +active_record: + id: 1 + name: Active Record diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/replies.yml b/groups/vendor/plugins/classic_pagination/test/fixtures/replies.yml new file mode 100644 index 000000000..284c9c079 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/replies.yml @@ -0,0 +1,13 @@ +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 diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/reply.rb b/groups/vendor/plugins/classic_pagination/test/fixtures/reply.rb new file mode 100644 index 000000000..ea84042b9 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/reply.rb @@ -0,0 +1,5 @@ +class Reply < ActiveRecord::Base + belongs_to :topic, :include => [:replies] + + validates_presence_of :content +end diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/schema.sql b/groups/vendor/plugins/classic_pagination/test/fixtures/schema.sql new file mode 100644 index 000000000..b4e7539d1 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/schema.sql @@ -0,0 +1,42 @@ +CREATE TABLE 'companies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'rating' INTEGER DEFAULT 1 +); + +CREATE TABLE 'replies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'content' text, + 'created_at' datetime, + 'updated_at' datetime, + 'topic_id' integer +); + +CREATE TABLE 'topics' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' varchar(255), + 'subtitle' varchar(255), + 'content' text, + 'created_at' datetime, + 'updated_at' datetime +); + +CREATE TABLE 'developers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'salary' INTEGER DEFAULT 70000, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'projects' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL +); + +CREATE TABLE 'developers_projects' ( + 'developer_id' INTEGER NOT NULL, + 'project_id' INTEGER NOT NULL, + 'joined_on' DATE DEFAULT NULL, + 'access_level' INTEGER DEFAULT 1 +); diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/topic.rb b/groups/vendor/plugins/classic_pagination/test/fixtures/topic.rb new file mode 100644 index 000000000..0beeecf28 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/topic.rb @@ -0,0 +1,3 @@ +class Topic < ActiveRecord::Base + has_many :replies, :include => [:user], :dependent => :destroy +end diff --git a/groups/vendor/plugins/classic_pagination/test/fixtures/topics.yml b/groups/vendor/plugins/classic_pagination/test/fixtures/topics.yml new file mode 100644 index 000000000..61ea02d76 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/fixtures/topics.yml @@ -0,0 +1,22 @@ +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) %> diff --git a/groups/vendor/plugins/classic_pagination/test/helper.rb b/groups/vendor/plugins/classic_pagination/test/helper.rb new file mode 100644 index 000000000..3f76d5a76 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/helper.rb @@ -0,0 +1,117 @@ +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 diff --git a/groups/vendor/plugins/classic_pagination/test/pagination_helper_test.rb b/groups/vendor/plugins/classic_pagination/test/pagination_helper_test.rb new file mode 100644 index 000000000..d8394a793 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/pagination_helper_test.rb @@ -0,0 +1,38 @@ +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 2 3 ", 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 2 3 ", 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 2 3 Older", output + end +end diff --git a/groups/vendor/plugins/classic_pagination/test/pagination_test.rb b/groups/vendor/plugins/classic_pagination/test/pagination_test.rb new file mode 100644 index 000000000..16a6f1d84 --- /dev/null +++ b/groups/vendor/plugins/classic_pagination/test/pagination_test.rb @@ -0,0 +1,177 @@ +require File.dirname(__FILE__) + '/helper' +require File.dirname(__FILE__) + '/../init' + +class PaginationTest < ActiveRecordTestCase + fixtures :topics, :replies, :developers, :projects, :developers_projects + + class PaginationController < ActionController::Base + if respond_to? :view_paths= + self.view_paths = [ "#{File.dirname(__FILE__)}/../fixtures/" ] + else + self.template_root = [ "#{File.dirname(__FILE__)}/../fixtures/" ] + end + + def simple_paginate + @topic_pages, @topics = paginate(:topics) + render :nothing => true + end + + def paginate_with_per_page + @topic_pages, @topics = paginate(:topics, :per_page => 1) + render :nothing => true + end + + def paginate_with_order + @topic_pages, @topics = paginate(:topics, :order => 'created_at asc') + render :nothing => true + end + + def paginate_with_order_by + @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc') + render :nothing => true + end + + def paginate_with_include_and_order + @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc') + render :nothing => true + end + + def paginate_with_conditions + @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago]) + render :nothing => true + end + + def paginate_with_class_name + @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR") + render :nothing => true + end + + def paginate_with_singular_name + @developer_pages, @developers = paginate() + render :nothing => true + end + + def paginate_with_joins + @developer_pages, @developers = paginate(:developers, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join + @developer_pages, @developers = paginate(:developers, + :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join_and_count + @developer_pages, @developers = paginate(:developers, + :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id', + :conditions => 'project_id=1', + :count => "d.id") + render :nothing => true + end + + def paginate_with_join_and_group + @developer_pages, @developers = paginate(:developers, + :join => 'INNER JOIN developers_projects ON developers.id = developers_projects.developer_id', + :group => 'developers.id') + render :nothing => true + end + + def rescue_errors(e) raise e end + + def rescue_action(e) raise end + + end + + def setup + @controller = PaginationController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + super + end + + # Single Action Pagination Tests + + def test_simple_paginate + get :simple_paginate + assert_equal 1, assigns(:topic_pages).page_count + assert_equal 3, assigns(:topics).size + end + + def test_paginate_with_per_page + get :paginate_with_per_page + assert_equal 1, assigns(:topics).size + assert_equal 3, assigns(:topic_pages).page_count + end + + def test_paginate_with_order + get :paginate_with_order + expected = [topics(:futurama), + topics(:harvey_birdman), + topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_order_by + get :paginate_with_order + expected = assigns(:topics) + get :paginate_with_order_by + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_conditions + get :paginate_with_conditions + expected = [topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_class_name + get :paginate_with_class_name + + assert assigns(:developers).size > 0 + assert_equal DeVeLoPeR, assigns(:developers).first.class + end + + def test_paginate_with_joins + get :paginate_with_joins + assert_equal 2, assigns(:developers).size + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end + + def test_paginate_with_join_and_conditions + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_join_and_count + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join_and_count + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_include_and_order + get :paginate_with_include_and_order + expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10) + assert_equal expected, assigns(:topics) + end + + def test_paginate_with_join_and_group + get :paginate_with_join_and_group + assert_equal 2, assigns(:developers).size + assert_equal 2, assigns(:developer_pages).item_count + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end +end diff --git a/groups/vendor/plugins/coderay-0.7.6.227/FOLDERS b/groups/vendor/plugins/coderay-0.7.6.227/FOLDERS new file mode 100644 index 000000000..e393ed7f9 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/FOLDERS @@ -0,0 +1,53 @@ += CodeRay - Trunk folder structure + +== bench - Benchmarking system + +All benchmarking stuff goes here. + +Test inputs are stored in files named example.. +Test outputs go to bench/test.. + +Run bench/bench.rb to get a usage description. + +Run rake bench to perform an example benchmark. + + +== bin - Scripts + +Executional files for CodeRay. + + +== demo - Demos and functional tests + +Demonstrational scripts to show of CodeRay's features. + +Run them as functional tests with rake test:demos. + + +== etc - Lots of stuff + +Some addidtional files for CodeRay, mainly graphics and Vim scripts. + + +== gem_server - Gem output folder + +For rake gem. + + +== lib - CodeRay library code + +This is the base directory for the CodeRay library. + + +== rake_helpers - Rake helper libraries + +Some files to enhance Rake, including the Autumnal Rdoc template and some scripts. + + +== test - Tests + +Tests for the scanners. + +Each language has its own subfolder and sub-suite. + +Run with rake test. diff --git a/groups/vendor/plugins/coderay-0.7.6.227/LICENSE b/groups/vendor/plugins/coderay-0.7.6.227/LICENSE new file mode 100644 index 000000000..c00103def --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/LICENSE @@ -0,0 +1,504 @@ + 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. + + 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. + + 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. + + 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. + + 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. + + 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. + + 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. + + 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. + + 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 + + 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. + + + Copyright (C) + + 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. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/groups/vendor/plugins/coderay-0.7.6.227/README b/groups/vendor/plugins/coderay-0.7.6.227/README new file mode 100644 index 000000000..ef8275529 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/README @@ -0,0 +1,128 @@ += 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 . + + +----- + +== 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 :) diff --git a/groups/vendor/plugins/coderay-0.7.6.227/bin/coderay b/groups/vendor/plugins/coderay-0.7.6.227/bin/coderay new file mode 100644 index 000000000..52477613c --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/bin/coderay @@ -0,0 +1,82 @@ +#!/usr/bin/env ruby +# CodeRay Executable +# +# Version: 0.1 +# Author: murphy + +def err msg + $stderr.puts msg +end + +begin + require 'coderay' + + if ARGV.empty? + puts <<-USAGE +CodeRay #{CodeRay::VERSION} (http://rd.cYcnus.de/coderay) +Usage: + coderay - [-] < file > output + coderay file [-] +Example: + coderay -ruby -statistic < foo.rb + coderay codegen.c # generates codegen.c.html + USAGE + end + + first, second = ARGV + + if first + if first[/-(\w+)/] == first + lang = $1.to_sym + input = $stdin.read + tokens = :scan + elsif first == '-' + lang = $1.to_sym + input = $stdin.read + tokens = :scan + else + file = first + tokens = CodeRay.scan_file file + output_filename, output_ext = file, /#{Regexp.escape(File.extname(file))}$/ + end + else + puts 'No lang/file given.' + exit 1 + end + + if second + if second[/-(\w+)/] == second + format = $1.to_sym + else + raise 'Invalid format (must be -xxx).' + end + else + $stderr.puts 'No format given; setting to default (HTML Page)' + format = :page + end + + # TODO: allow streaming + if tokens == :scan + output = CodeRay::Duo[lang => format].highlight input #, :stream => true + else + output = tokens.encode format + end + out = $stdout + if output_filename + output_filename += '.' + CodeRay::Encoders[format]::FILE_EXTENSION + if File.exist? output_filename + err 'File %s already exists.' % output_filename + exit + else + out = File.open output_filename, 'w' + end + end + out.print output + +rescue => boom + err "Error: #{boom.message}\n" + err boom.backtrace + err '-' * 50 + err ARGV + exit 1 +end diff --git a/groups/vendor/plugins/coderay-0.7.6.227/bin/coderay_stylesheet b/groups/vendor/plugins/coderay-0.7.6.227/bin/coderay_stylesheet new file mode 100644 index 000000000..baa7c260e --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/bin/coderay_stylesheet @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require 'coderay' + +puts CodeRay::Encoders[:html]::CSS.new.stylesheet diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay.rb new file mode 100644 index 000000000..fb6a29e1f --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay.rb @@ -0,0 +1,320 @@ +# = 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 "Hello, world!" +# +# +# === 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 :python or :html. +# * 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, :ruby 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 :html or :statistic. +# +# 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
    . + # + # 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
    . + # + # 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 :scanner_options 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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/duo.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/duo.rb new file mode 100644 index 000000000..9d11c0e37 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/duo.rb @@ -0,0 +1,87 @@ +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 + diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoder.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoder.rb new file mode 100644 index 000000000..8e67172ca --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoder.rb @@ -0,0 +1,177 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/_map.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/_map.rb new file mode 100644 index 000000000..8e9732b05 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/_map.rb @@ -0,0 +1,9 @@ +module CodeRay +module Encoders + + map :stats => :statistic, + :plain => :text, + :tex => :latex + +end +end diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/count.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/count.rb new file mode 100644 index 000000000..c9a6dfdea --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/count.rb @@ -0,0 +1,21 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/debug.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/debug.rb new file mode 100644 index 000000000..8e1c0f01a --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/debug.rb @@ -0,0 +1,41 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/div.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/div.rb new file mode 100644 index 000000000..3d55415f7 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/div.rb @@ -0,0 +1,20 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html.rb new file mode 100644 index 000000000..f0a123ed8 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html.rb @@ -0,0 +1,268 @@ +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) + # #-> Some /code/ + # puts CodeRay.scan('Some /code/', :ruby).span #-> the same + # + # puts CodeRay.scan('Some code', :ruby).html( + # :wrap => nil, + # :line_numbers => :inline, + # :css => :style + # ) + # #-> 1 Some 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] = '' % [title] + else + h[k.dup] = '' % [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 + '' % [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 << '' * @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 << '' + else + @out << text + end + else + case text + when :open + @opened[0] = type + @out << (@css_style[@opened] || '') + @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 << '' + @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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/css.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/css.rb new file mode 100644 index 000000000..d5776027f --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/css.rb @@ -0,0 +1,65 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/numerization.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/numerization.rb new file mode 100644 index 000000000..1e4a4ed53 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/numerization.rb @@ -0,0 +1,122 @@ +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 + "#{line}" # 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 = "#{indent}#{line_number} " + 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/) { "\n" } + + line_numbers_table_tpl = TABLE.apply('LINE_NUMBERS', line_numbers) + gsub!(/\n/) { "\n" } + 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 = '' * opened_tags.size + + "
  • #{open}#{line}#{close}
  • " + 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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/output.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/output.rb new file mode 100644 index 000000000..e74e55e6e --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/html/output.rb @@ -0,0 +1,195 @@ +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 'Code' + # 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 + + 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 = `<%CONTENT%>` + + DIV = <<-`DIV` +
    +
    <%CONTENT%>
    +
    + DIV + + TABLE = <<-`TABLE` + + + +
    <%LINE_NUMBERS%>
    <%CONTENT%>
    + TABLE + # title="double click to expand" + + LIST = <<-`LIST` +
      <%CONTENT%>
    + LIST + + PAGE = <<-`PAGE` + + + + + CodeRay HTML Encoder Example + + + + +<%CONTENT%> + + + PAGE + + end + + end + +end +end diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/null.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/null.rb new file mode 100644 index 000000000..add3862a3 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/null.rb @@ -0,0 +1,26 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/page.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/page.rb new file mode 100644 index 000000000..c08f09468 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/page.rb @@ -0,0 +1,21 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/span.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/span.rb new file mode 100644 index 000000000..988afec17 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/span.rb @@ -0,0 +1,20 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/statistic.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/statistic.rb new file mode 100644 index 000000000..6d0c64680 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/statistic.rb @@ -0,0 +1,77 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/text.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/text.rb new file mode 100644 index 000000000..14282ac5f --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/text.rb @@ -0,0 +1,32 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/tokens.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/tokens.rb new file mode 100644 index 000000000..27c7f6d5a --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/tokens.rb @@ -0,0 +1,44 @@ +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: + # + # \t \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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/xml.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/xml.rb new file mode 100644 index 000000000..dffa98c36 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/xml.rb @@ -0,0 +1,70 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/yaml.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/yaml.rb new file mode 100644 index 000000000..5564e58a4 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/encoders/yaml.rb @@ -0,0 +1,22 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/file_type.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/file_type.rb new file mode 100644 index 000000000..7e65fe425 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/file_type.rb @@ -0,0 +1,195 @@ +module CodeRay + +# = FileType +# +# A simple filetype recognizer. +# +# Copyright (c) 2006 by murphy (Kornelius Kalnbach) +# +# 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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/gzip_simple.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/gzip_simple.rb new file mode 100644 index 000000000..76aeb2274 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/gzip_simple.rb @@ -0,0 +1,123 @@ +# =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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/plugin.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/plugin.rb new file mode 100644 index 000000000..29b546ae6 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/plugin.rb @@ -0,0 +1,329 @@ +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 '/' +# +# 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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/word_list.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/word_list.rb new file mode 100644 index 000000000..5196a5d68 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/helpers/word_list.rb @@ -0,0 +1,123 @@ +module CodeRay + +# = WordList +# +# A Hash subclass designed for mapping word lists to token types. +# +# Copyright (c) 2006 by murphy (Kornelius Kalnbach) +# +# 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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanner.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanner.rb new file mode 100644 index 000000000..c956bad98 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanner.rb @@ -0,0 +1,253 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/_map.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/_map.rb new file mode 100644 index 000000000..1c5fc8922 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/_map.rb @@ -0,0 +1,15 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb new file mode 100644 index 000000000..f6d71ade2 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb @@ -0,0 +1,165 @@ +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]+>?|"[^"\n\\]*(?:\\.[^"\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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/debug.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/debug.rb new file mode 100644 index 000000000..0dee38fa9 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/debug.rb @@ -0,0 +1,60 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/delphi.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/delphi.rb new file mode 100644 index 000000000..5ee07a3ad --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/delphi.rb @@ -0,0 +1,149 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/html.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/html.rb new file mode 100644 index 000000000..5f647d3a6 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/html.rb @@ -0,0 +1,177 @@ +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(//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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/java.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/java.rb new file mode 100644 index 000000000..4c9ac0060 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/java.rb @@ -0,0 +1,130 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/javascript.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/javascript.rb new file mode 100644 index 000000000..419a5255b --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/javascript.rb @@ -0,0 +1,176 @@ +# http://pastie.textmate.org/50774/ +module CodeRay module Scanners + + class JavaScript < Scanner + + register_for :javascript + + RESERVED_WORDS = [ + 'asm', 'break', 'case', 'continue', 'default', 'do', 'else', + 'for', 'goto', 'if', 'return', 'switch', 'while', +# 'struct', 'union', 'enum', 'typedef', +# 'static', 'register', 'auto', 'extern', +# 'sizeof', + 'typeof', +# 'volatile', 'const', # C89 +# 'inline', 'restrict', # C99 + 'var', 'function','try','new','in', + 'instanceof','throw','catch' + ] + + PREDEFINED_CONSTANTS = [ + 'void', 'null', 'this', + 'true', 'false','undefined', + ] + + IDENT_KIND = WordList.new(:ident). + add(RESERVED_WORDS, :reserved). + 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 + string_type = nil + regexp_allowed = true + + until eos? + + kind = :error + match = nil + + if state == :initial + + if scan(/ \s+ | \\\n /x) + kind = :space + + 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] +# if kind == :ident and check(/:(?!:)/) +# match << scan(/:/) +# kind = :label +# end + regexp_allowed=false + + elsif match = scan(/["']/) + tokens << [:open, :string] + string_type = matched + 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 + 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(/ \\ (?: #{ESCAPE} | #{UNICODE_ESCAPE} ) /mox) + kind = :char + elsif scan(/ \\ | $ /x) + kind = :error + state = :initial + else + raise "else case \" reached; %p not handled." % peek(1), tokens + end + +# elsif state == :include_expected +# if scan(/<[^>\n]+>?|"[^"\n\\]*(?:\\.[^"\n\\]*)*"?/) +# kind = :include +# state = :initial +# +# elsif match = scan(/\s+/) +# kind = :space +# state = :initial if match.index ?\n +# +# else +# getch +# +# end +# + else + raise 'else-case reached', tokens + + end + + match ||= matched +# raise [match, kind], tokens if kind == :error + + tokens << [match, kind] + + end + tokens + + end + + end + +end end \ No newline at end of file diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/nitro_xhtml.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/nitro_xhtml.rb new file mode 100644 index 000000000..d7968cc83 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/nitro_xhtml.rb @@ -0,0 +1,133 @@ +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> )? + | + <% + (?> + [^%]* + (?> %(?!>) [^%]* )* + ) + (?: %> )? + /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 = '' ? '?>' : '' + 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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/php.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/php.rb new file mode 100644 index 000000000..0249e0ef4 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/php.rb @@ -0,0 +1,165 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/plaintext.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/plaintext.rb new file mode 100644 index 000000000..7a08c3a55 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/plaintext.rb @@ -0,0 +1,18 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/rhtml.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/rhtml.rb new file mode 100644 index 000000000..18cc60be6 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/rhtml.rb @@ -0,0 +1,73 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/ruby.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/ruby.rb new file mode 100644 index 000000000..d49773181 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/ruby.rb @@ -0,0 +1,368 @@ +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(/< 1 + state = this_block.first + tokens << [:close, state.type] + end + + tokens + end + + end + +end +end + +# vim:fdm=marker diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/ruby/patterns.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/ruby/patterns.rb new file mode 100644 index 000000000..39962ec06 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/ruby/patterns.rb @@ -0,0 +1,230 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/scheme.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/scheme.rb new file mode 100644 index 000000000..2aee223a7 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/scheme.rb @@ -0,0 +1,142 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/xml.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/xml.rb new file mode 100644 index 000000000..ff923fbf5 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/xml.rb @@ -0,0 +1,18 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/style.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/style.rb new file mode 100644 index 000000000..c2977c5f8 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/style.rb @@ -0,0 +1,20 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/_map.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/_map.rb new file mode 100644 index 000000000..52035fea3 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/_map.rb @@ -0,0 +1,7 @@ +module CodeRay +module Styles + + default :cycnus + +end +end diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/cycnus.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/cycnus.rb new file mode 100644 index 000000000..7747c753f --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/cycnus.rb @@ -0,0 +1,127 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/murphy.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/murphy.rb new file mode 100644 index 000000000..b42f0e043 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/styles/murphy.rb @@ -0,0 +1,119 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/token_classes.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/token_classes.rb new file mode 100644 index 000000000..d0de855a2 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/token_classes.rb @@ -0,0 +1,71 @@ +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 diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/tokens.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/tokens.rb new file mode 100644 index 000000000..26c923f42 --- /dev/null +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/tokens.rb @@ -0,0 +1,383 @@ +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 diff --git a/groups/vendor/plugins/gloc-1.1.0/CHANGELOG b/groups/vendor/plugins/gloc-1.1.0/CHANGELOG new file mode 100644 index 000000000..6392d7cbe --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/CHANGELOG @@ -0,0 +1,19 @@ +== Version 1.1 (28 May 2006) + +* The charset for each and/or all languages can now be easily configured. +* Added a ActionController filter that auto-detects the client language. +* The rake task "sort" now merges lines that match 100%, and warns if duplicate keys are found. +* Rule support. Create flexible rules to handle issues such as pluralization. +* Massive speed and stability improvements to development mode. +* Added Russian strings. (Thanks to Evgeny Lineytsev) +* Complete RDoc documentation. +* Improved helpers. +* GLoc now configurable via get_config and set_config +* Added an option to tell GLoc to output various verbose information. +* More useful functions such as set_language_if_valid, similar_language +* GLoc's entire internal state can now be backed up and restored. + + +== Version 1.0 (17 April 2006) + +* Initial public release. diff --git a/groups/vendor/plugins/gloc-1.1.0/MIT-LICENSE b/groups/vendor/plugins/gloc-1.1.0/MIT-LICENSE new file mode 100644 index 000000000..081774a65 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/MIT-LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2005-2006 David Barri + +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 diff --git a/groups/vendor/plugins/gloc-1.1.0/README b/groups/vendor/plugins/gloc-1.1.0/README new file mode 100644 index 000000000..66f8e5e9f --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/README @@ -0,0 +1,208 @@ += About + +=== Preface +I originally started designing this on weekends and after work in 2005. We started to become very interested in Rails at work and I wanted to get some experience with ruby with before we started using it full-time. I didn't have very many ideas for anything interesting to create so, because we write a lot of multilingual webapps at my company, I decided to write a localization library. That way if my little hobby project developed into something decent, I could at least put it to good use. +And here we are in 2006, my little hobby project has come a long way and become quite a useful piece of software. Not only do I use it in production sites I write at work, but I also prefer it to other existing alternatives. Therefore I have decided to make it publicly available, and I hope that other developers will find it useful too. + +=== About +GLoc is a localization library. It doesn't aim to do everything l10n-related that you can imagine, but what it does, it does very well. It was originally designed as a Rails plugin, but can also be used for plain ruby projects. Here are a list of its main features: +* Lightweight and efficient. +* Uses file-based string bundles. Strings can also be set directly. +* Intelligent, cascading language configuration. +* Create flexible rules to handle issues such as pluralization. +* Includes a ActionController filter that auto-detects the client language. +* Works perfectly with Rails Engines and allows strings to be overridden just as easily as controllers, models, etc. +* Automatically localizes Rails functions such as distance_in_minutes, select_month etc +* Supports different charsets. You can even specify the encoding to use for each language seperately. +* Special Rails mods/helpers. + +=== What does GLoc mean? +If you're wondering about the name "GLoc", I'm sure you're not alone. +This project was originally just called "Localization" which was a bit too common, so when I decided to release it I decided to call it "Golly's Localization Library" instead (Golly is my nickname), and that was long and boring so I then abbreviated that to "GLoc". What a fun story!! + +=== Localization helpers +This also includes a few helpers for common situations such as displaying localized date, time, "yes" or "no", etc. + +=== Rails Localization +At the moment, unless you manually remove the require 'gloc-rails-text' line from init.rb, this plugin overrides certain Rails functions to provide multilingual versions. This automatically localizes functions such as select_date(), distance_of_time_in_words() and more... +The strings can be found in lang/*.yml. +NOTE: This is not complete. Timezones and countries are not currently localized. + + + + += Usage + +=== Quickstart + +Windows users will need to first install iconv. http://wiki.rubyonrails.com/rails/pages/iconv + +* Create a dir "#{RAILS_ROOT}/lang" +* Create a file "#{RAILS_ROOT}/lang/en.yml" and write your strings. The format is "key: string". Save it as UTF-8. If you save it in a different encoding, add a key called file_charset (eg. "file_charset: iso-2022-jp") +* Put the following in config/environment.rb and change the values as you see fit. The following example is for an app that uses English and Japanese, with Japanese being the default. + GLoc.set_config :default_language => :ja + GLoc.clear_strings_except :en, :ja + GLoc.set_kcode + GLoc.load_localized_strings +* Add 'include GLoc' to all classes that will use localization. This is added to most Rails classes automatically. +* Optionally, you can set the language for models and controllers by simply inserting set_language :en in classes and/or methods. +* To use localized strings, replace text such as "Welcome" with l(:welcome_string_key), and "Hello #{name}." with l(:hello_string_key, name). (Of course the strings will need to exist in your string bundle.) + +There is more functionality provided by this plugin, that is not demonstrated above. Please read the API summary for details. + +=== API summary + +The following methods are added as both class methods and instance methods to modules/classes that include GLoc. They are also available as class methods of GLoc. + current_language # Returns the current language + l(symbol, *arguments) # Returns a localized string + ll(lang, symbol, *arguments) # Returns a localized string in a specific language + ltry(possible_key) # Returns a localized string if passed a Symbol, else returns the same argument passed + lwr(symbol, *arguments) # Uses the default rule to return a localized string. + lwr_(rule, symbol, *arguments) # Uses a specified rule to return a localized string. + l_has_string?(symbol) # Checks if a localized string exists + set_language(language) # Sets the language for the current class or class instance + set_language_if_valid(lang) # Sets the current language if the language passed is a valid language + +The GLoc module also defines the following class methods: + add_localized_strings(lang, symbol_hash, override=true) # Adds a hash of localized strings + backup_state(clear=false) # Creates a backup of GLoc's internal state and optionally clears everything too + clear_strings(*languages) # Removes localized strings from memory + clear_strings_except(*languages) # Removes localized strings from memory except for those of certain specified languages + get_charset(lang) # Returns the charset used to store localized strings in memory + get_config(key) # Returns a GLoc configuration value (see below) + load_localized_strings(dir=nil, override=true) # Loads localized strings from all YML files in a given directory + restore_state(state) # Restores a backup of GLoc's internal state + set_charset(new_charset, *langs) # Sets the charset used to internally store localized strings + set_config(hash) # Sets GLoc configuration values (see below) + set_kcode(charset=nil) # Sets the $KCODE global variable + similar_language(language) # Tries to find a valid language that is similar to the argument passed + valid_languages # Returns an array of (currently) valid languages (ie. languages for which localized data exists) + valid_language?(language) # Checks whether any localized strings are in memory for a given language + +GLoc uses the following configuration items. They can be accessed via get_config and set_config. + :default_cookie_name + :default_language + :default_param_name + :raise_string_not_found_errors + :verbose + +The GLoc module is automatically included in the following classes: + ActionController::Base + ActionMailer::Base + ActionView::Base + ActionView::Helpers::InstanceTag + ActiveRecord::Base + ActiveRecord::Errors + ApplicationHelper + Test::Unit::TestCase + +The GLoc module also defines the following controller filters: + autodetect_language_filter + +GLoc also makes the following change to Rails: +* Views for ActionMailer are now #{view_name}_#{language}.rb rather than just #{view_name}.rb +* All ActiveRecord validation class methods now accept a localized string key (symbol) as a :message value. +* ActiveRecord::Errors.add now accepts symbols as valid message values. At runtime these symbols are converted to localized strings using the current_language of the base record. +* ActiveRecord::Errors.add now accepts arrays as arguments so that printf-style strings can be generated at runtime. This also applies to the validates_* class methods. + Eg. validates_xxxxxx_of :name, :message => ['Your name must be at least %d characters.', MIN_LEN] + Eg. validates_xxxxxx_of :name, :message => [:user_error_validation_name_too_short, MIN_LEN] +* Instances of ActiveView inherit their current_language from the controller (or mailer) creating them. + +This plugin also adds the following rake tasks: + * gloc:sort - Sorts the keys in the lang ymls (also accepts a DIR argument) + +=== Cascading language configuration + +The language can be set at three levels: + 1. The default # GLoc.get_config :default_language + 2. Class level # class A; set_language :de; end + 3. Instance level # b= B.new; b.set_language :zh + +Instance level has the highest priority and the default has the lowest. + +Because GLoc is included at class level too, it becomes easy to associate languages with contexts. +For example: + class Student + set_language :en + def say_hello + puts "We say #{l :hello} but our teachers say #{Teacher.l :hello}" + end + end + +=== Rules + +There are often situations when depending on the value of one or more variables, the surrounding text +changes. The most common case of this is pluralization. Rather than hardcode these rules, they are +completely definable by the user so that the user can eaasily accomodate for more complicated grammatical +rules such as those found in Russian and Polish (or so I hear). To define a rule, simply include a string +in the string bundle whose key begins with "_gloc_rule_" and then write ruby code as the value. The ruby +code will be converted to a Proc when the string bundle is first read, and should return a prefix that will +be appended to the string key at runtime to point to a new string. Make sense? Probably not... Please look +at the following example and I am sure it will all make sense. + +Simple example (string bundle / en.yml) + _gloc_rule_default: ' |n| n==1 ? "_single" : "_plural" ' + man_count_plural: There are %d men. + man_count_single: There is 1 man. + +Simple example (code) + lwr(:man_count, 1) # => There is 1 man. + lwr(:man_count, 8) # => There are 8 men. + +To use rules other than the default simply call lwr_ instead of lwr, and specify the rule. + +Example #2 (string bundle / en.yml) + _gloc_rule_default: ' |n| n==1 ? "_single" : "_plural" ' + _gloc_rule_custom: ' |n| return "_none" if n==0; return "_heaps" if n>100; n==1 ? "_single" : "_plural" ' + man_count_none: There are no men. + man_count_heaps: There are heaps of men!! + man_count_plural: There are %d men. + man_count_single: There is 1 man. + +Example #2 (code) + lwr_(:custom, :man_count, 0) # => There are no men. + lwr_(:custom, :man_count, 1) # => There is 1 man. + lwr_(:custom, :man_count, 8) # => There are 8 men. + lwr_(:custom, :man_count, 150) # => There are heaps of men!! + + +=== Helpers + +GLoc includes the following helpers: + l_age(age) # Returns a localized version of an age. eg "3 years old" + l_date(date) # Returns a date in a localized format + l_datetime(date) # Returns a date+time in a localized format + l_datetime_short(date) # Returns a date+time in a localized short format. + l_lang_name(l,dl=nil) # Returns the name of a language (you must supply your own strings) + l_strftime(date,fmt) # Formats a date/time in a localized format. + l_time(date) # Returns a time in a localized format + l_YesNo(value) # Returns localized string of "Yes" or "No" depending on the arg + l_yesno(value) # Returns localized string of "yes" or "no" depending on the arg + +=== Rails localization + +Not all of Rails is covered but the following functions are: + distance_of_time_in_words + select_day + select_month + select_year + add_options + + + + += FAQ + +==== How do I use it in engines? +Simply put this in your init_engine.rb + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') +That way your engines strings will be loaded when the engine is started. Just simply make sure that you load your application strings after you start your engines to safely override any engine strings. + +==== Why am I getting an Iconv::IllegalSequence error when calling GLoc.set_charset? +By default GLoc loads all of its default strings at startup. For example, calling set_charset 'iso-2022-jp' will cause this error because Russian strings are loaded by default, and the Russian strings use characters that cannot be expressed in the ISO-2022-JP charset. +Before calling set_charset you should call clear_strings_except to remove strings from any languages that you will not be using. +Alternatively, you can simply specify the language(s) as follows, set_charset 'iso-2022-jp', :ja. + +==== How do I make GLoc ignore StringNotFoundErrors? +Disable it as follows: + GLoc.set_config :raise_string_not_found_errors => false diff --git a/groups/vendor/plugins/gloc-1.1.0/Rakefile b/groups/vendor/plugins/gloc-1.1.0/Rakefile new file mode 100644 index 000000000..a5b8fe762 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/Rakefile @@ -0,0 +1,15 @@ +Dir.glob("#{File.dirname(__FILE__)}/tasks/*.rake").each {|f| load f} + +task :default => 'gloc:sort' + +# RDoc task +require 'rake/rdoctask' +Rake::RDocTask.new() { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "GLoc Localization Library Documentation" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README', 'CHANGELOG') + rdoc.rdoc_files.include('lib/**/*.rb') + rdoc.rdoc_files.exclude('lib/gloc-dev.rb') + rdoc.rdoc_files.exclude('lib/gloc-config.rb') +} diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionController/Filters/ClassMethods.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionController/Filters/ClassMethods.html new file mode 100644 index 000000000..fba33b5b5 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionController/Filters/ClassMethods.html @@ -0,0 +1,230 @@ + + + + + + Module: ActionController::Filters::ClassMethods + + + + + + + + + + +
    + + + + + + + + + + +
    ModuleActionController::Filters::ClassMethods
    In: + + lib/gloc-rails.rb + +
    +
    +
    + + +
    + + + +
    + + + +
    + +
    +

    Methods

    + + +
    + +
    + + + + +
    + + + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +This filter attempts to auto-detect the clients desired language. It first +checks the params, then a cookie and then the HTTP_ACCEPT_LANGUAGE request +header. If a language is found to match or be similar to a currently valid +language, then it sets the current_language of the controller. +

    +
    +  class ExampleController < ApplicationController
    +    set_language :en
    +    autodetect_language_filter :except => 'monkey', :on_no_lang => :lang_not_autodetected_callback
    +    autodetect_language_filter :only => 'monkey', :check_cookie => 'monkey_lang', :check_accept_header => false
    +    ...
    +    def lang_not_autodetected_callback
    +      redirect_to somewhere
    +    end
    +  end
    +
    +

    +The args for this filter are exactly the same the arguments of +before_filter with the following exceptions: +

    +
      +
    • :check_params — If false, then params will not be checked +for a language. If a String, then this will value will be used as the name +of the param. + +
    • +
    • :check_cookie — If false, then the cookie will not be +checked for a language. If a String, then this will value will be used as +the name of the cookie. + +
    • +
    • :check_accept_header — If false, then HTTP_ACCEPT_LANGUAGE +will not be checked for a language. + +
    • +
    • :on_set_lang — You can specify the name of a callback +function to be called when the language is successfully detected and set. +The param must be a Symbol or a String which is the name of the function. +The callback function must accept one argument (the language) and must be +instance level. + +
    • +
    • :on_no_lang — You can specify the name of a callback +function to be called when the language couldn’t be detected +automatically. The param must be a Symbol or a String which is the name of +the function. The callback function must be instance level. + +
    • +
    +

    +You override the default names of the param or cookie by calling GLoc.set_config :default_param_name +=> ‘new_param_name‘ and GLoc.set_config :default_cookie_name +=> ‘new_cookie_name‘. +

    +

    [Source]

    +
    +
    +    # File lib/gloc-rails.rb, line 43
    +43:       def autodetect_language_filter(*args)
    +44:         options= args.last.is_a?(Hash) ? args.last : {}
    +45:         x= 'Proc.new { |c| l= nil;'
    +46:         # :check_params
    +47:         unless (v= options.delete(:check_params)) == false
    +48:           name= v ? ":#{v}" : 'GLoc.get_config(:default_param_name)'
    +49:           x << "l ||= GLoc.similar_language(c.params[#{name}]);"
    +50:         end
    +51:         # :check_cookie
    +52:         unless (v= options.delete(:check_cookie)) == false
    +53:           name= v ? ":#{v}" : 'GLoc.get_config(:default_cookie_name)'
    +54:           x << "l ||= GLoc.similar_language(c.send(:cookies)[#{name}]);"
    +55:         end
    +56:         # :check_accept_header
    +57:         unless options.delete(:check_accept_header) == false
    +58:           x << %<
    +59:               unless l
    +60:                 a= c.request.env['HTTP_ACCEPT_LANGUAGE'].split(/,|;/) rescue nil
    +61:                 a.each {|x| l ||= GLoc.similar_language(x)} if a
    +62:               end; >
    +63:         end
    +64:         # Set language
    +65:         x << 'ret= true;'
    +66:         x << 'if l; c.set_language(l); c.headers[\'Content-Language\']= l.to_s; '
    +67:         if options.has_key?(:on_set_lang)
    +68:           x << "ret= c.#{options.delete(:on_set_lang)}(l);"
    +69:         end
    +70:         if options.has_key?(:on_no_lang)
    +71:           x << "else; ret= c.#{options.delete(:on_no_lang)};"
    +72:         end
    +73:         x << 'end; ret }'
    +74:         
    +75:         # Create filter
    +76:         block= eval x
    +77:         before_filter(*args, &block)
    +78:       end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionMailer/Base.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionMailer/Base.html new file mode 100644 index 000000000..056b23d85 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionMailer/Base.html @@ -0,0 +1,140 @@ + + + + + + Class: ActionMailer::Base + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    ClassActionMailer::Base
    In: + + lib/gloc-rails.rb + +
    +
    Parent: + Object +
    +
    + + +
    + + + +
    + +
    +

    +In addition to including GLoc, +render_message is also overridden so that mail templates contain +the current language at the end of the file. Eg. deliver_hello +will render hello_en.rhtml. +

    + +
    + + +
    + + +
    + + + +
    +

    Included Modules

    + +
    + GLoc +
    +
    + +
    + + + +
    +

    External Aliases

    + +
    + + + + + + +
    render_message->render_message_without_gloc
    +
    +
    + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Base.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Base.html new file mode 100644 index 000000000..00767055d --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Base.html @@ -0,0 +1,174 @@ + + + + + + Class: ActionView::Base + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    ClassActionView::Base
    In: + + lib/gloc-rails.rb + +
    +
    Parent: + Object +
    +
    + + +
    + + + +
    + +
    +

    +initialize is overridden so that new instances of this class +inherit the current language of the controller. +

    + +
    + + +
    + +
    +

    Methods

    + +
    + new   +
    +
    + +
    + + + +
    +

    Included Modules

    + +
    + GLoc +
    +
    + +
    + + + +
    +

    External Aliases

    + +
    + + + + + + +
    initialize->initialize_without_gloc
    +
    +
    + + + + + + +
    +

    Public Class methods

    + +
    + + + + +
    +

    [Source]

    +
    +
    +     # File lib/gloc-rails.rb, line 109
    +109:     def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)
    +110:       initialize_without_gloc(base_path, assigns_for_first_render, controller)
    +111:       set_language controller.current_language unless controller.nil?
    +112:     end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/DateHelper.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/DateHelper.html new file mode 100644 index 000000000..84ca8fae3 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/DateHelper.html @@ -0,0 +1,348 @@ + + + + + + Module: ActionView::Helpers::DateHelper + + + + + + + + + + +
    + + + + + + + + + + +
    ModuleActionView::Helpers::DateHelper
    In: + + lib/gloc-rails-text.rb + +
    +
    +
    + + +
    + + + +
    + + + +
    + +
    +

    Methods

    + + +
    + +
    + + + + +
    + + +
    +

    Constants

    + +
    + + + + + + + + + + + + + + + + +
    LOCALIZED_HELPERS=true
    LOCALIZED_MONTHNAMES={}
    LOCALIZED_ABBR_MONTHNAMES={}
    +
    +
    + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +This method uses current_language to return a localized string. +

    +

    [Source]

    +
    +
    +    # File lib/gloc-rails-text.rb, line 16
    +16:       def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
    +17:         from_time = from_time.to_time if from_time.respond_to?(:to_time)
    +18:         to_time = to_time.to_time if to_time.respond_to?(:to_time)
    +19:         distance_in_minutes = (((to_time - from_time).abs)/60).round
    +20:         distance_in_seconds = ((to_time - from_time).abs).round
    +21: 
    +22:         case distance_in_minutes
    +23:           when 0..1
    +24:             return (distance_in_minutes==0) ? l(:actionview_datehelper_time_in_words_minute_less_than) : l(:actionview_datehelper_time_in_words_minute_single) unless include_seconds
    +25:             case distance_in_seconds
    +26:               when 0..5   then lwr(:actionview_datehelper_time_in_words_second_less_than, 5)
    +27:               when 6..10  then lwr(:actionview_datehelper_time_in_words_second_less_than, 10)
    +28:               when 11..20 then lwr(:actionview_datehelper_time_in_words_second_less_than, 20)
    +29:               when 21..40 then l(:actionview_datehelper_time_in_words_minute_half)
    +30:               when 41..59 then l(:actionview_datehelper_time_in_words_minute_less_than)
    +31:               else             l(:actionview_datehelper_time_in_words_minute)
    +32:             end
    +33:                                 
    +34:           when 2..45      then lwr(:actionview_datehelper_time_in_words_minute, distance_in_minutes)
    +35:           when 46..90     then l(:actionview_datehelper_time_in_words_hour_about_single)
    +36:           when 90..1440   then lwr(:actionview_datehelper_time_in_words_hour_about, (distance_in_minutes.to_f / 60.0).round)
    +37:           when 1441..2880 then lwr(:actionview_datehelper_time_in_words_day, 1)
    +38:           else                 lwr(:actionview_datehelper_time_in_words_day, (distance_in_minutes / 1440).round)
    +39:         end
    +40:       end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +This method has been modified so that a localized string can be appended to +the day numbers. +

    +

    [Source]

    +
    +
    +    # File lib/gloc-rails-text.rb, line 43
    +43:       def select_day(date, options = {})
    +44:         day_options = []
    +45:         prefix = l :actionview_datehelper_select_day_prefix
    +46: 
    +47:         1.upto(31) do |day|
    +48:           day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ?
    +49:             %(<option value="#{day}" selected="selected">#{day}#{prefix}</option>\n) :
    +50:             %(<option value="#{day}">#{day}#{prefix}</option>\n)
    +51:           )
    +52:         end
    +53: 
    +54:         select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
    +55:       end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +This method has been modified so that +

    +
      +
    • the month names are localized. + +
    • +
    • it uses options: :min_date, :max_date, +:start_month, :end_month + +
    • +
    • a localized string can be appended to the month numbers when the +:use_month_numbers option is specified. + +
    • +
    +

    [Source]

    +
    +
    +    # File lib/gloc-rails-text.rb, line 61
    +61:       def select_month(date, options = {})
    +62:         unless LOCALIZED_MONTHNAMES.has_key?(current_language)
    +63:           LOCALIZED_MONTHNAMES[current_language] = [''] + l(:actionview_datehelper_select_month_names).split(',')
    +64:           LOCALIZED_ABBR_MONTHNAMES[current_language] = [''] + l(:actionview_datehelper_select_month_names_abbr).split(',')
    +65:         end
    +66:         
    +67:         month_options = []
    +68:         month_names = options[:use_short_month] ? LOCALIZED_ABBR_MONTHNAMES[current_language] : LOCALIZED_MONTHNAMES[current_language]
    +69:         
    +70:         if options.has_key?(:min_date) && options.has_key?(:max_date)
    +71:           if options[:min_date].year == options[:max_date].year
    +72:             start_month, end_month = options[:min_date].month, options[:max_date].month
    +73:           end
    +74:         end
    +75:         start_month = (options[:start_month] || 1) unless start_month
    +76:         end_month = (options[:end_month] || 12) unless end_month
    +77:         prefix = l :actionview_datehelper_select_month_prefix
    +78: 
    +79:         start_month.upto(end_month) do |month_number|
    +80:           month_name = if options[:use_month_numbers]
    +81:             "#{month_number}#{prefix}"
    +82:           elsif options[:add_month_numbers]
    +83:             month_number.to_s + ' - ' + month_names[month_number]
    +84:           else
    +85:             month_names[month_number]
    +86:           end
    +87: 
    +88:           month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ?
    +89:             %(<option value="#{month_number}" selected="selected">#{month_name}</option>\n) :
    +90:             %(<option value="#{month_number}">#{month_name}</option>\n)
    +91:           )
    +92:         end
    +93: 
    +94:         select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
    +95:       end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +This method has been modified so that +

    +
      +
    • it uses options: :min_date, :max_date + +
    • +
    • a localized string can be appended to the years numbers. + +
    • +
    +

    [Source]

    +
    +
    +     # File lib/gloc-rails-text.rb, line 100
    +100:       def select_year(date, options = {})
    +101:         year_options = []
    +102:         y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year
    +103: 
    +104:         start_year = options.has_key?(:min_date) ? options[:min_date].year : (options[:start_year] || y-5)
    +105:         end_year = options.has_key?(:max_date) ? options[:max_date].year : (options[:end_year] || y+5)
    +106:         step_val = start_year < end_year ? 1 : -1
    +107:         prefix = l :actionview_datehelper_select_year_prefix
    +108: 
    +109:         start_year.step(end_year, step_val) do |year|
    +110:           year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ?
    +111:             %(<option value="#{year}" selected="selected">#{year}#{prefix}</option>\n) :
    +112:             %(<option value="#{year}">#{year}#{prefix}</option>\n)
    +113:           )
    +114:         end
    +115: 
    +116:         select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled])
    +117:       end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/InstanceTag.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/InstanceTag.html new file mode 100644 index 000000000..a236e0e5d --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/InstanceTag.html @@ -0,0 +1,167 @@ + + + + + + Class: ActionView::Helpers::InstanceTag + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    ClassActionView::Helpers::InstanceTag
    In: + + lib/gloc-rails-text.rb + +
    + + lib/gloc-rails.rb + +
    +
    Parent: + Object +
    +
    + + +
    + + + +
    + +
    +

    +The private method add_options is overridden so that "Please +select" is localized. +

    + +
    + + +
    + +
    +

    Methods

    + + +
    + +
    + + + +
    +

    Included Modules

    + +
    + GLoc +
    +
    + +
    + + + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +Inherits the current language from the template object. +

    +

    [Source]

    +
    +
    +     # File lib/gloc-rails.rb, line 119
    +119:       def current_language
    +120:         @template_object.current_language
    +121:       end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Errors.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Errors.html new file mode 100644 index 000000000..9a16f608b --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Errors.html @@ -0,0 +1,215 @@ + + + + + + Class: ActiveRecord::Errors + + + + + + + + + + +
    + + + + + + + + + + + + + + +
    ClassActiveRecord::Errors
    In: + + lib/gloc-rails.rb + +
    +
    Parent: + Object +
    +
    + + +
    + + + +
    + + + +
    + +
    +

    Methods

    + +
    + add   + current_language   +
    +
    + +
    + + + +
    +

    Included Modules

    + +
    + GLoc +
    +
    + +
    + + + +
    +

    External Aliases

    + +
    + + + + + + +
    add->add_without_gloc
    +
    +
    + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +The GLoc version of this method provides two +extra features +

    +
      +
    • If msg is a string, it will be considered a GLoc string key. + +
    • +
    • If msg is an array, the first element will be considered the +string and the remaining elements will be considered arguments for the +string. Eg. [‘Hi %s.’,’John’] + +
    • +
    +

    [Source]

    +
    +
    +     # File lib/gloc-rails.rb, line 141
    +141:     def add(attribute, msg= @@default_error_messages[:invalid])
    +142:       if msg.is_a?(Array)
    +143:         args= msg.clone
    +144:         msg= args.shift
    +145:         args= nil if args.empty?
    +146:       end
    +147:       msg= ltry(msg)
    +148:       msg= msg % args unless args.nil?
    +149:       add_without_gloc(attribute, msg)
    +150:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Inherits the current language from the base record. +

    +

    [Source]

    +
    +
    +     # File lib/gloc-rails.rb, line 152
    +152:     def current_language
    +153:       @base.current_language
    +154:     end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Validations/ClassMethods.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Validations/ClassMethods.html new file mode 100644 index 000000000..145a74c2b --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Validations/ClassMethods.html @@ -0,0 +1,217 @@ + + + + + + Module: ActiveRecord::Validations::ClassMethods + + + + + + + + + + +
    + + + + + + + + + + +
    ModuleActiveRecord::Validations::ClassMethods
    In: + + lib/gloc-rails.rb + +
    +
    +
    + + +
    + + + +
    + + + +
    + +
    +

    Methods

    + + +
    + +
    + + + + +
    + + + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +The default Rails version of this function creates an error message and +then passes it to ActiveRecord.Errors. The GLoc version of this method, sends an array to +ActiveRecord.Errors that will be turned into a +string by ActiveRecord.Errors which in turn +allows for the message of this validation function to be a GLoc string key. +

    +

    [Source]

    +
    +
    +     # File lib/gloc-rails.rb, line 164
    +164:       def validates_length_of(*attrs)
    +165:         # Merge given options with defaults.
    +166:         options = {
    +167:           :too_long     => ActiveRecord::Errors.default_error_messages[:too_long],
    +168:           :too_short    => ActiveRecord::Errors.default_error_messages[:too_short],
    +169:           :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
    +170:         }.merge(DEFAULT_VALIDATION_OPTIONS)
    +171:         options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
    +172: 
    +173:         # Ensure that one and only one range option is specified.
    +174:         range_options = ALL_RANGE_OPTIONS & options.keys
    +175:         case range_options.size
    +176:           when 0
    +177:             raise ArgumentError, 'Range unspecified.  Specify the :within, :maximum, :minimum, or :is option.'
    +178:           when 1
    +179:             # Valid number of options; do nothing.
    +180:           else
    +181:             raise ArgumentError, 'Too many range options specified.  Choose only one.'
    +182:         end
    +183: 
    +184:         # Get range option and value.
    +185:         option = range_options.first
    +186:         option_value = options[range_options.first]
    +187: 
    +188:         case option
    +189:         when :within, :in
    +190:           raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
    +191: 
    +192:           too_short = [options[:too_short] , option_value.begin]
    +193:           too_long  = [options[:too_long]  , option_value.end  ]
    +194: 
    +195:           validates_each(attrs, options) do |record, attr, value|
    +196:             if value.nil? or value.split(//).size < option_value.begin
    +197:               record.errors.add(attr, too_short)
    +198:             elsif value.split(//).size > option_value.end
    +199:               record.errors.add(attr, too_long)
    +200:             end
    +201:           end
    +202:         when :is, :minimum, :maximum
    +203:           raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
    +204: 
    +205:           # Declare different validations per option.
    +206:           validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
    +207:           message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
    +208: 
    +209:           message = [(options[:message] || options[message_options[option]]) , option_value]
    +210: 
    +211:           validates_each(attrs, options) do |record, attr, value|
    +212:             if value.kind_of?(String)
    +213:               record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value]
    +214:             else
    +215:               record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
    +216:             end
    +217:           end
    +218:         end
    +219:       end
    +
    +
    +
    +
    + +
    + + +
    + validates_size_of(*attrs) +
    + +
    +

    +Alias for validates_length_of +

    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc.html new file mode 100644 index 000000000..8a25c7de8 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc.html @@ -0,0 +1,774 @@ + + + + + + Module: GLoc + + + + + + + + + + + + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + + +
    + + + +
    + + + +
    +

    Included Modules

    + + +
    + +
    + +
    +

    Classes and Modules

    + + Module GLoc::ClassMethods
    +Module GLoc::Helpers
    +Module GLoc::InstanceMethods
    + +
    + +
    +

    Constants

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    LOCALIZED_STRINGS={}
    RULES={}
    LOWERCASE_LANGUAGES={}
    UTF_8='utf-8'
    SHIFT_JIS='sjis'
    EUC_JP='euc-jp'
    +
    +
    + +
    +

    External Aliases

    + +
    + + + + + + +
    clear_strings->_clear_strings
    +
    +
    + + + + + + +
    +

    Public Class methods

    + +
    + + + + +
    +

    +Adds a collection of localized strings to the in-memory string store. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 113
    +113:     def add_localized_strings(lang, symbol_hash, override=true, strings_charset=nil)
    +114:       _verbose_msg {"Adding #{symbol_hash.size} #{lang} strings."}
    +115:       _add_localized_strings(lang, symbol_hash, override, strings_charset)
    +116:       _verbose_msg :stats
    +117:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Creates a backup of the internal state of GLoc (ie. +strings, langs, rules, config) and optionally clears everything. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 121
    +121:     def backup_state(clear=false)
    +122:       s= _get_internal_state_vars.map{|o| o.clone}
    +123:       _get_internal_state_vars.each{|o| o.clear} if clear
    +124:       s
    +125:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Removes all localized strings from memory, either of a certain language (or +languages), or entirely. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 129
    +129:     def clear_strings(*languages)
    +130:       if languages.empty?
    +131:         _verbose_msg {"Clearing all strings"}
    +132:         LOCALIZED_STRINGS.clear
    +133:         LOWERCASE_LANGUAGES.clear
    +134:       else
    +135:         languages.each {|l|
    +136:           _verbose_msg {"Clearing :#{l} strings"}
    +137:           l= l.to_sym
    +138:           LOCALIZED_STRINGS.delete l
    +139:           LOWERCASE_LANGUAGES.each_pair {|k,v| LOWERCASE_LANGUAGES.delete k if v == l}
    +140:         }
    +141:       end
    +142:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Removes all localized strings from memory, except for those of certain +specified languages. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 146
    +146:     def clear_strings_except(*languages)
    +147:       clear= (LOCALIZED_STRINGS.keys - languages)
    +148:       _clear_strings(*clear) unless clear.empty?
    +149:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns the default language +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 108
    +108:     def current_language
    +109:       GLoc::CONFIG[:default_language]
    +110:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns the charset used to store localized strings in memory. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 152
    +152:     def get_charset(lang)
    +153:       CONFIG[:internal_charset_per_lang][lang] || CONFIG[:internal_charset]
    +154:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns a GLoc configuration value. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 157
    +157:     def get_config(key)
    +158:       CONFIG[key]
    +159:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Loads the localized strings that are included in the GLoc library. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 162
    +162:     def load_gloc_default_localized_strings(override=false)
    +163:       GLoc.load_localized_strings "#{File.dirname(__FILE__)}/../lang", override
    +164:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Loads localized strings from all yml files in the specifed directory. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 167
    +167:     def load_localized_strings(dir=nil, override=true)
    +168:       _charset_required
    +169:       _get_lang_file_list(dir).each {|filename|
    +170:         
    +171:         # Load file
    +172:         raw_hash = YAML::load(File.read(filename))
    +173:         raw_hash={} unless raw_hash.kind_of?(Hash)
    +174:         filename =~ /([^\/\\]+)\.ya?ml$/
    +175:         lang = $1.to_sym
    +176:         file_charset = raw_hash['file_charset'] || UTF_8
    +177:   
    +178:         # Convert string keys to symbols
    +179:         dest_charset= get_charset(lang)
    +180:         _verbose_msg {"Reading file #{filename} [charset: #{file_charset} --> #{dest_charset}]"}
    +181:         symbol_hash = {}
    +182:         Iconv.open(dest_charset, file_charset) do |i|
    +183:           raw_hash.each {|key, value|
    +184:             symbol_hash[key.to_sym] = i.iconv(value)
    +185:           }
    +186:         end
    +187:   
    +188:         # Add strings to repos
    +189:         _add_localized_strings(lang, symbol_hash, override)
    +190:       }
    +191:       _verbose_msg :stats
    +192:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Restores a backup of GLoc’s internal state +that was made with backup_state. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 195
    +195:     def restore_state(state)
    +196:       _get_internal_state_vars.each do |o|
    +197:         o.clear
    +198:         o.send o.respond_to?(:merge!) ? :merge! : :concat, state.shift
    +199:       end
    +200:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Sets the charset used to internally store localized strings. You can set +the charset to use for a specific language or languages, or if none are +specified the charset for ALL localized strings will be set. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 205
    +205:     def set_charset(new_charset, *langs)
    +206:       CONFIG[:internal_charset_per_lang] ||= {}
    +207:       
    +208:       # Convert symbol shortcuts
    +209:       if new_charset.is_a?(Symbol)
    +210:         new_charset= case new_charset
    +211:           when :utf8, :utf_8 then UTF_8
    +212:           when :sjis, :shift_jis, :shiftjis then SHIFT_JIS
    +213:           when :eucjp, :euc_jp then EUC_JP
    +214:           else new_charset.to_s
    +215:           end
    +216:       end
    +217:       
    +218:       # Convert existing strings
    +219:       (langs.empty? ? LOCALIZED_STRINGS.keys : langs).each do |lang|
    +220:         cur_charset= get_charset(lang)
    +221:         if cur_charset && new_charset != cur_charset
    +222:           _verbose_msg {"Converting :#{lang} strings from #{cur_charset} to #{new_charset}"}
    +223:           Iconv.open(new_charset, cur_charset) do |i|
    +224:             bundle= LOCALIZED_STRINGS[lang]
    +225:             bundle.each_pair {|k,v| bundle[k]= i.iconv(v)}
    +226:           end
    +227:         end
    +228:       end
    +229:       
    +230:       # Set new charset value
    +231:       if langs.empty?
    +232:         _verbose_msg {"Setting GLoc charset for all languages to #{new_charset}"}
    +233:         CONFIG[:internal_charset]= new_charset
    +234:         CONFIG[:internal_charset_per_lang].clear
    +235:       else
    +236:         langs.each do |lang|
    +237:           _verbose_msg {"Setting GLoc charset for :#{lang} strings to #{new_charset}"}
    +238:           CONFIG[:internal_charset_per_lang][lang]= new_charset
    +239:         end
    +240:       end
    +241:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Sets GLoc configuration values. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 244
    +244:     def set_config(hash)
    +245:       CONFIG.merge! hash
    +246:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Sets the $KCODE global variable according to a specified charset, or else +the current default charset for the default language. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 250
    +250:     def set_kcode(charset=nil)
    +251:       _charset_required
    +252:       charset ||= get_charset(current_language)
    +253:       $KCODE= case charset
    +254:         when UTF_8 then 'u'
    +255:         when SHIFT_JIS then 's'
    +256:         when EUC_JP then 'e'
    +257:         else 'n'
    +258:         end
    +259:       _verbose_msg {"$KCODE set to #{$KCODE}"}
    +260:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Tries to find a valid language that is similar to the argument passed. Eg. +:en, :en_au, :EN_US are all similar languages. Returns nil if no +similar languages are found. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 265
    +265:     def similar_language(lang)
    +266:       return nil if lang.nil?
    +267:       return lang.to_sym if valid_language?(lang)
    +268:       # Check lowercase without dashes
    +269:       lang= lang.to_s.downcase.gsub('-','_')
    +270:       return LOWERCASE_LANGUAGES[lang] if LOWERCASE_LANGUAGES.has_key?(lang)
    +271:       # Check without dialect
    +272:       if lang.to_s =~ /^([a-z]+?)[^a-z].*/
    +273:         lang= $1
    +274:         return LOWERCASE_LANGUAGES[lang] if LOWERCASE_LANGUAGES.has_key?(lang)
    +275:       end
    +276:       # Check other dialects
    +277:       lang= "#{lang}_"
    +278:       LOWERCASE_LANGUAGES.keys.each {|k| return LOWERCASE_LANGUAGES[k] if k.starts_with?(lang)}
    +279:       # Nothing found
    +280:       nil
    +281:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns true if there are any localized strings for a specified +language. Note that although set_langauge nil is perfectly valid, +nil is not a valid language. +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 290
    +290:     def valid_language?(language)
    +291:       LOCALIZED_STRINGS.has_key? language.to_sym rescue false
    +292:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns an array of (currently) valid languages (ie. languages for which +localized data exists). +

    +

    [Source]

    +
    +
    +     # File lib/gloc.rb, line 284
    +284:     def valid_languages
    +285:       LOCALIZED_STRINGS.keys
    +286:     end
    +
    +
    +
    +
    + +

    Public Instance methods

    + +
    + + + + +
    +

    +Returns the instance-level current language, or if not set, returns the +class-level current language. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 77
    +77:   def current_language
    +78:     @gloc_language || self.class.current_language
    +79:   end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/ClassMethods.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/ClassMethods.html new file mode 100644 index 000000000..ba1a28ad0 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/ClassMethods.html @@ -0,0 +1,160 @@ + + + + + + Module: GLoc::ClassMethods + + + + + + + + + + +
    + + + + + + + + + + +
    ModuleGLoc::ClassMethods
    In: + + lib/gloc.rb + +
    +
    +
    + + +
    + + + +
    + +
    +

    +All classes/modules that include GLoc will also +gain these class methods. Notice that the GLoc::InstanceMethods module is also +included. +

    + +
    + + +
    + +
    +

    Methods

    + + +
    + +
    + + + +
    +

    Included Modules

    + + +
    + +
    + + + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +Returns the current language, or if not set, returns the GLoc current language. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 89
    +89:     def current_language
    +90:       @gloc_language || GLoc.current_language
    +91:     end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/Helpers.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/Helpers.html new file mode 100644 index 000000000..f3fdf63e1 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/Helpers.html @@ -0,0 +1,323 @@ + + + + + + Module: GLoc::Helpers + + + + + + + + + + +
    + + + + + + + + + + +
    ModuleGLoc::Helpers
    In: + + lib/gloc-helpers.rb + +
    +
    +
    + + +
    + + + +
    + +
    +

    +These helper methods will be included in the InstanceMethods module. +

    + +
    + + +
    + +
    +

    Methods

    + +
    + l_YesNo   + l_age   + l_date   + l_datetime   + l_datetime_short   + l_lang_name   + l_strftime   + l_time   + l_yesno   +
    +
    + +
    + + + + +
    + + + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    [Source]

    +
    +
    +    # File lib/gloc-helpers.rb, line 12
    +12:     def l_YesNo(value)         l(value ? :general_text_Yes : :general_text_No) end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +   # File lib/gloc-helpers.rb, line 6
    +6:     def l_age(age)             lwr :general_fmt_age, age end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +   # File lib/gloc-helpers.rb, line 7
    +7:     def l_date(date)           l_strftime date, :general_fmt_date end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +   # File lib/gloc-helpers.rb, line 8
    +8:     def l_datetime(date)       l_strftime date, :general_fmt_datetime end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +   # File lib/gloc-helpers.rb, line 9
    +9:     def l_datetime_short(date) l_strftime date, :general_fmt_datetime_short end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +    # File lib/gloc-helpers.rb, line 15
    +15:     def l_lang_name(lang, display_lang=nil)
    +16:       ll display_lang || current_language, "general_lang_#{lang}"
    +17:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +    # File lib/gloc-helpers.rb, line 10
    +10:     def l_strftime(date,fmt)   date.strftime l(fmt) end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +    # File lib/gloc-helpers.rb, line 11
    +11:     def l_time(time)           l_strftime time, :general_fmt_time end
    +
    +
    +
    +
    + +
    + + + + +
    +

    [Source]

    +
    +
    +    # File lib/gloc-helpers.rb, line 13
    +13:     def l_yesno(value)         l(value ? :general_text_yes : :general_text_no) end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/InstanceMethods.html b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/InstanceMethods.html new file mode 100644 index 000000000..4e15c9383 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/InstanceMethods.html @@ -0,0 +1,364 @@ + + + + + + Module: GLoc::InstanceMethods + + + + + + + + + + +
    + + + + + + + + + + +
    ModuleGLoc::InstanceMethods
    In: + + lib/gloc.rb + +
    +
    +
    + + +
    + + + +
    + +
    +

    +This module will be included in both instances and classes of GLoc includees. It is also included as class +methods in the GLoc module itself. +

    + +
    + + +
    + +
    +

    Methods

    + +
    + l   + l_has_string?   + ll   + ltry   + lwr   + lwr_   + set_language   + set_language_if_valid   +
    +
    + +
    + + + +
    +

    Included Modules

    + +
    + Helpers +
    +
    + +
    + + + + + + + + + +
    +

    Public Instance methods

    + +
    + + + + +
    +

    +Returns a localized string. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 18
    +18:     def l(symbol, *arguments)
    +19:       return GLoc._l(symbol,current_language,*arguments)
    +20:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns true if a localized string with the specified key exists. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 48
    +48:     def l_has_string?(symbol)
    +49:       return GLoc._l_has_string?(symbol,current_language)
    +50:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns a localized string in a specified language. This does not effect +current_language. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 24
    +24:     def ll(lang, symbol, *arguments)
    +25:       return GLoc._l(symbol,lang.to_sym,*arguments)
    +26:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Returns a localized string if the argument is a Symbol, else just returns +the argument. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 29
    +29:     def ltry(possible_key)
    +30:       possible_key.is_a?(Symbol) ? l(possible_key) : possible_key
    +31:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Uses the default GLoc rule to return a localized +string. See lwr_() for more info. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 35
    +35:     def lwr(symbol, *arguments)
    +36:       lwr_(:default, symbol, *arguments)
    +37:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Uses a rule to return a localized string. A rule is a function +that uses specified arguments to return a localization key prefix. The +prefix is appended to the localization key originally specified, to create +a new key which is then used to lookup a localized string. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 43
    +43:     def lwr_(rule, symbol, *arguments)
    +44:       GLoc._l("#{symbol}#{GLoc::_l_rule(rule,current_language).call(*arguments)}",current_language,*arguments)
    +45:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Sets the current language for this instance/class. Setting the language of +a class effects all instances unless the instance has its own language +defined. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 54
    +54:     def set_language(language)
    +55:       @gloc_language= language.nil? ? nil : language.to_sym
    +56:     end
    +
    +
    +
    +
    + +
    + + + + +
    +

    +Sets the current language if the language passed is a valid language. If +the language was valid, this method returns true else it will +return false. Note that nil is not a valid language. See +set_language(language) for more +info. +

    +

    [Source]

    +
    +
    +    # File lib/gloc.rb, line 62
    +62:     def set_language_if_valid(language)
    +63:       if GLoc.valid_language?(language)
    +64:         set_language(language)
    +65:         true
    +66:       else
    +67:         false
    +68:       end
    +69:     end
    +
    +
    +
    +
    + + +
    + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/created.rid b/groups/vendor/plugins/gloc-1.1.0/doc/created.rid new file mode 100644 index 000000000..eba9efa29 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/created.rid @@ -0,0 +1 @@ +Sun May 28 15:21:13 E. Australia Standard Time 2006 diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/CHANGELOG.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/CHANGELOG.html new file mode 100644 index 000000000..aec36c5bf --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/CHANGELOG.html @@ -0,0 +1,153 @@ + + + + + + File: CHANGELOG + + + + + + + + + + +
    +

    CHANGELOG

    + + + + + + + + + +
    Path:CHANGELOG +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    Version 1.1 (28 May 2006)

    +
      +
    • The charset for each and/or all languages can now be easily configured. + +
    • +
    • Added a ActionController filter that auto-detects the client language. + +
    • +
    • The rake task "sort" now merges lines that match 100%, and warns +if duplicate keys are found. + +
    • +
    • Rule support. Create flexible rules to handle issues such as pluralization. + +
    • +
    • Massive speed and stability improvements to development mode. + +
    • +
    • Added Russian strings. (Thanks to Evgeny Lineytsev) + +
    • +
    • Complete RDoc documentation. + +
    • +
    • Improved helpers. + +
    • +
    • GLoc now configurable via get_config and +set_config + +
    • +
    • Added an option to tell GLoc to output +various verbose information. + +
    • +
    • More useful functions such as set_language_if_valid, similar_language + +
    • +
    • GLoc’s entire internal state can +now be backed up and restored. + +
    • +
    +

    Version 1.0 (17 April 2006)

    +
      +
    • Initial public release. + +
    • +
    + +
    + + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/README.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/README.html new file mode 100644 index 000000000..d078659d2 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/README.html @@ -0,0 +1,480 @@ + + + + + + File: README + + + + + + + + + + +
    +

    README

    + + + + + + + + + +
    Path:README +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    About

    +

    Preface

    +

    +I originally started designing this on weekends and after work in 2005. We +started to become very interested in Rails at work and I wanted to get some +experience with ruby with before we started using it full-time. I +didn’t have very many ideas for anything interesting to create so, +because we write a lot of multilingual webapps at my company, I decided to +write a localization library. That way if my little hobby project developed +into something decent, I could at least put it to good use. And here we are +in 2006, my little hobby project has come a long way and become quite a +useful piece of software. Not only do I use it in production sites I write +at work, but I also prefer it to other existing alternatives. Therefore I +have decided to make it publicly available, and I hope that other +developers will find it useful too. +

    +

    About

    +

    +GLoc is a localization library. It +doesn’t aim to do everything l10n-related that you can imagine, but +what it does, it does very well. It was originally designed as a Rails +plugin, but can also be used for plain ruby projects. Here are a list of +its main features: +

    +
      +
    • Lightweight and efficient. + +
    • +
    • Uses file-based string bundles. Strings can also be set directly. + +
    • +
    • Intelligent, cascading language configuration. + +
    • +
    • Create flexible rules to handle issues such as pluralization. + +
    • +
    • Includes a ActionController filter that auto-detects the client language. + +
    • +
    • Works perfectly with Rails Engines and allows strings to be overridden just +as easily as controllers, models, etc. + +
    • +
    • Automatically localizes Rails functions such as distance_in_minutes, +select_month etc + +
    • +
    • Supports different charsets. You can even specify the encoding to use for +each language seperately. + +
    • +
    • Special Rails mods/helpers. + +
    • +
    +

    What does GLoc mean?

    +

    +If you’re wondering about the name "GLoc", I’m sure you’re not +alone. This project was originally just called "Localization" +which was a bit too common, so when I decided to release it I decided to +call it "Golly’s Localization Library" instead (Golly is my +nickname), and that was long and boring so I then abbreviated that to +"GLoc". What a fun story!! +

    +

    Localization helpers

    +

    +This also includes a few helpers for common situations such as displaying +localized date, time, "yes" or "no", etc. +

    +

    Rails Localization

    +

    +At the moment, unless you manually remove the require +‘gloc-rails-text’ line from init.rb, this plugin overrides +certain Rails functions to provide multilingual versions. This +automatically localizes functions such as select_date(), +distance_of_time_in_words() and more… The strings can be found in +lang/*.yml. NOTE: This is not complete. Timezones and countries are not +currently localized. +

    +

    Usage

    +

    Quickstart

    +

    +Windows users will need to first install iconv. wiki.rubyonrails.com/rails/pages/iconv +

    +
      +
    • Create a dir "#{RAILS_ROOT}/lang" + +
    • +
    • Create a file "#{RAILS_ROOT}/lang/en.yml" and write your strings. +The format is "key: string". Save it as UTF-8. If you save it in +a different encoding, add a key called file_charset (eg. +"file_charset: iso-2022-jp") + +
    • +
    • Put the following in config/environment.rb and change the values as you see +fit. The following example is for an app that uses English and Japanese, +with Japanese being the default. + +
      +  GLoc.set_config :default_language => :ja
      +  GLoc.clear_strings_except :en, :ja
      +  GLoc.set_kcode
      +  GLoc.load_localized_strings
      +
      +
    • +
    • Add ‘include GLoc’ to all +classes that will use localization. This is added to most Rails classes +automatically. + +
    • +
    • Optionally, you can set the language for models and controllers by simply +inserting set_language :en in classes and/or methods. + +
    • +
    • To use localized strings, replace text such as "Welcome" +with l(:welcome_string_key), and "Hello +#{name}." with l(:hello_string_key, name). (Of course +the strings will need to exist in your string bundle.) + +
    • +
    +

    +There is more functionality provided by this plugin, that is not +demonstrated above. Please read the API summary for details. +

    +

    API summary

    +

    +The following methods are added as both class methods and instance methods +to modules/classes that include GLoc. +They are also available as class methods of GLoc. +

    +
    +  current_language               # Returns the current language
    +  l(symbol, *arguments)          # Returns a localized string
    +  ll(lang, symbol, *arguments)   # Returns a localized string in a specific language
    +  ltry(possible_key)             # Returns a localized string if passed a Symbol, else returns the same argument passed
    +  lwr(symbol, *arguments)        # Uses the default rule to return a localized string.
    +  lwr_(rule, symbol, *arguments) # Uses a specified rule to return a localized string.
    +  l_has_string?(symbol)          # Checks if a localized string exists
    +  set_language(language)         # Sets the language for the current class or class instance
    +  set_language_if_valid(lang)    # Sets the current language if the language passed is a valid language
    +
    +

    +The GLoc module also defines the +following class methods: +

    +
    +  add_localized_strings(lang, symbol_hash, override=true) # Adds a hash of localized strings
    +  backup_state(clear=false)                               # Creates a backup of GLoc's internal state and optionally clears everything too
    +  clear_strings(*languages)                               # Removes localized strings from memory
    +  clear_strings_except(*languages)                        # Removes localized strings from memory except for those of certain specified languages
    +  get_charset(lang)                                       # Returns the charset used to store localized strings in memory
    +  get_config(key)                                         # Returns a GLoc configuration value (see below)
    +  load_localized_strings(dir=nil, override=true)          # Loads localized strings from all YML files in a given directory
    +  restore_state(state)                                    # Restores a backup of GLoc's internal state
    +  set_charset(new_charset, *langs)                        # Sets the charset used to internally store localized strings
    +  set_config(hash)                                        # Sets GLoc configuration values (see below)
    +  set_kcode(charset=nil)                                  # Sets the $KCODE global variable
    +  similar_language(language)                              # Tries to find a valid language that is similar to the argument passed
    +  valid_languages                                         # Returns an array of (currently) valid languages (ie. languages for which localized data exists)
    +  valid_language?(language)                               # Checks whether any localized strings are in memory for a given language
    +
    +

    +GLoc uses the following configuration +items. They can be accessed via get_config and +set_config. +

    +
    +  :default_cookie_name
    +  :default_language
    +  :default_param_name
    +  :raise_string_not_found_errors
    +  :verbose
    +
    +

    +The GLoc module is automatically +included in the following classes: +

    +
    +  ActionController::Base
    +  ActionMailer::Base
    +  ActionView::Base
    +  ActionView::Helpers::InstanceTag
    +  ActiveRecord::Base
    +  ActiveRecord::Errors
    +  ApplicationHelper
    +  Test::Unit::TestCase
    +
    +

    +The GLoc module also defines the +following controller filters: +

    +
    +  autodetect_language_filter
    +
    +

    +GLoc also makes the following change to +Rails: +

    +
      +
    • Views for ActionMailer are now #{view_name}_#{language}.rb rather than just +#{view_name}.rb + +
    • +
    • All ActiveRecord validation class methods now accept a localized string key +(symbol) as a :message value. + +
    • +
    • ActiveRecord::Errors.add +now accepts symbols as valid message values. At runtime these symbols are +converted to localized strings using the current_language of the base +record. + +
    • +
    • ActiveRecord::Errors.add +now accepts arrays as arguments so that printf-style strings can be +generated at runtime. This also applies to the validates_* class methods. + +
      +  Eg. validates_xxxxxx_of :name, :message => ['Your name must be at least %d characters.', MIN_LEN]
      +  Eg. validates_xxxxxx_of :name, :message => [:user_error_validation_name_too_short, MIN_LEN]
      +
      +
    • +
    • Instances of ActiveView inherit their current_language from the controller +(or mailer) creating them. + +
    • +
    +

    +This plugin also adds the following rake tasks: +

    +
    +  * gloc:sort - Sorts the keys in the lang ymls (also accepts a DIR argument)
    +
    +

    Cascading language configuration

    +

    +The language can be set at three levels: +

    +
    +  1. The default     # GLoc.get_config :default_language
    +  2. Class level     # class A; set_language :de; end
    +  3. Instance level  # b= B.new; b.set_language :zh
    +
    +

    +Instance level has the highest priority and the default has the lowest. +

    +

    +Because GLoc is included at class level +too, it becomes easy to associate languages with contexts. For example: +

    +
    +  class Student
    +    set_language :en
    +    def say_hello
    +      puts "We say #{l :hello} but our teachers say #{Teacher.l :hello}"
    +    end
    +  end
    +
    +

    Rules

    +

    +There are often situations when depending on the value of one or more +variables, the surrounding text changes. The most common case of this is +pluralization. Rather than hardcode these rules, they are completely +definable by the user so that the user can eaasily accomodate for more +complicated grammatical rules such as those found in Russian and Polish (or +so I hear). To define a rule, simply include a string in the string bundle +whose key begins with "gloc_rule" and then write ruby +code as the value. The ruby code will be converted to a Proc when the +string bundle is first read, and should return a prefix that will be +appended to the string key at runtime to point to a new string. Make sense? +Probably not… Please look at the following example and I am sure it +will all make sense. +

    +

    +Simple example (string bundle / en.yml) +

    +
    +  _gloc_rule_default: ' |n| n==1 ? "_single" : "_plural" '
    +  man_count_plural: There are %d men.
    +  man_count_single: There is 1 man.
    +
    +

    +Simple example (code) +

    +
    +  lwr(:man_count, 1)  # => There is 1 man.
    +  lwr(:man_count, 8)  # => There are 8 men.
    +
    +

    +To use rules other than the default simply call lwr_ instead of lwr, and +specify the rule. +

    +

    +Example 2 (string bundle / en.yml) +

    +
    +  _gloc_rule_default: ' |n| n==1 ? "_single" : "_plural" '
    +  _gloc_rule_custom: ' |n| return "_none" if n==0; return "_heaps" if n>100; n==1 ? "_single" : "_plural" '
    +  man_count_none: There are no men.
    +  man_count_heaps: There are heaps of men!!
    +  man_count_plural: There are %d men.
    +  man_count_single: There is 1 man.
    +
    +

    +Example 2 (code) +

    +
    +  lwr_(:custom, :man_count, 0)    # => There are no men.
    +  lwr_(:custom, :man_count, 1)    # => There is 1 man.
    +  lwr_(:custom, :man_count, 8)    # => There are 8 men.
    +  lwr_(:custom, :man_count, 150)  # => There are heaps of men!!
    +
    +

    Helpers

    +

    +GLoc includes the following helpers: +

    +
    +  l_age(age)             # Returns a localized version of an age. eg "3 years old"
    +  l_date(date)           # Returns a date in a localized format
    +  l_datetime(date)       # Returns a date+time in a localized format
    +  l_datetime_short(date) # Returns a date+time in a localized short format.
    +  l_lang_name(l,dl=nil)  # Returns the name of a language (you must supply your own strings)
    +  l_strftime(date,fmt)   # Formats a date/time in a localized format.
    +  l_time(date)           # Returns a time in a localized format
    +  l_YesNo(value)         # Returns localized string of "Yes" or "No" depending on the arg
    +  l_yesno(value)         # Returns localized string of "yes" or "no" depending on the arg
    +
    +

    Rails localization

    +

    +Not all of Rails is covered but the following functions are: +

    +
    +  distance_of_time_in_words
    +  select_day
    +  select_month
    +  select_year
    +  add_options
    +
    +

    FAQ

    +

    How do I use it in engines?

    +

    +Simply put this in your init_engine.rb +

    +
    +  GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang')
    +
    +

    +That way your engines strings will be loaded when the engine is started. +Just simply make sure that you load your application strings after you +start your engines to safely override any engine strings. +

    +

    Why am I getting an Iconv::IllegalSequence error when calling GLoc.set_charset?

    +

    +By default GLoc loads all of its default +strings at startup. For example, calling set_charset +‘iso-2022-jp’ will cause this error because Russian +strings are loaded by default, and the Russian strings use characters that +cannot be expressed in the ISO-2022-JP charset. Before calling +set_charset you should call clear_strings_except to +remove strings from any languages that you will not be using. +Alternatively, you can simply specify the language(s) as follows, +set_charset ‘iso-2022-jp’, :ja. +

    +

    How do I make GLoc ignore StringNotFoundErrors?

    +

    +Disable it as follows: +

    +
    +  GLoc.set_config :raise_string_not_found_errors => false
    +
    + +
    + + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-helpers_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-helpers_rb.html new file mode 100644 index 000000000..394b79d70 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-helpers_rb.html @@ -0,0 +1,107 @@ + + + + + + File: gloc-helpers.rb + + + + + + + + + + +
    +

    gloc-helpers.rb

    + + + + + + + + + +
    Path:lib/gloc-helpers.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-internal_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-internal_rb.html new file mode 100644 index 000000000..6d09fec7b --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-internal_rb.html @@ -0,0 +1,115 @@ + + + + + + File: gloc-internal.rb + + + + + + + + + + +
    +

    gloc-internal.rb

    + + + + + + + + + +
    Path:lib/gloc-internal.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + +
    +

    Required files

    + +
    + iconv   + gloc-version   +
    +
    + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails-text_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails-text_rb.html new file mode 100644 index 000000000..52a387218 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails-text_rb.html @@ -0,0 +1,114 @@ + + + + + + File: gloc-rails-text.rb + + + + + + + + + + +
    +

    gloc-rails-text.rb

    + + + + + + + + + +
    Path:lib/gloc-rails-text.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + +
    +

    Required files

    + +
    + date   +
    +
    + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails_rb.html new file mode 100644 index 000000000..3ae73b87b --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails_rb.html @@ -0,0 +1,114 @@ + + + + + + File: gloc-rails.rb + + + + + + + + + + +
    +

    gloc-rails.rb

    + + + + + + + + + +
    Path:lib/gloc-rails.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + +
    +

    Required files

    + +
    + gloc   +
    +
    + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-ruby_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-ruby_rb.html new file mode 100644 index 000000000..4b29e9d94 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-ruby_rb.html @@ -0,0 +1,107 @@ + + + + + + File: gloc-ruby.rb + + + + + + + + + + +
    +

    gloc-ruby.rb

    + + + + + + + + + +
    Path:lib/gloc-ruby.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-version_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-version_rb.html new file mode 100644 index 000000000..17f93aa43 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-version_rb.html @@ -0,0 +1,101 @@ + + + + + + File: gloc-version.rb + + + + + + + + + + +
    +

    gloc-version.rb

    + + + + + + + + + +
    Path:lib/gloc-version.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + + + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc_rb.html b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc_rb.html new file mode 100644 index 000000000..9e68a89cd --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc_rb.html @@ -0,0 +1,116 @@ + + + + + + File: gloc.rb + + + + + + + + + + +
    +

    gloc.rb

    + + + + + + + + + +
    Path:lib/gloc.rb +
    Last Update:Sun May 28 15:19:38 E. Australia Standard Time 2006
    +
    + + +
    + + + +
    + +
    +

    +Copyright © 2005-2006 David Barri +

    + +
    + +
    +

    Required files

    + +
    + yaml   + gloc-internal   + gloc-helpers   +
    +
    + +
    + + +
    + + + + +
    + + + + + + + + + + + +
    + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/fr_class_index.html b/groups/vendor/plugins/gloc-1.1.0/doc/fr_class_index.html new file mode 100644 index 000000000..08e0418f3 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/fr_class_index.html @@ -0,0 +1,37 @@ + + + + + + + + Classes + + + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/fr_file_index.html b/groups/vendor/plugins/gloc-1.1.0/doc/fr_file_index.html new file mode 100644 index 000000000..839e378d3 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/fr_file_index.html @@ -0,0 +1,35 @@ + + + + + + + + Files + + + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/fr_method_index.html b/groups/vendor/plugins/gloc-1.1.0/doc/fr_method_index.html new file mode 100644 index 000000000..325ed3589 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/fr_method_index.html @@ -0,0 +1,72 @@ + + + + + + + + Methods + + + + + +
    +

    Methods

    +
    + add (ActiveRecord::Errors)
    + add_localized_strings (GLoc)
    + autodetect_language_filter (ActionController::Filters::ClassMethods)
    + backup_state (GLoc)
    + clear_strings (GLoc)
    + clear_strings_except (GLoc)
    + current_language (GLoc::ClassMethods)
    + current_language (GLoc)
    + current_language (GLoc)
    + current_language (ActionView::Helpers::InstanceTag)
    + current_language (ActiveRecord::Errors)
    + distance_of_time_in_words (ActionView::Helpers::DateHelper)
    + get_charset (GLoc)
    + get_config (GLoc)
    + l (GLoc::InstanceMethods)
    + l_YesNo (GLoc::Helpers)
    + l_age (GLoc::Helpers)
    + l_date (GLoc::Helpers)
    + l_datetime (GLoc::Helpers)
    + l_datetime_short (GLoc::Helpers)
    + l_has_string? (GLoc::InstanceMethods)
    + l_lang_name (GLoc::Helpers)
    + l_strftime (GLoc::Helpers)
    + l_time (GLoc::Helpers)
    + l_yesno (GLoc::Helpers)
    + ll (GLoc::InstanceMethods)
    + load_gloc_default_localized_strings (GLoc)
    + load_localized_strings (GLoc)
    + ltry (GLoc::InstanceMethods)
    + lwr (GLoc::InstanceMethods)
    + lwr_ (GLoc::InstanceMethods)
    + new (ActionView::Base)
    + restore_state (GLoc)
    + select_day (ActionView::Helpers::DateHelper)
    + select_month (ActionView::Helpers::DateHelper)
    + select_year (ActionView::Helpers::DateHelper)
    + set_charset (GLoc)
    + set_config (GLoc)
    + set_kcode (GLoc)
    + set_language (GLoc::InstanceMethods)
    + set_language_if_valid (GLoc::InstanceMethods)
    + similar_language (GLoc)
    + valid_language? (GLoc)
    + valid_languages (GLoc)
    + validates_length_of (ActiveRecord::Validations::ClassMethods)
    + validates_size_of (ActiveRecord::Validations::ClassMethods)
    +
    +
    + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/index.html b/groups/vendor/plugins/gloc-1.1.0/doc/index.html new file mode 100644 index 000000000..f29103142 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/index.html @@ -0,0 +1,24 @@ + + + + + + + GLoc Localization Library Documentation + + + + + + + + + + + \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/doc/rdoc-style.css b/groups/vendor/plugins/gloc-1.1.0/doc/rdoc-style.css new file mode 100644 index 000000000..fbf7326af --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/doc/rdoc-style.css @@ -0,0 +1,208 @@ + +body { + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 90%; + margin: 0; + margin-left: 40px; + padding: 0; + background: white; +} + +h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } +h1 { font-size: 150%; } +h2,h3,h4 { margin-top: 1em; } + +a { background: #eef; color: #039; text-decoration: none; } +a:hover { background: #039; color: #eef; } + +/* Override the base stylesheet's Anchor inside a table cell */ +td > a { + background: transparent; + color: #039; + text-decoration: none; +} + +/* and inside a section title */ +.section-title > a { + background: transparent; + color: #eee; + text-decoration: none; +} + +/* === Structural elements =================================== */ + +div#index { + margin: 0; + margin-left: -40px; + padding: 0; + font-size: 90%; +} + + +div#index a { + margin-left: 0.7em; +} + +div#index .section-bar { + margin-left: 0px; + padding-left: 0.7em; + background: #ccc; + font-size: small; +} + + +div#classHeader, div#fileHeader { + width: auto; + color: white; + padding: 0.5em 1.5em 0.5em 1.5em; + margin: 0; + margin-left: -40px; + border-bottom: 3px solid #006; +} + +div#classHeader a, div#fileHeader a { + background: inherit; + color: white; +} + +div#classHeader td, div#fileHeader td { + background: inherit; + color: white; +} + + +div#fileHeader { + background: #057; +} + +div#classHeader { + background: #048; +} + + +.class-name-in-header { + font-size: 180%; + font-weight: bold; +} + + +div#bodyContent { + padding: 0 1.5em 0 1.5em; +} + +div#description { + padding: 0.5em 1.5em; + background: #efefef; + border: 1px dotted #999; +} + +div#description h1,h2,h3,h4,h5,h6 { + color: #125;; + background: transparent; +} + +div#validator-badges { + text-align: center; +} +div#validator-badges img { border: 0; } + +div#copyright { + color: #333; + background: #efefef; + font: 0.75em sans-serif; + margin-top: 5em; + margin-bottom: 0; + padding: 0.5em 2em; +} + + +/* === Classes =================================== */ + +table.header-table { + color: white; + font-size: small; +} + +.type-note { + font-size: small; + color: #DEDEDE; +} + +.xxsection-bar { + background: #eee; + color: #333; + padding: 3px; +} + +.section-bar { + color: #333; + border-bottom: 1px solid #999; + margin-left: -20px; +} + + +.section-title { + background: #79a; + color: #eee; + padding: 3px; + margin-top: 2em; + margin-left: -30px; + border: 1px solid #999; +} + +.top-aligned-row { vertical-align: top } +.bottom-aligned-row { vertical-align: bottom } + +/* --- Context section classes ----------------------- */ + +.context-row { } +.context-item-name { font-family: monospace; font-weight: bold; color: black; } +.context-item-value { font-size: small; color: #448; } +.context-item-desc { color: #333; padding-left: 2em; } + +/* --- Method classes -------------------------- */ +.method-detail { + background: #efefef; + padding: 0; + margin-top: 0.5em; + margin-bottom: 1em; + border: 1px dotted #ccc; +} +.method-heading { + color: black; + background: #ccc; + border-bottom: 1px solid #666; + padding: 0.2em 0.5em 0 0.5em; +} +.method-signature { color: black; background: inherit; } +.method-name { font-weight: bold; } +.method-args { font-style: italic; } +.method-description { padding: 0 0.5em 0 0.5em; } + +/* --- Source code sections -------------------- */ + +a.source-toggle { font-size: 90%; } +div.method-source-code { + background: #262626; + color: #ffdead; + margin: 1em; + padding: 0.5em; + border: 1px dashed #999; + overflow: hidden; +} + +div.method-source-code pre { color: #ffdead; overflow: hidden; } + +/* --- Ruby keyword styles --------------------- */ + +.standalone-code { background: #221111; color: #ffdead; overflow: hidden; } + +.ruby-constant { color: #7fffd4; background: transparent; } +.ruby-keyword { color: #00ffff; background: transparent; } +.ruby-ivar { color: #eedd82; background: transparent; } +.ruby-operator { color: #00ffee; background: transparent; } +.ruby-identifier { color: #ffdead; background: transparent; } +.ruby-node { color: #ffa07a; background: transparent; } +.ruby-comment { color: #b22222; font-weight: bold; background: transparent; } +.ruby-regexp { color: #ffa07a; background: transparent; } +.ruby-value { color: #7fffd4; background: transparent; } \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/init.rb b/groups/vendor/plugins/gloc-1.1.0/init.rb new file mode 100644 index 000000000..9d99acd61 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/init.rb @@ -0,0 +1,11 @@ +# Copyright (c) 2005-2006 David Barri + +require 'gloc' +require 'gloc-ruby' +require 'gloc-rails' +require 'gloc-rails-text' +require 'gloc-config' + +require 'gloc-dev' if ENV['RAILS_ENV'] == 'development' + +GLoc.load_gloc_default_localized_strings diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-config.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-config.rb new file mode 100644 index 000000000..e85b041f5 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-config.rb @@ -0,0 +1,16 @@ +# Copyright (c) 2005-2006 David Barri + +module GLoc + + private + + CONFIG= {} unless const_defined?(:CONFIG) + unless CONFIG.frozen? + CONFIG[:default_language] ||= :en + CONFIG[:default_param_name] ||= 'lang' + CONFIG[:default_cookie_name] ||= 'lang' + CONFIG[:raise_string_not_found_errors]= true unless CONFIG.has_key?(:raise_string_not_found_errors) + CONFIG[:verbose] ||= false + end + +end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-dev.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-dev.rb new file mode 100644 index 000000000..cb12b4cb3 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-dev.rb @@ -0,0 +1,97 @@ +# Copyright (c) 2005-2006 David Barri + +puts "GLoc v#{GLoc::VERSION} running in development mode. Strings can be modified at runtime." + +module GLoc + class << self + + alias :actual_add_localized_strings :add_localized_strings + def add_localized_strings(lang, symbol_hash, override=true, strings_charset=nil) + _verbose_msg {"dev::add_localized_strings #{lang}, [#{symbol_hash.size}], #{override}, #{strings_charset ? strings_charset : 'nil'}"} + STATE.push [:hash, lang, {}.merge(symbol_hash), override, strings_charset] + _force_refresh + end + + alias :actual_load_localized_strings :load_localized_strings + def load_localized_strings(dir=nil, override=true) + _verbose_msg {"dev::load_localized_strings #{dir ? dir : 'nil'}, #{override}"} + STATE.push [:dir, dir, override] + _get_lang_file_list(dir).each {|filename| FILES[filename]= nil} + end + + alias :actual_clear_strings :clear_strings + def clear_strings(*languages) + _verbose_msg {"dev::clear_strings #{languages.map{|l|l.to_s}.join(', ')}"} + STATE.push [:clear, languages.clone] + _force_refresh + end + + alias :actual_clear_strings_except :clear_strings_except + def clear_strings_except(*languages) + _verbose_msg {"dev::clear_strings_except #{languages.map{|l|l.to_s}.join(', ')}"} + STATE.push [:clear_except, languages.clone] + _force_refresh + end + + # Replace methods + [:_l, :_l_rule, :_l_has_string?, :similar_language, :valid_languages, :valid_language?].each do |m| + class_eval <<-EOB + alias :actual_#{m} :#{m} + def #{m}(*args) + _assert_gloc_strings_up_to_date + actual_#{m}(*args) + end + EOB + end + + #------------------------------------------------------------------------- + private + + STATE= [] + FILES= {} + + def _assert_gloc_strings_up_to_date + changed= @@force_refresh + + # Check if any lang files have changed + unless changed + FILES.each_pair {|f,mtime| + changed ||= (File.stat(f).mtime != mtime) + } + end + + return unless changed + puts "GLoc reloading strings..." + @@force_refresh= false + + # Update file timestamps + FILES.each_key {|f| + FILES[f]= File.stat(f).mtime + } + + # Reload strings + actual_clear_strings + STATE.each {|s| + case s[0] + when :dir then actual_load_localized_strings s[1], s[2] + when :hash then actual_add_localized_strings s[1], s[2], s[3], s[4] + when :clear then actual_clear_strings(*s[1]) + when :clear_except then actual_clear_strings_except(*s[1]) + else raise "Invalid state id: '#{s[0]}'" + end + } + _verbose_msg :stats + end + + @@force_refresh= false + def _force_refresh + @@force_refresh= true + end + + alias :super_get_internal_state_vars :_get_internal_state_vars + def _get_internal_state_vars + super_get_internal_state_vars + [ STATE, FILES ] + end + + end +end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-helpers.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-helpers.rb new file mode 100644 index 000000000..f2ceb8e3d --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-helpers.rb @@ -0,0 +1,20 @@ +# Copyright (c) 2005-2006 David Barri + +module GLoc + # These helper methods will be included in the InstanceMethods module. + module Helpers + def l_age(age) lwr :general_fmt_age, age end + def l_date(date) l_strftime date, :general_fmt_date end + def l_datetime(date) l_strftime date, :general_fmt_datetime end + def l_datetime_short(date) l_strftime date, :general_fmt_datetime_short end + def l_strftime(date,fmt) date.strftime l(fmt) end + def l_time(time) l_strftime time, :general_fmt_time end + def l_YesNo(value) l(value ? :general_text_Yes : :general_text_No) end + def l_yesno(value) l(value ? :general_text_yes : :general_text_no) end + + def l_lang_name(lang, display_lang=nil) + ll display_lang || current_language, "general_lang_#{lang}" + end + + end +end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-internal.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-internal.rb new file mode 100644 index 000000000..faed551ca --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-internal.rb @@ -0,0 +1,134 @@ +# Copyright (c) 2005-2006 David Barri + +require 'iconv' +require 'gloc-version' + +module GLoc + class GLocError < StandardError #:nodoc: + end + class InvalidArgumentsError < GLocError #:nodoc: + end + class InvalidKeyError < GLocError #:nodoc: + end + class RuleNotFoundError < GLocError #:nodoc: + end + class StringNotFoundError < GLocError #:nodoc: + end + + class << self + private + + def _add_localized_data(lang, symbol_hash, override, target) #:nodoc: + lang= lang.to_sym + if override + target[lang] ||= {} + target[lang].merge!(symbol_hash) + else + symbol_hash.merge!(target[lang]) if target[lang] + target[lang]= symbol_hash + end + end + + def _add_localized_strings(lang, symbol_hash, override=true, strings_charset=nil) #:nodoc: + _charset_required + + # Convert all incoming strings to the gloc charset + if strings_charset + Iconv.open(get_charset(lang), strings_charset) do |i| + symbol_hash.each_pair {|k,v| symbol_hash[k]= i.iconv(v)} + end + end + + # Convert rules + rules= {} + old_kcode= $KCODE + begin + $KCODE= 'u' + Iconv.open(UTF_8, get_charset(lang)) do |i| + symbol_hash.each {|k,v| + if /^_gloc_rule_(.+)$/ =~ k.to_s + v= i.iconv(v) if v + v= '""' if v.nil? + rules[$1.to_sym]= eval "Proc.new do #{v} end" + end + } + end + ensure + $KCODE= old_kcode + end + rules.keys.each {|k| symbol_hash.delete "_gloc_rule_#{k}".to_sym} + + # Add new localized data + LOWERCASE_LANGUAGES[lang.to_s.downcase]= lang + _add_localized_data(lang, symbol_hash, override, LOCALIZED_STRINGS) + _add_localized_data(lang, rules, override, RULES) + end + + def _charset_required #:nodoc: + set_charset UTF_8 unless CONFIG[:internal_charset] + end + + def _get_internal_state_vars + [ CONFIG, LOCALIZED_STRINGS, RULES, LOWERCASE_LANGUAGES ] + end + + def _get_lang_file_list(dir) #:nodoc: + dir= File.join(RAILS_ROOT,'{.,vendor/plugins/*}','lang') if dir.nil? + Dir[File.join(dir,'*.{yaml,yml}')] + end + + def _l(symbol, language, *arguments) #:nodoc: + symbol= symbol.to_sym if symbol.is_a?(String) + raise InvalidKeyError.new("Symbol or String expected as key.") unless symbol.kind_of?(Symbol) + + translation= LOCALIZED_STRINGS[language][symbol] rescue nil + if translation.nil? + raise StringNotFoundError.new("There is no key called '#{symbol}' in the #{language} strings.") if CONFIG[:raise_string_not_found_errors] + translation= symbol.to_s + end + + begin + return translation % arguments + rescue => e + raise InvalidArgumentsError.new("Translation value #{translation.inspect} with arguments #{arguments.inspect} caused error '#{e.message}'") + end + end + + def _l_has_string?(symbol,lang) #:nodoc: + symbol= symbol.to_sym if symbol.is_a?(String) + LOCALIZED_STRINGS[lang].has_key?(symbol.to_sym) rescue false + end + + def _l_rule(symbol,lang) #:nodoc: + symbol= symbol.to_sym if symbol.is_a?(String) + raise InvalidKeyError.new("Symbol or String expected as key.") unless symbol.kind_of?(Symbol) + + r= RULES[lang][symbol] rescue nil + raise RuleNotFoundError.new("There is no rule called '#{symbol}' in the #{lang} rules.") if r.nil? + r + end + + def _verbose_msg(type=nil) + return unless CONFIG[:verbose] + x= case type + when :stats + x= valid_languages.map{|l| ":#{l}(#{LOCALIZED_STRINGS[l].size}/#{RULES[l].size})"}.sort.join(', ') + "Current stats -- #{x}" + else + yield + end + puts "[GLoc] #{x}" + end + + public :_l, :_l_has_string?, :_l_rule + end + + private + + unless const_defined?(:LOCALIZED_STRINGS) + LOCALIZED_STRINGS= {} + RULES= {} + LOWERCASE_LANGUAGES= {} + end + +end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb new file mode 100644 index 000000000..f437410dc --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb @@ -0,0 +1,150 @@ +# Copyright (c) 2005-2006 David Barri + +require 'date' + +module ActionView #:nodoc: + module Helpers #:nodoc: + module DateHelper + + unless const_defined?(:LOCALIZED_HELPERS) + LOCALIZED_HELPERS= true + LOCALIZED_MONTHNAMES = {} + LOCALIZED_ABBR_MONTHNAMES = {} + end + + # This method uses current_language to return a localized string. + def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) + from_time = from_time.to_time if from_time.respond_to?(:to_time) + to_time = to_time.to_time if to_time.respond_to?(:to_time) + distance_in_minutes = (((to_time - from_time).abs)/60).round + distance_in_seconds = ((to_time - from_time).abs).round + + case distance_in_minutes + when 0..1 + return (distance_in_minutes==0) ? l(:actionview_datehelper_time_in_words_minute_less_than) : l(:actionview_datehelper_time_in_words_minute_single) unless include_seconds + case distance_in_seconds + when 0..5 then lwr(:actionview_datehelper_time_in_words_second_less_than, 5) + when 6..10 then lwr(:actionview_datehelper_time_in_words_second_less_than, 10) + when 11..20 then lwr(:actionview_datehelper_time_in_words_second_less_than, 20) + when 21..40 then l(:actionview_datehelper_time_in_words_minute_half) + when 41..59 then l(:actionview_datehelper_time_in_words_minute_less_than) + else l(:actionview_datehelper_time_in_words_minute) + end + + when 2..45 then lwr(:actionview_datehelper_time_in_words_minute, distance_in_minutes) + when 46..90 then l(:actionview_datehelper_time_in_words_hour_about_single) + when 90..1440 then lwr(:actionview_datehelper_time_in_words_hour_about, (distance_in_minutes.to_f / 60.0).round) + when 1441..2880 then lwr(:actionview_datehelper_time_in_words_day, 1) + else lwr(:actionview_datehelper_time_in_words_day, (distance_in_minutes / 1440).round) + end + end + + # This method has been modified so that a localized string can be appended to the day numbers. + def select_day(date, options = {}) + day_options = [] + prefix = l :actionview_datehelper_select_day_prefix + + 1.upto(31) do |day| + day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # This method has been modified so that + # * the month names are localized. + # * it uses options: :min_date, :max_date, :start_month, :end_month + # * a localized string can be appended to the month numbers when the :use_month_numbers option is specified. + def select_month(date, options = {}) + unless LOCALIZED_MONTHNAMES.has_key?(current_language) + LOCALIZED_MONTHNAMES[current_language] = [''] + l(:actionview_datehelper_select_month_names).split(',') + LOCALIZED_ABBR_MONTHNAMES[current_language] = [''] + l(:actionview_datehelper_select_month_names_abbr).split(',') + end + + month_options = [] + month_names = options[:use_short_month] ? LOCALIZED_ABBR_MONTHNAMES[current_language] : LOCALIZED_MONTHNAMES[current_language] + + if options.has_key?(:min_date) && options.has_key?(:max_date) + if options[:min_date].year == options[:max_date].year + start_month, end_month = options[:min_date].month, options[:max_date].month + end + end + start_month = (options[:start_month] || 1) unless start_month + end_month = (options[:end_month] || 12) unless end_month + prefix = l :actionview_datehelper_select_month_prefix + + start_month.upto(end_month) do |month_number| + month_name = if options[:use_month_numbers] + "#{month_number}#{prefix}" + elsif options[:add_month_numbers] + month_number.to_s + ' - ' + month_names[month_number] + else + month_names[month_number] + end + + month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # This method has been modified so that + # * it uses options: :min_date, :max_date + # * a localized string can be appended to the years numbers. + def select_year(date, options = {}) + year_options = [] + y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year + + start_year = options.has_key?(:min_date) ? options[:min_date].year : (options[:start_year] || y-5) + end_year = options.has_key?(:max_date) ? options[:max_date].year : (options[:end_year] || y+5) + step_val = start_year < end_year ? 1 : -1 + prefix = l :actionview_datehelper_select_year_prefix + + start_year.step(end_year, step_val) do |year| + year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ? + %(\n) : + %(\n) + ) + end + + select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) + end + + # added by JP Lang + # select_html is a rails private method and changed in 1.2 + # implementation added here for compatibility + def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false) + select_html = %(\n" + end + end + + # The private method add_options is overridden so that "Please select" is localized. + class InstanceTag + private + + def add_options(option_tags, options, value = nil) + option_tags = "\n" + option_tags if options[:include_blank] + + if value.blank? && options[:prompt] + ("\n") + option_tags + else + option_tags + end + end + + end + end +end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb new file mode 100644 index 000000000..aa65991b0 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb @@ -0,0 +1,252 @@ +# Copyright (c) 2005-2006 David Barri + +require 'gloc' + +module ActionController #:nodoc: + class Base #:nodoc: + include GLoc + end + module Filters #:nodoc: + module ClassMethods + + # This filter attempts to auto-detect the clients desired language. + # It first checks the params, then a cookie and then the HTTP_ACCEPT_LANGUAGE + # request header. If a language is found to match or be similar to a currently + # valid language, then it sets the current_language of the controller. + # + # class ExampleController < ApplicationController + # set_language :en + # autodetect_language_filter :except => 'monkey', :on_no_lang => :lang_not_autodetected_callback + # autodetect_language_filter :only => 'monkey', :check_cookie => 'monkey_lang', :check_accept_header => false + # ... + # def lang_not_autodetected_callback + # redirect_to somewhere + # end + # end + # + # The args for this filter are exactly the same the arguments of + # before_filter with the following exceptions: + # * :check_params -- If false, then params will not be checked for a language. + # If a String, then this will value will be used as the name of the param. + # * :check_cookie -- If false, then the cookie will not be checked for a language. + # If a String, then this will value will be used as the name of the cookie. + # * :check_accept_header -- If false, then HTTP_ACCEPT_LANGUAGE will not be checked for a language. + # * :on_set_lang -- You can specify the name of a callback function to be called when the language + # is successfully detected and set. The param must be a Symbol or a String which is the name of the function. + # The callback function must accept one argument (the language) and must be instance level. + # * :on_no_lang -- You can specify the name of a callback function to be called when the language + # couldn't be detected automatically. The param must be a Symbol or a String which is the name of the function. + # The callback function must be instance level. + # + # You override the default names of the param or cookie by calling GLoc.set_config :default_param_name => 'new_param_name' + # and GLoc.set_config :default_cookie_name => 'new_cookie_name'. + def autodetect_language_filter(*args) + options= args.last.is_a?(Hash) ? args.last : {} + x= 'Proc.new { |c| l= nil;' + # :check_params + unless (v= options.delete(:check_params)) == false + name= v ? ":#{v}" : 'GLoc.get_config(:default_param_name)' + x << "l ||= GLoc.similar_language(c.params[#{name}]);" + end + # :check_cookie + unless (v= options.delete(:check_cookie)) == false + name= v ? ":#{v}" : 'GLoc.get_config(:default_cookie_name)' + x << "l ||= GLoc.similar_language(c.send(:cookies)[#{name}]);" + end + # :check_accept_header + unless options.delete(:check_accept_header) == false + x << %< + unless l + a= c.request.env['HTTP_ACCEPT_LANGUAGE'].split(/,|;/) rescue nil + a.each {|x| l ||= GLoc.similar_language(x)} if a + end; > + end + # Set language + x << 'ret= true;' + x << 'if l; c.set_language(l); c.headers[\'Content-Language\']= l.to_s; ' + if options.has_key?(:on_set_lang) + x << "ret= c.#{options.delete(:on_set_lang)}(l);" + end + if options.has_key?(:on_no_lang) + x << "else; ret= c.#{options.delete(:on_no_lang)};" + end + x << 'end; ret }' + + # Create filter + block= eval x + before_filter(*args, &block) + end + + end + end +end + +# ============================================================================== + +module ActionMailer #:nodoc: + # In addition to including GLoc, render_message is also overridden so + # that mail templates contain the current language at the end of the file. + # Eg. deliver_hello will render hello_en.rhtml. + class Base + include GLoc + private + alias :render_message_without_gloc :render_message + def render_message(method_name, body) + template = File.exist?("#{template_path}/#{method_name}_#{current_language}.rhtml") ? "#{method_name}_#{current_language}" : "#{method_name}" + render_message_without_gloc(template, body) + end + end +end + +# ============================================================================== + +module ActionView #:nodoc: + # initialize is overridden so that new instances of this class inherit + # the current language of the controller. + class Base + include GLoc + + alias :initialize_without_gloc :initialize + def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil) + initialize_without_gloc(base_path, assigns_for_first_render, controller) + set_language controller.current_language unless controller.nil? + end + end + + module Helpers #:nodoc: + class InstanceTag + include GLoc + # Inherits the current language from the template object. + def current_language + @template_object.current_language + end + end + end +end + +# ============================================================================== + +module ActiveRecord #:nodoc: + class Base #:nodoc: + include GLoc + end + +# class Errors +# include GLoc +# alias :add_without_gloc :add +# # The GLoc version of this method provides two extra features +# # * If msg is a string, it will be considered a GLoc string key. +# # * If msg is an array, the first element will be considered +# # the string and the remaining elements will be considered arguments for the +# # string. Eg. ['Hi %s.','John'] +# def add(attribute, msg= @@default_error_messages[:invalid]) +# if msg.is_a?(Array) +# args= msg.clone +# msg= args.shift +# args= nil if args.empty? +# end +# msg= ltry(msg) +# msg= msg % args unless args.nil? +# add_without_gloc(attribute, msg) +# end +# # Inherits the current language from the base record. +# def current_language +# @base.current_language +# end +# end + + class Errors + include GLoc + + def full_messages + full_messages = [] + + @errors.each_key do |attr| + @errors[attr].each do |msg| + next if msg.nil? + + if attr == "base" + full_messages << (msg.is_a?(Symbol) ? l(msg) : msg) + else + full_messages << @base.class.human_attribute_name(attr) + " " + (msg.is_a?(Symbol) ? l(msg) : msg) + end + end + end + full_messages + end + end + + module Validations #:nodoc: + module ClassMethods + # The default Rails version of this function creates an error message and then + # passes it to ActiveRecord.Errors. + # The GLoc version of this method, sends an array to ActiveRecord.Errors that will + # be turned into a string by ActiveRecord.Errors which in turn allows for the message + # of this validation function to be a GLoc string key. + def validates_length_of(*attrs) + # Merge given options with defaults. + options = { + :too_long => ActiveRecord::Errors.default_error_messages[:too_long], + :too_short => ActiveRecord::Errors.default_error_messages[:too_short], + :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] + }.merge(DEFAULT_VALIDATION_OPTIONS) + options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash) + + # Ensure that one and only one range option is specified. + range_options = ALL_RANGE_OPTIONS & options.keys + case range_options.size + when 0 + raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.' + when 1 + # Valid number of options; do nothing. + else + raise ArgumentError, 'Too many range options specified. Choose only one.' + end + + # Get range option and value. + option = range_options.first + option_value = options[range_options.first] + + case option + when :within, :in + raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) + + too_short = [options[:too_short] , option_value.begin] + too_long = [options[:too_long] , option_value.end ] + + validates_each(attrs, options) do |record, attr, value| + if value.nil? or value.split(//).size < option_value.begin + record.errors.add(attr, too_short) + elsif value.split(//).size > option_value.end + record.errors.add(attr, too_long) + end + end + when :is, :minimum, :maximum + raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 + + # Declare different validations per option. + validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } + message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } + + message = [(options[:message] || options[message_options[option]]) , option_value] + + validates_each(attrs, options) do |record, attr, value| + if value.kind_of?(String) + record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] + else + record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + end + end + end + end + + alias_method :validates_size_of, :validates_length_of + end + end +end + +# ============================================================================== + +module ApplicationHelper #:nodoc: + include GLoc +end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-ruby.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-ruby.rb new file mode 100644 index 000000000..f96ab6cf9 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-ruby.rb @@ -0,0 +1,7 @@ +# Copyright (c) 2005-2006 David Barri + +module Test # :nodoc: + module Unit # :nodoc: + class TestCase # :nodoc: + include GLoc +end; end; end diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-version.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-version.rb new file mode 100644 index 000000000..91afcf482 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-version.rb @@ -0,0 +1,12 @@ +module GLoc + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 1 + TINY = nil + + STRING= [MAJOR, MINOR, TINY].delete_if{|x|x.nil?}.join('.') + def self.to_s; STRING end + end +end + +puts "NOTICE: You are using a dev version of GLoc." if GLoc::VERSION::TINY == 'DEV' \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc.rb new file mode 100644 index 000000000..bcad0ed9b --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc.rb @@ -0,0 +1,294 @@ +# Copyright (c) 2005-2006 David Barri + +require 'yaml' +require 'gloc-internal' +require 'gloc-helpers' + +module GLoc + UTF_8= 'utf-8' + SHIFT_JIS= 'sjis' + EUC_JP= 'euc-jp' + + # This module will be included in both instances and classes of GLoc includees. + # It is also included as class methods in the GLoc module itself. + module InstanceMethods + include Helpers + + # Returns a localized string. + def l(symbol, *arguments) + return GLoc._l(symbol,current_language,*arguments) + end + + # Returns a localized string in a specified language. + # This does not effect current_language. + def ll(lang, symbol, *arguments) + return GLoc._l(symbol,lang.to_sym,*arguments) + end + + # Returns a localized string if the argument is a Symbol, else just returns the argument. + def ltry(possible_key) + possible_key.is_a?(Symbol) ? l(possible_key) : possible_key + end + + # Uses the default GLoc rule to return a localized string. + # See lwr_() for more info. + def lwr(symbol, *arguments) + lwr_(:default, symbol, *arguments) + end + + # Uses a rule to return a localized string. + # A rule is a function that uses specified arguments to return a localization key prefix. + # The prefix is appended to the localization key originally specified, to create a new key which + # is then used to lookup a localized string. + def lwr_(rule, symbol, *arguments) + GLoc._l("#{symbol}#{GLoc::_l_rule(rule,current_language).call(*arguments)}",current_language,*arguments) + end + + # Returns true if a localized string with the specified key exists. + def l_has_string?(symbol) + return GLoc._l_has_string?(symbol,current_language) + end + + # Sets the current language for this instance/class. + # Setting the language of a class effects all instances unless the instance has its own language defined. + def set_language(language) + @gloc_language= language.nil? ? nil : language.to_sym + end + + # Sets the current language if the language passed is a valid language. + # If the language was valid, this method returns true else it will return false. + # Note that nil is not a valid language. + # See set_language(language) for more info. + def set_language_if_valid(language) + if GLoc.valid_language?(language) + set_language(language) + true + else + false + end + end + end + + #--------------------------------------------------------------------------- + # Instance + + include ::GLoc::InstanceMethods + # Returns the instance-level current language, or if not set, returns the class-level current language. + def current_language + @gloc_language || self.class.current_language + end + + #--------------------------------------------------------------------------- + # Class + + # All classes/modules that include GLoc will also gain these class methods. + # Notice that the GLoc::InstanceMethods module is also included. + module ClassMethods + include ::GLoc::InstanceMethods + # Returns the current language, or if not set, returns the GLoc current language. + def current_language + @gloc_language || GLoc.current_language + end + end + + def self.included(target) #:nodoc: + super + class << target + include ::GLoc::ClassMethods + end + end + + #--------------------------------------------------------------------------- + # GLoc module + + class << self + include ::GLoc::InstanceMethods + + # Returns the default language + def current_language + GLoc::CONFIG[:default_language] + end + + # Adds a collection of localized strings to the in-memory string store. + def add_localized_strings(lang, symbol_hash, override=true, strings_charset=nil) + _verbose_msg {"Adding #{symbol_hash.size} #{lang} strings."} + _add_localized_strings(lang, symbol_hash, override, strings_charset) + _verbose_msg :stats + end + + # Creates a backup of the internal state of GLoc (ie. strings, langs, rules, config) + # and optionally clears everything. + def backup_state(clear=false) + s= _get_internal_state_vars.map{|o| o.clone} + _get_internal_state_vars.each{|o| o.clear} if clear + s + end + + # Removes all localized strings from memory, either of a certain language (or languages), + # or entirely. + def clear_strings(*languages) + if languages.empty? + _verbose_msg {"Clearing all strings"} + LOCALIZED_STRINGS.clear + LOWERCASE_LANGUAGES.clear + else + languages.each {|l| + _verbose_msg {"Clearing :#{l} strings"} + l= l.to_sym + LOCALIZED_STRINGS.delete l + LOWERCASE_LANGUAGES.each_pair {|k,v| LOWERCASE_LANGUAGES.delete k if v == l} + } + end + end + alias :_clear_strings :clear_strings + + # Removes all localized strings from memory, except for those of certain specified languages. + def clear_strings_except(*languages) + clear= (LOCALIZED_STRINGS.keys - languages) + _clear_strings(*clear) unless clear.empty? + end + + # Returns the charset used to store localized strings in memory. + def get_charset(lang) + CONFIG[:internal_charset_per_lang][lang] || CONFIG[:internal_charset] + end + + # Returns a GLoc configuration value. + def get_config(key) + CONFIG[key] + end + + # Loads the localized strings that are included in the GLoc library. + def load_gloc_default_localized_strings(override=false) + GLoc.load_localized_strings "#{File.dirname(__FILE__)}/../lang", override + end + + # Loads localized strings from all yml files in the specifed directory. + def load_localized_strings(dir=nil, override=true) + _charset_required + _get_lang_file_list(dir).each {|filename| + + # Load file + raw_hash = YAML::load(File.read(filename)) + raw_hash={} unless raw_hash.kind_of?(Hash) + filename =~ /([^\/\\]+)\.ya?ml$/ + lang = $1.to_sym + file_charset = raw_hash['file_charset'] || UTF_8 + + # Convert string keys to symbols + dest_charset= get_charset(lang) + _verbose_msg {"Reading file #{filename} [charset: #{file_charset} --> #{dest_charset}]"} + symbol_hash = {} + Iconv.open(dest_charset, file_charset) do |i| + raw_hash.each {|key, value| + symbol_hash[key.to_sym] = i.iconv(value) + } + end + + # Add strings to repos + _add_localized_strings(lang, symbol_hash, override) + } + _verbose_msg :stats + end + + # Restores a backup of GLoc's internal state that was made with backup_state. + def restore_state(state) + _get_internal_state_vars.each do |o| + o.clear + o.send o.respond_to?(:merge!) ? :merge! : :concat, state.shift + end + end + + # Sets the charset used to internally store localized strings. + # You can set the charset to use for a specific language or languages, + # or if none are specified the charset for ALL localized strings will be set. + def set_charset(new_charset, *langs) + CONFIG[:internal_charset_per_lang] ||= {} + + # Convert symbol shortcuts + if new_charset.is_a?(Symbol) + new_charset= case new_charset + when :utf8, :utf_8 then UTF_8 + when :sjis, :shift_jis, :shiftjis then SHIFT_JIS + when :eucjp, :euc_jp then EUC_JP + else new_charset.to_s + end + end + + # Convert existing strings + (langs.empty? ? LOCALIZED_STRINGS.keys : langs).each do |lang| + cur_charset= get_charset(lang) + if cur_charset && new_charset != cur_charset + _verbose_msg {"Converting :#{lang} strings from #{cur_charset} to #{new_charset}"} + Iconv.open(new_charset, cur_charset) do |i| + bundle= LOCALIZED_STRINGS[lang] + bundle.each_pair {|k,v| bundle[k]= i.iconv(v)} + end + end + end + + # Set new charset value + if langs.empty? + _verbose_msg {"Setting GLoc charset for all languages to #{new_charset}"} + CONFIG[:internal_charset]= new_charset + CONFIG[:internal_charset_per_lang].clear + else + langs.each do |lang| + _verbose_msg {"Setting GLoc charset for :#{lang} strings to #{new_charset}"} + CONFIG[:internal_charset_per_lang][lang]= new_charset + end + end + end + + # Sets GLoc configuration values. + def set_config(hash) + CONFIG.merge! hash + end + + # Sets the $KCODE global variable according to a specified charset, or else the + # current default charset for the default language. + def set_kcode(charset=nil) + _charset_required + charset ||= get_charset(current_language) + $KCODE= case charset + when UTF_8 then 'u' + when SHIFT_JIS then 's' + when EUC_JP then 'e' + else 'n' + end + _verbose_msg {"$KCODE set to #{$KCODE}"} + end + + # Tries to find a valid language that is similar to the argument passed. + # Eg. :en, :en_au, :EN_US are all similar languages. + # Returns nil if no similar languages are found. + def similar_language(lang) + return nil if lang.nil? + return lang.to_sym if valid_language?(lang) + # Check lowercase without dashes + lang= lang.to_s.downcase.gsub('-','_') + return LOWERCASE_LANGUAGES[lang] if LOWERCASE_LANGUAGES.has_key?(lang) + # Check without dialect + if lang.to_s =~ /^([a-z]+?)[^a-z].*/ + lang= $1 + return LOWERCASE_LANGUAGES[lang] if LOWERCASE_LANGUAGES.has_key?(lang) + end + # Check other dialects + lang= "#{lang}_" + LOWERCASE_LANGUAGES.keys.each {|k| return LOWERCASE_LANGUAGES[k] if k.starts_with?(lang)} + # Nothing found + nil + end + + # Returns an array of (currently) valid languages (ie. languages for which localized data exists). + def valid_languages + LOCALIZED_STRINGS.keys + end + + # Returns true if there are any localized strings for a specified language. + # Note that although set_langauge nil is perfectly valid, nil is not a valid language. + def valid_language?(language) + LOCALIZED_STRINGS.has_key? language.to_sym rescue false + end + end +end diff --git a/groups/vendor/plugins/gloc-1.1.0/tasks/gloc.rake b/groups/vendor/plugins/gloc-1.1.0/tasks/gloc.rake new file mode 100644 index 000000000..88f3472ec --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/tasks/gloc.rake @@ -0,0 +1,53 @@ +namespace :gloc do + desc 'Sorts the keys in the lang ymls' + task :sort do + dir = ENV['DIR'] || '{.,vendor/plugins/*}/lang' + puts "Processing directory #{dir}" + files = Dir.glob(File.join(dir,'*.{yaml,yml}')) + puts 'No files found.' if files.empty? + files.each {|file| + puts "Sorting file: #{file}" + header = [] + content = IO.readlines(file) + content.each {|line| line.gsub!(/[\s\r\n\t]+$/,'')} + content.delete_if {|line| line==''} + tmp= [] + content.each {|x| tmp << x unless tmp.include?(x)} + content= tmp + header << content.shift if !content.empty? && content[0] =~ /^file_charset:/ + content.sort! + filebak = "#{file}.bak" + File.rename file, filebak + File.open(file, 'w') {|fout| fout << header.join("\n") << content.join("\n") << "\n"} + File.delete filebak + # Report duplicates + count= {} + content.map {|x| x.gsub(/:.+$/, '') }.each {|x| count[x] ||= 0; count[x] += 1} + count.delete_if {|k,v|v==1} + puts count.keys.sort.map{|x|" WARNING: Duplicate key '#{x}' (#{count[x]} occurances)"}.join("\n") unless count.empty? + } + end + + desc 'Updates language files based on em.yml content' + task :update do + dir = ENV['DIR'] || './lang' + + en_strings = {} + en_file = File.open(File.join(dir,'en.yml'), 'r') + en_file.each_line {|line| en_strings[$1] = $2 if line =~ %r{^([\w_]+):\s(.+)$} } + en_file.close + + files = Dir.glob(File.join(dir,'*.{yaml,yml}')) + files.each do |file| + puts "Updating file #{file}" + keys = IO.readlines(file).collect {|line| $1 if line =~ %r{^([\w_]+):\s(.+)$} }.compact + lang = File.open(file, 'a') + en_strings.each do |key, str| + next if keys.include?(key) + puts "added: #{key}" + lang << "#{key}: #{str}\n" + end + lang.close + end + end +end \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/gloc_rails_test.rb b/groups/vendor/plugins/gloc-1.1.0/test/gloc_rails_test.rb new file mode 100644 index 000000000..4cb232904 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/gloc_rails_test.rb @@ -0,0 +1,118 @@ +# Copyright (c) 2005-2006 David Barri + +$LOAD_PATH.push File.join(File.dirname(__FILE__),'..','lib') +require "#{File.dirname(__FILE__)}/../../../../test/test_helper" +require "#{File.dirname(__FILE__)}/../init" + +class GLocRailsTestController < ActionController::Base + autodetect_language_filter :only => :auto, :on_set_lang => :called_when_set, :on_no_lang => :called_when_bad + autodetect_language_filter :only => :auto2, :check_accept_header => false, :check_params => 'xx' + autodetect_language_filter :only => :auto3, :check_cookie => false + autodetect_language_filter :only => :auto4, :check_cookie => 'qwe', :check_params => false + def rescue_action(e) raise e end + def auto; render :text => 'auto'; end + def auto2; render :text => 'auto'; end + def auto3; render :text => 'auto'; end + def auto4; render :text => 'auto'; end + attr_accessor :callback_set, :callback_bad + def called_when_set(l) @callback_set ||= 0; @callback_set += 1 end + def called_when_bad; @callback_bad ||= 0; @callback_bad += 1 end +end + +class GLocRailsTest < Test::Unit::TestCase + + def setup + @lstrings = GLoc::LOCALIZED_STRINGS.clone + @old_config= GLoc::CONFIG.clone + begin_new_request + end + + def teardown + GLoc.clear_strings + GLoc::LOCALIZED_STRINGS.merge! @lstrings + GLoc::CONFIG.merge! @old_config + end + + def begin_new_request + @controller = GLocRailsTestController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_autodetect_language + GLoc::CONFIG[:default_language]= :def + GLoc::CONFIG[:default_param_name] = 'plang' + GLoc::CONFIG[:default_cookie_name] = 'clang' + GLoc.clear_strings + GLoc.add_localized_strings :en, :a => 'a' + GLoc.add_localized_strings :en_au, :a => 'a' + GLoc.add_localized_strings :en_US, :a => 'a' + GLoc.add_localized_strings :Ja, :a => 'a' + GLoc.add_localized_strings :ZH_HK, :a => 'a' + + # default + subtest_autodetect_language :def, nil, nil, nil + subtest_autodetect_language :def, 'its', 'all', 'bullshit,man;q=zxc' + # simple + subtest_autodetect_language :en_au, 'en_au', nil, nil + subtest_autodetect_language :en_US, nil, 'en_us', nil + subtest_autodetect_language :Ja, nil, nil, 'ja' + # priority + subtest_autodetect_language :Ja, 'ja', 'en_us', 'qwe_ja,zh,monkey_en;q=0.5' + subtest_autodetect_language :en_US, 'why', 'en_us', 'qwe_ja,zh,monkey_en;q=0.5' + subtest_autodetect_language :Ja, nil, nil, 'qwe_en,JA,zh,monkey_en;q=0.5' + # dashes to underscores in accept string + subtest_autodetect_language :en_au, 'monkey', nil, 'de,EN-Au' + # remove dialect + subtest_autodetect_language :en, nil, 'en-bullshit', nil + subtest_autodetect_language :en, 'monkey', nil, 'de,EN-NZ,ja' + # different dialect + subtest_autodetect_language :ZH_HK, 'zh', nil, 'de,EN-NZ,ja' + subtest_autodetect_language :ZH_HK, 'monkey', 'zh', 'de,EN-NZ,ja' + + # Check param/cookie names use defaults + GLoc::CONFIG[:default_param_name] = 'p_lang' + GLoc::CONFIG[:default_cookie_name] = 'c_lang' + # :check_params + subtest_autodetect_language :def, 'en_au', nil, nil + subtest_autodetect_language :en_au, {:p_lang => 'en_au'}, nil, nil + # :check_cookie + subtest_autodetect_language :def, nil, 'en_us', nil + subtest_autodetect_language :en_US, nil, {:c_lang => 'en_us'}, nil + GLoc::CONFIG[:default_param_name] = 'plang' + GLoc::CONFIG[:default_cookie_name] = 'clang' + + # autodetect_language_filter :only => :auto2, :check_accept_header => false, :check_params => 'xx' + subtest_autodetect_language :def, 'ja', nil, 'en_US', :auto2 + subtest_autodetect_language :Ja, {:xx => 'ja'}, nil, 'en_US', :auto2 + subtest_autodetect_language :en_au, 'ja', 'en_au', 'en_US', :auto2 + + # autodetect_language_filter :only => :auto3, :check_cookie => false + subtest_autodetect_language :Ja, 'ja', 'en_us', 'qwe_ja,zh,monkey_en;q=0.5', :auto3 + subtest_autodetect_language :ZH_HK, 'hehe', 'en_us', 'qwe_ja,zh,monkey_en;q=0.5', :auto3 + + # autodetect_language_filter :only => :auto4, :check_cookie => 'qwe', :check_params => false + subtest_autodetect_language :def, 'ja', 'en_us', nil, :auto4 + subtest_autodetect_language :ZH_HK, 'ja', 'en_us', 'qwe_ja,zh,monkey_en;q=0.5', :auto4 + subtest_autodetect_language :en_US, 'ja', {:qwe => 'en_us'}, 'ja', :auto4 + end + + def subtest_autodetect_language(expected,params,cookie,accept, action=:auto) + begin_new_request + params= {'plang' => params} if params.is_a?(String) + params ||= {} + if cookie + cookie={'clang' => cookie} unless cookie.is_a?(Hash) + cookie.each_pair {|k,v| @request.cookies[k.to_s]= CGI::Cookie.new(k.to_s,v)} + end + @request.env['HTTP_ACCEPT_LANGUAGE']= accept + get action, params + assert_equal expected, @controller.current_language + if action == :auto + s,b = expected != :def ? [1,nil] : [nil,1] + assert_equal s, @controller.callback_set + assert_equal b, @controller.callback_bad + end + end + +end \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/gloc_test.rb b/groups/vendor/plugins/gloc-1.1.0/test/gloc_test.rb new file mode 100644 index 000000000..a39d5c41c --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/gloc_test.rb @@ -0,0 +1,433 @@ +# Copyright (c) 2005-2006 David Barri + +$LOAD_PATH.push File.join(File.dirname(__FILE__),'..','lib') +require 'gloc' +require 'gloc-ruby' +require 'gloc-config' +require 'gloc-rails-text' +require File.join(File.dirname(__FILE__),'lib','rails-time_ext') unless 3.respond_to?(:days) +require File.join(File.dirname(__FILE__),'lib','rails-string_ext') unless ''.respond_to?(:starts_with?) +#require 'gloc-dev' + +class LClass; include GLoc; end +class LClass2 < LClass; end +class LClass_en < LClass2; set_language :en; end +class LClass_ja < LClass2; set_language :ja; end +# class LClass_forced_au < LClass; set_language :en; force_language :en_AU; set_language :ja; end + +class GLocTest < Test::Unit::TestCase + include GLoc + include ActionView::Helpers::DateHelper + + def setup + @l1 = LClass.new + @l2 = LClass.new + @l3 = LClass.new + @l1.set_language :ja + @l2.set_language :en + @l3.set_language 'en_AU' + @gloc_state= GLoc.backup_state true + GLoc::CONFIG.merge!({ + :default_param_name => 'lang', + :default_cookie_name => 'lang', + :default_language => :ja, + :raise_string_not_found_errors => true, + :verbose => false, + }) + end + + def teardown + GLoc.restore_state @gloc_state + end + + #--------------------------------------------------------------------------- + + def test_basic + assert_localized_value [nil, @l1, @l2, @l3], nil, :in_both_langs + + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + + assert_localized_value [nil, @l1], 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', :in_both_langs + assert_localized_value [nil, @l1], '日本語ã®ã¿', :ja_only + assert_localized_value [nil, @l1], nil, :en_only + + assert_localized_value @l2, 'This is in en+ja', :in_both_langs + assert_localized_value @l2, nil, :ja_only + assert_localized_value @l2, 'English only', :en_only + + assert_localized_value @l3, "Thiz in en 'n' ja", :in_both_langs + assert_localized_value @l3, nil, :ja_only + assert_localized_value @l3, 'Aussie English only bro', :en_only + + @l3.set_language :en + assert_localized_value @l3, 'This is in en+ja', :in_both_langs + assert_localized_value @l3, nil, :ja_only + assert_localized_value @l3, 'English only', :en_only + + assert_localized_value nil, 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', :in_both_langs + assert_localized_value nil, '日本語ã®ã¿', :ja_only + assert_localized_value nil, nil, :en_only + end + + def test_load_twice_with_override + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang2') + + assert_localized_value [nil, @l1], 'æ›´æ–°ã•れãŸ', :in_both_langs + assert_localized_value [nil, @l1], '日本語ã®ã¿', :ja_only + assert_localized_value [nil, @l1], nil, :en_only + assert_localized_value [nil, @l1], nil, :new_en + assert_localized_value [nil, @l1], 'æ–°ãŸãªæ—¥æœ¬èªžã‚¹ãƒˆãƒªãƒ³ã‚°', :new_ja + + assert_localized_value @l2, 'This is in en+ja', :in_both_langs + assert_localized_value @l2, nil, :ja_only + assert_localized_value @l2, 'overriden dude', :en_only + assert_localized_value @l2, 'This is a new English string', :new_en + assert_localized_value @l2, nil, :new_ja + + assert_localized_value @l3, "Thiz in en 'n' ja", :in_both_langs + assert_localized_value @l3, nil, :ja_only + assert_localized_value @l3, 'Aussie English only bro', :en_only + end + + def test_load_twice_without_override + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang2'), false + + assert_localized_value [nil, @l1], 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', :in_both_langs + assert_localized_value [nil, @l1], '日本語ã®ã¿', :ja_only + assert_localized_value [nil, @l1], nil, :en_only + assert_localized_value [nil, @l1], nil, :new_en + assert_localized_value [nil, @l1], 'æ–°ãŸãªæ—¥æœ¬èªžã‚¹ãƒˆãƒªãƒ³ã‚°', :new_ja + + assert_localized_value @l2, 'This is in en+ja', :in_both_langs + assert_localized_value @l2, nil, :ja_only + assert_localized_value @l2, 'English only', :en_only + assert_localized_value @l2, 'This is a new English string', :new_en + assert_localized_value @l2, nil, :new_ja + + assert_localized_value @l3, "Thiz in en 'n' ja", :in_both_langs + assert_localized_value @l3, nil, :ja_only + assert_localized_value @l3, 'Aussie English only bro', :en_only + end + + def test_add_localized_strings + assert_localized_value nil, nil, :add + assert_localized_value nil, nil, :ja_only + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + assert_localized_value nil, nil, :add + assert_localized_value nil, '日本語ã®ã¿', :ja_only + GLoc.add_localized_strings 'en', {:ja_only => 'bullshit'}, true + GLoc.add_localized_strings 'en', {:ja_only => 'bullshit'}, false + assert_localized_value nil, nil, :add + assert_localized_value nil, '日本語ã®ã¿', :ja_only + GLoc.add_localized_strings 'ja', {:ja_only => 'bullshit', :add => '123'}, false + assert_localized_value nil, '123', :add + assert_localized_value nil, '日本語ã®ã¿', :ja_only + GLoc.add_localized_strings 'ja', {:ja_only => 'bullshit', :add => '234'} + assert_localized_value nil, '234', :add + assert_localized_value nil, 'bullshit', :ja_only + end + + def test_class_set_language + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + + @l1 = LClass_ja.new + @l2 = LClass_en.new + @l3 = LClass_en.new + + assert_localized_value @l1, 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', :in_both_langs + assert_localized_value @l2, 'This is in en+ja', :in_both_langs + assert_localized_value @l3, 'This is in en+ja', :in_both_langs + + @l3.set_language 'en_AU' + + assert_localized_value @l1, 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', :in_both_langs + assert_localized_value @l2, 'This is in en+ja', :in_both_langs + assert_localized_value @l3, "Thiz in en 'n' ja", :in_both_langs + end + + def test_ll + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + + assert_equal 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', ll('ja',:in_both_langs) + assert_equal 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', GLoc::ll('ja',:in_both_langs) + assert_equal 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', LClass_en.ll('ja',:in_both_langs) + assert_equal 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', LClass_ja.ll('ja',:in_both_langs) + + assert_equal 'This is in en+ja', ll('en',:in_both_langs) + assert_equal 'This is in en+ja', GLoc::ll('en',:in_both_langs) + assert_equal 'This is in en+ja', LClass_en.ll('en',:in_both_langs) + assert_equal 'This is in en+ja', LClass_ja.ll('en',:in_both_langs) + end + + def test_lsym + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'lang') + assert_equal 'enã«ã‚‚jaã«ã‚‚ã‚ã‚‹', LClass_ja.ltry(:in_both_langs) + assert_equal 'hello', LClass_ja.ltry('hello') + assert_equal nil, LClass_ja.ltry(nil) + end + +# def test_forced +# assert_equal :en_AU, LClass_forced_au.current_language +# a= LClass_forced_au.new +# a.set_language :ja +# assert_equal :en_AU, a.current_language +# a.force_language :ja +# assert_equal :ja, a.current_language +# assert_equal :en_AU, LClass_forced_au.current_language +# end + + def test_pluralization + GLoc.add_localized_strings :en, :_gloc_rule_default => %[|n| case n; when 0 then '_none'; when 1 then '_single'; else '_many'; end], :a_single => '%d man', :a_many => '%d men', :a_none => 'No men' + GLoc.add_localized_strings :en, :_gloc_rule_asd => %[|n| n<10 ? '_few' : '_heaps'], :a_few => 'a few men (%d)', :a_heaps=> 'soo many men' + set_language :en + + assert_equal 'No men', lwr(:a, 0) + assert_equal '1 man', lwr(:a, 1) + assert_equal '3 men', lwr(:a, 3) + assert_equal '20 men', lwr(:a, 20) + + assert_equal 'a few men (0)', lwr_(:asd, :a, 0) + assert_equal 'a few men (1)', lwr_(:asd, :a, 1) + assert_equal 'a few men (3)', lwr_(:asd, :a, 3) + assert_equal 'soo many men', lwr_(:asd, :a, 12) + assert_equal 'soo many men', lwr_(:asd, :a, 20) + + end + + def test_distance_in_words + load_default_strings + [ + [20.seconds, 'less than a minute', '1分以内', 'меньше минуты'], + [80.seconds, '1 minute', '1分', '1 минуту'], + [3.seconds, 'less than 5 seconds', '5秒以内', 'менее 5 Ñекунд', true], + [9.seconds, 'less than 10 seconds', '10秒以内', 'менее 10 Ñекунд', true], + [16.seconds, 'less than 20 seconds', '20秒以内', 'менее 20 Ñекунд', true], + [35.seconds, 'half a minute', 'ç´„30ç§’', 'полминуты', true], + [50.seconds, 'less than a minute', '1分以内', 'меньше минуты', true], + [1.1.minutes, '1 minute', '1分', '1 минуту'], + [2.1.minutes, '2 minutes', '2分', '2 минуты'], + [4.1.minutes, '4 minutes', '4分', '4 минуты'], + [5.1.minutes, '5 minutes', '5分', '5 минут'], + [1.1.hours, 'about an hour', 'ç´„1時間', 'около чаÑа'], + [3.1.hours, 'about 3 hours', 'ç´„3時間', 'около 3 чаÑов'], + [9.1.hours, 'about 9 hours', 'ç´„9時間', 'около 9 чаÑов'], + [1.1.days, '1 day', '1日間', '1 день'], + [2.1.days, '2 days', '2日間', '2 днÑ'], + [4.days, '4 days', '4日間', '4 днÑ'], + [6.days, '6 days', '6日間', '6 дней'], + [11.days, '11 days', '11日間', '11 дней'], + [12.days, '12 days', '12日間', '12 дней'], + [15.days, '15 days', '15日間', '15 дней'], + [20.days, '20 days', '20日間', '20 дней'], + [21.days, '21 days', '21日間', '21 день'], + [22.days, '22 days', '22日間', '22 днÑ'], + [25.days, '25 days', '25日間', '25 дней'], + ].each do |a| + t, en, ja, ru = a + inc_sec= (a.size == 5) ? a[-1] : false + set_language :en + assert_equal en, distance_of_time_in_words(t,0,inc_sec) + set_language :ja + assert_equal ja, distance_of_time_in_words(t,0,inc_sec) + set_language :ru + assert_equal ru, distance_of_time_in_words(t,0,inc_sec) + end + end + + def test_age + load_default_strings + [ + [1, '1 yr', '1æ­³', '1 год'], + [22, '22 yrs', '22æ­³', '22 года'], + [27, '27 yrs', '27æ­³', '27 лет'], + ].each do |a, en, ja, ru| + set_language :en + assert_equal en, l_age(a) + set_language :ja + assert_equal ja, l_age(a) + set_language :ru + assert_equal ru, l_age(a) + end + end + + def test_yesno + load_default_strings + set_language :en + assert_equal 'yes', l_yesno(true) + assert_equal 'no', l_yesno(false) + assert_equal 'Yes', l_YesNo(true) + assert_equal 'No', l_YesNo(false) + end + + def test_all_languages_have_values_for_helpers + load_default_strings + t= Time.local(2000, 9, 15, 11, 23, 57) + GLoc.valid_languages.each {|l| + set_language l + 0.upto(120) {|n| l_age(n)} + l_date(t) + l_datetime(t) + l_datetime_short(t) + l_time(t) + [true,false].each{|v| l_YesNo(v); l_yesno(v) } + } + end + + def test_similar_languages + GLoc.add_localized_strings :en, :a => 'a' + GLoc.add_localized_strings :en_AU, :a => 'a' + GLoc.add_localized_strings :ja, :a => 'a' + GLoc.add_localized_strings :zh_tw, :a => 'a' + + assert_equal :en, GLoc.similar_language(:en) + assert_equal :en, GLoc.similar_language('en') + assert_equal :ja, GLoc.similar_language(:ja) + assert_equal :ja, GLoc.similar_language('ja') + # lowercase + dashes to underscores + assert_equal :en, GLoc.similar_language('EN') + assert_equal :en, GLoc.similar_language(:EN) + assert_equal :en_AU, GLoc.similar_language(:EN_Au) + assert_equal :en_AU, GLoc.similar_language('eN-Au') + # remove dialect + assert_equal :ja, GLoc.similar_language(:ja_Au) + assert_equal :ja, GLoc.similar_language('JA-ASDF') + assert_equal :ja, GLoc.similar_language('jA_ASD_ZXC') + # different dialect + assert_equal :zh_tw, GLoc.similar_language('ZH') + assert_equal :zh_tw, GLoc.similar_language('ZH_HK') + assert_equal :zh_tw, GLoc.similar_language('ZH-BUL') + # non matching + assert_equal nil, GLoc.similar_language('WW') + assert_equal nil, GLoc.similar_language('WW_AU') + assert_equal nil, GLoc.similar_language('WW-AU') + assert_equal nil, GLoc.similar_language('eZ_en') + assert_equal nil, GLoc.similar_language('AU-ZH') + end + + def test_clear_strings_and_similar_langs + GLoc.add_localized_strings :en, :a => 'a' + GLoc.add_localized_strings :en_AU, :a => 'a' + GLoc.add_localized_strings :ja, :a => 'a' + GLoc.add_localized_strings :zh_tw, :a => 'a' + GLoc.clear_strings :en, :ja + assert_equal nil, GLoc.similar_language('ja') + assert_equal :en_AU, GLoc.similar_language('en') + assert_equal :zh_tw, GLoc.similar_language('ZH_HK') + GLoc.clear_strings + assert_equal nil, GLoc.similar_language('ZH_HK') + end + + def test_lang_name + GLoc.add_localized_strings :en, :general_lang_en => 'English', :general_lang_ja => 'Japanese' + GLoc.add_localized_strings :ja, :general_lang_en => '英語', :general_lang_ja => '日本語' + set_language :en + assert_equal 'Japanese', l_lang_name(:ja) + assert_equal 'English', l_lang_name('en') + set_language :ja + assert_equal '日本語', l_lang_name('ja') + assert_equal '英語', l_lang_name(:en) + end + + def test_charset_change_all + load_default_strings + GLoc.add_localized_strings :ja2, :a => 'a' + GLoc.valid_languages # Force refresh if in dev mode + GLoc.class_eval 'LOCALIZED_STRINGS[:ja2]= LOCALIZED_STRINGS[:ja].clone' + + [:ja, :ja2].each do |l| + set_language l + assert_equal 'ã¯ã„', l_yesno(true) + assert_equal "E381AFE38184", l_yesno(true).unpack('H*')[0].upcase + end + + GLoc.set_charset 'sjis' + assert_equal 'sjis', GLoc.get_charset(:ja) + assert_equal 'sjis', GLoc.get_charset(:ja2) + + [:ja, :ja2].each do |l| + set_language l + assert_equal "82CD82A2", l_yesno(true).unpack('H*')[0].upcase + end + end + + def test_charset_change_single + load_default_strings + GLoc.add_localized_strings :ja2, :a => 'a' + GLoc.add_localized_strings :ja3, :a => 'a' + GLoc.valid_languages # Force refresh if in dev mode + GLoc.class_eval 'LOCALIZED_STRINGS[:ja2]= LOCALIZED_STRINGS[:ja].clone' + GLoc.class_eval 'LOCALIZED_STRINGS[:ja3]= LOCALIZED_STRINGS[:ja].clone' + + [:ja, :ja2, :ja3].each do |l| + set_language l + assert_equal 'ã¯ã„', l_yesno(true) + assert_equal "E381AFE38184", l_yesno(true).unpack('H*')[0].upcase + end + + GLoc.set_charset 'sjis', :ja + assert_equal 'sjis', GLoc.get_charset(:ja) + assert_equal 'utf-8', GLoc.get_charset(:ja2) + assert_equal 'utf-8', GLoc.get_charset(:ja3) + + set_language :ja + assert_equal "82CD82A2", l_yesno(true).unpack('H*')[0].upcase + set_language :ja2 + assert_equal "E381AFE38184", l_yesno(true).unpack('H*')[0].upcase + set_language :ja3 + assert_equal "E381AFE38184", l_yesno(true).unpack('H*')[0].upcase + + GLoc.set_charset 'euc-jp', :ja, :ja3 + assert_equal 'euc-jp', GLoc.get_charset(:ja) + assert_equal 'utf-8', GLoc.get_charset(:ja2) + assert_equal 'euc-jp', GLoc.get_charset(:ja3) + + set_language :ja + assert_equal "A4CFA4A4", l_yesno(true).unpack('H*')[0].upcase + set_language :ja2 + assert_equal "E381AFE38184", l_yesno(true).unpack('H*')[0].upcase + set_language :ja3 + assert_equal "A4CFA4A4", l_yesno(true).unpack('H*')[0].upcase + end + + def test_set_language_if_valid + GLoc.add_localized_strings :en, :a => 'a' + GLoc.add_localized_strings :zh_tw, :a => 'a' + + assert set_language_if_valid('en') + assert_equal :en, current_language + + assert set_language_if_valid('zh_tw') + assert_equal :zh_tw, current_language + + assert !set_language_if_valid(nil) + assert_equal :zh_tw, current_language + + assert !set_language_if_valid('ja') + assert_equal :zh_tw, current_language + + assert set_language_if_valid(:en) + assert_equal :en, current_language + end + + #=========================================================================== + protected + + def assert_localized_value(objects,expected,key) + objects = [objects] unless objects.kind_of?(Array) + objects.each {|object| + o = object || GLoc + assert_equal !expected.nil?, o.l_has_string?(key) + if expected.nil? + assert_raise(GLoc::StringNotFoundError) {o.l(key)} + else + assert_equal expected, o.l(key) + end + } + end + + def load_default_strings + GLoc.load_localized_strings File.join(File.dirname(__FILE__),'..','lang') + end +end \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lang/en.yaml b/groups/vendor/plugins/gloc-1.1.0/test/lang/en.yaml new file mode 100644 index 000000000..325dc599e --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lang/en.yaml @@ -0,0 +1,2 @@ +in_both_langs: This is in en+ja +en_only: English only \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lang/en_AU.yaml b/groups/vendor/plugins/gloc-1.1.0/test/lang/en_AU.yaml new file mode 100644 index 000000000..307cc7859 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lang/en_AU.yaml @@ -0,0 +1,2 @@ +in_both_langs: Thiz in en 'n' ja +en_only: Aussie English only bro \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lang/ja.yml b/groups/vendor/plugins/gloc-1.1.0/test/lang/ja.yml new file mode 100644 index 000000000..64df03376 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lang/ja.yml @@ -0,0 +1,2 @@ +in_both_langs: enã«ã‚‚jaã«ã‚‚ã‚ã‚‹ +ja_only: 日本語ã®ã¿ \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lang2/en.yml b/groups/vendor/plugins/gloc-1.1.0/test/lang2/en.yml new file mode 100644 index 000000000..e6467e7a0 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lang2/en.yml @@ -0,0 +1,2 @@ +en_only: overriden dude +new_en: This is a new English string \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lang2/ja.yaml b/groups/vendor/plugins/gloc-1.1.0/test/lang2/ja.yaml new file mode 100644 index 000000000..864b287d0 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lang2/ja.yaml @@ -0,0 +1,2 @@ +in_both_langs: æ›´æ–°ã•れ㟠+new_ja: æ–°ãŸãªæ—¥æœ¬èªžã‚¹ãƒˆãƒªãƒ³ã‚° \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lib/rails-string_ext.rb b/groups/vendor/plugins/gloc-1.1.0/test/lib/rails-string_ext.rb new file mode 100644 index 000000000..418d28db2 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lib/rails-string_ext.rb @@ -0,0 +1,23 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Additional string tests. + module StartsEndsWith + # Does the string start with the specified +prefix+? + def starts_with?(prefix) + prefix = prefix.to_s + self[0, prefix.length] == prefix + end + + # Does the string end with the specified +suffix+? + def ends_with?(suffix) + suffix = suffix.to_s + self[-suffix.length, suffix.length] == suffix + end + end + end + end +end +class String + include ActiveSupport::CoreExtensions::String::StartsEndsWith +end \ No newline at end of file diff --git a/groups/vendor/plugins/gloc-1.1.0/test/lib/rails-time_ext.rb b/groups/vendor/plugins/gloc-1.1.0/test/lib/rails-time_ext.rb new file mode 100644 index 000000000..d8771e4e6 --- /dev/null +++ b/groups/vendor/plugins/gloc-1.1.0/test/lib/rails-time_ext.rb @@ -0,0 +1,76 @@ +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Numeric #:nodoc: + # Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years. + # + # If you need precise date calculations that doesn't just treat months as 30 days, then have + # a look at Time#advance. + # + # Some of these methods are approximations, Ruby's core + # Date[http://stdlib.rubyonrails.org/libdoc/date/rdoc/index.html] and + # Time[http://stdlib.rubyonrails.org/libdoc/time/rdoc/index.html] should be used for precision + # date and time arithmetic + module Time + def seconds + self + end + alias :second :seconds + + def minutes + self * 60 + end + alias :minute :minutes + + def hours + self * 60.minutes + end + alias :hour :hours + + def days + self * 24.hours + end + alias :day :days + + def weeks + self * 7.days + end + alias :week :weeks + + def fortnights + self * 2.weeks + end + alias :fortnight :fortnights + + def months + self * 30.days + end + alias :month :months + + def years + (self * 365.25.days).to_i + end + alias :year :years + + # Reads best without arguments: 10.minutes.ago + def ago(time = ::Time.now) + time - self + end + + # Reads best with argument: 10.minutes.until(time) + alias :until :ago + + # Reads best with argument: 10.minutes.since(time) + def since(time = ::Time.now) + time + self + end + + # Reads best without arguments: 10.minutes.from_now + alias :from_now :since + end + end + end +end + +class Numeric #:nodoc: + include ActiveSupport::CoreExtensions::Numeric::Time +end diff --git a/groups/vendor/plugins/rfpdf/CHANGELOG b/groups/vendor/plugins/rfpdf/CHANGELOG new file mode 100644 index 000000000..6822b8364 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/CHANGELOG @@ -0,0 +1,13 @@ +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). diff --git a/groups/vendor/plugins/rfpdf/MIT-LICENSE b/groups/vendor/plugins/rfpdf/MIT-LICENSE new file mode 100644 index 000000000..f39a79dc0 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 4ssoM LLC + +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 diff --git a/groups/vendor/plugins/rfpdf/README b/groups/vendor/plugins/rfpdf/README new file mode 100644 index 000000000..9db19075b --- /dev/null +++ b/groups/vendor/plugins/rfpdf/README @@ -0,0 +1,99 @@ += 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. diff --git a/groups/vendor/plugins/rfpdf/init.rb b/groups/vendor/plugins/rfpdf/init.rb new file mode 100644 index 000000000..7e51d9eba --- /dev/null +++ b/groups/vendor/plugins/rfpdf/init.rb @@ -0,0 +1,3 @@ +require 'rfpdf' + +ActionView::Base::register_template_handler 'rfpdf', RFPDF::View \ No newline at end of file diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf.rb new file mode 100644 index 000000000..9fc0683ef --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf.rb @@ -0,0 +1,31 @@ +# Copyright (c) 2006 4ssoM LLC +# +# 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' diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/bookmark.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/bookmark.rb new file mode 100644 index 000000000..a04ccd18d --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/bookmark.rb @@ -0,0 +1,99 @@ +# 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('<>') + out('endobj') + end + # Outline root + newobj + @OutlineRoot=@n + out('<>') + 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 diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb new file mode 100644 index 000000000..6fe3eee8a --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb @@ -0,0 +1,473 @@ +# Copyright (c) 2006 4ssoM LLC +# 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 chinese.php +# +# Chinese PDF support. +# +# Usage is as follows: +# +# require 'fpdf' +# require 'chinese' +# pdf = FPDF.new +# pdf.extend(PDF_Chinese) +# +# This allows it to be combined with other extensions, such as the bookmark +# module. + +module PDF_Chinese + + Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,''=>490,'%'=>875,'&'=>698,'\''=>250, + '('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500, + '2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,''=>250, + '<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625, + 'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823, + 'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677, + 'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427, + 'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802, + 'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677, + 'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'end'=>480,'~'=>667} + + GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,''=>462,'%'=>797,'&'=>710,'\''=>239, + '('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462, + '2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,''=>238, + '<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563, + 'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772, + 'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620, + 'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427, + 'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793, + 'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652, + 'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'end'=>370,'~'=>605} + + def AddCIDFont(family,style,name,cw,cMap,registry) +#ActionController::Base::logger.debug registry.to_a.join(":").to_s + fontkey=family.downcase+style.upcase + unless @fonts[fontkey].nil? + Error("Font already added: family style") + end + i=@fonts.length+1 + name=name.gsub(' ','') + @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'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 AddBig5Font(family='Big5',name='MSungStd-Light-Acro') + #Add Big5 font with proportional Latin + cw=Big5_widths + cMap='ETenms-B5-H' + registry={'ordering'=>'CNS1','supplement'=>0} +#ActionController::Base::logger.debug registry.to_a.join(":").to_s + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro') + #Add Big5 font with half-witdh Latin + cw = {} + 32.upto(126) do |i| + cw[i.chr]=500 + end + cMap='ETen-B5-H' + registry={'ordering'=>'CNS1','supplement'=>0} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddGBFont(family='GB',name='STSongStd-Light-Acro') + #Add GB font with proportional Latin + cw=GB_widths + cMap='GBKp-EUC-H' + registry={'ordering'=>'GB1','supplement'=>2} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro') + #Add GB font with half-width Latin + 32.upto(126) do |i| + cw[i.chr]=500 + end + cMap='GBK-EUC-H' + registry={'ordering'=>'GB1','supplement'=>2} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def GetStringWidth(s) + if(@CurrentFont['type']=='Type0') + return GetMBStringWidth(s) + else + return super(s) + end + end + + def GetMBStringWidth(s) + #Multi-byte version of GetStringWidth() + l=0 + cw=@CurrentFont['cw'] + nb=s.length + i=0 + while(i0 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(iwmax) + #Automatic line break + if(sep==-1 or i==j) + if(i==j) + i+=ascii ? 1 : 2 + 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+=ascii ? 1 : 2 + 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') + MBWrite(h,txt,link) + else + super(h,txt,link) + end + end + + def MBWrite(h,txt,link) + #Multi-byte 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(iwmax) + #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+=1 + nl+=1 + next + end + if(i==j) + i+=ascii ? 1 : 2 + 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+=ascii ? 1 : 2 + 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('<>') + 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('<>') + 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('<>') + 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='<>') + 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('<>') + out('/FontDescriptor '+(@n+1).to_s+' 0 R') + if(font['CMap']=='ETen-B5-H') + w='13648 13742 500' + elsif(font['CMap']=='GBK-EUC-H') + w='814 907 500 7716 [500]' + else + # ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s + # ActionController::Base::logger.debug font['cw'].values.join(' ').to_s + 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 + } + w +=']' + end + out('/W ['+w+']>>') + out('endobj') + #Font descriptor + newobj() + out('<>') + out('endobj') + end +end diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/errors.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/errors.rb new file mode 100644 index 000000000..2be2dae16 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/errors.rb @@ -0,0 +1,4 @@ +module RFPDF + class GenerationError < StandardError #:nodoc: + end +end \ No newline at end of file diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/fpdf.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/fpdf.rb new file mode 100644 index 000000000..ad52e9e62 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/fpdf.rb @@ -0,0 +1,1550 @@ +# 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 @x0 + # 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 i0 + @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 iwmax + # 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('<>>>' + 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('<>') + out('endobj') + end + + def putfonts + nf=@n + @diffs.each do |diff| + # Encodings + newobj + out('<>') + 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('<>') + 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('<>') + out('endobj') + elsif type=='Type1' or type=='TrueType' + # Additional Type1 or TrueType font + newobj + out('<>') + out('endobj') + # Widths + newobj + cw=font['cw'] + s='[' + 32.upto(255) do |i| + s << cw[i].to_s+' ' + end + out(s+']') + out('endobj') + # Descriptor + newobj + 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('<>') + 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 <>' + # 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 diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/fpdf_eps.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/fpdf_eps.rb new file mode 100644 index 000000000..c6a224310 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/fpdf_eps.rb @@ -0,0 +1,139 @@ +# 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 diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/japanese.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/japanese.rb new file mode 100644 index 000000000..4e611a6f6 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/japanese.rb @@ -0,0 +1,468 @@ +# Copyright (c) 2006 4ssoM LLC +# 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=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=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=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('<>') + 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('<>') + 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('<>') + 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='<>') + 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('<>') + out('/FontDescriptor '+(@n+1).to_s+' 0 R') + w='/W [1 [' + font['cw'].keys.sort.each {|key| + w+=font['cw'][key].to_s + " " +# ActionController::Base::logger.debug key.to_s +# ActionController::Base::logger.debug font['cw'][key].to_s + } + out(w+'] 231 325 500 631 [500] 326 389 500]') + out('>>') + out('endobj') + #Font descriptor + newobj() + out('<>') + out('endobj') + end +end diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/korean.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/korean.rb new file mode 100644 index 000000000..64131405e --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/korean.rb @@ -0,0 +1,436 @@ +# Copyright (c) 2006 4ssoM LLC +# 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 korean.php +# +# Korean PDF support. +# +# Usage is as follows: +# +# require 'fpdf' +# require 'chinese' +# pdf = FPDF.new +# pdf.extend(PDF_Korean) +# +# This allows it to be combined with other extensions, such as the bookmark +# module. + +module PDF_Korean + +UHC_widths={' ' => 333, '!' => 416, '"' => 416, '#' => 833, '$' => 625, '%' => 916, '&' => 833, '\'' => 250, + '(' => 500, ')' => 500, '*' => 500, '+' => 833, ',' => 291, '-' => 833, '.' => 291, '/' => 375, '0' => 625, '1' => 625, + '2' => 625, '3' => 625, '4' => 625, '5' => 625, '6' => 625, '7' => 625, '8' => 625, '9' => 625, ':' => 333, ';' => 333, + '<' => 833, '=' => 833, '>' => 916, '?' => 500, '@' => 1000, 'A' => 791, 'B' => 708, 'C' => 708, 'D' => 750, 'E' => 708, + 'F' => 666, 'G' => 750, 'H' => 791, 'I' => 375, 'J' => 500, 'K' => 791, 'L' => 666, 'M' => 916, 'N' => 791, 'O' => 750, + 'P' => 666, 'Q' => 750, 'R' => 708, 'S' => 666, 'T' => 791, 'U' => 791, 'V' => 750, 'W' => 1000, 'X' => 708, 'Y' => 708, + 'Z' => 666, '[' => 500, '\\' => 375, ']' => 500, '^' => 500, '_' => 500, '`' => 333, 'a' => 541, 'b' => 583, 'c' => 541, + 'd' => 583, 'e' => 583, 'f' => 375, 'g' => 583, 'h' => 583, 'i' => 291, 'j' => 333, 'k' => 583, 'l' => 291, 'm' => 875, + 'n' => 583, 'o' => 583, 'p' => 583, 'q' => 583, 'r' => 458, 's' => 541, 't' => 375, 'u' => 583, 'v' => 583, 'w' => 833, + 'x' => 625, 'y' => 625, 'z' => 500, '{' => 583, '|' => 583, '}' => 583, '~' => 750} + + def AddCIDFont(family,style,name,cw,cMap,registry) + fontkey=family.downcase+style.upcase + unless @fonts[fontkey].nil? + Error("Font already added: family style") + end + i=@fonts.length+1 + name=name.gsub(' ','') + @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'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 AddUHCFont(family='UHC',name='HYSMyeongJoStd-Medium-Acro') + #Add UHC font with proportional Latin + cw=UHC_widths + cMap='KSCms-UHC-H' + registry={'ordering'=>'Korea1','supplement'=>1} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddUHChwFont(family='UHC-hw',name='HYSMyeongJoStd-Medium-Acro') + #Add UHC font with half-witdh Latin + 32.upto(126) do |i| + cw[i.chr]=500 + end + cMap='KSCms-UHC-HW-H' + registry={'ordering'=>'Korea1','supplement'=>1} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def GetStringWidth(s) + if(@CurrentFont['type']=='Type0') + return GetMBStringWidth(s) + else + return super(s) + end + end + + def GetMBStringWidth(s) + #Multi-byte version of GetStringWidth() + l=0 + cw=@CurrentFont['cw'] + nb=s.length + i=0 + while(i0 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.index('L').nil?) + b2+='L' + end + if(border.index('R').nil?) + b2+='R' + end + b=border.index('T').nil? ? b2+'T' : b2 + end + end + sep=-1 + i=0 + j=0 + l=0 + nl=1 + while(iwmax) + #Automatic line break + if(sep==-1 or i==j) + if(i==j) + i+=ascii ? 1 : 2 + 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+=ascii ? 1 : 2 + end + end + #Last chunk + if(border and not border.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') + MBWrite(h,txt,link) + else + super(h,txt,link) + end + end + + def MBWrite(h,txt,link) + #Multi-byte 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(iwmax) + #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+=1 + nl+=1 + next + end + if(i==j) + i+=ascii ? 1 : 2 + 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+=ascii ? 1 : 2 + 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('<>') + 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('<>') + 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('<>') + 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='<>') + 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('<>') + out('/FontDescriptor '+(@n+1).to_s+' 0 R') + if(font['CMap']=='KSCms-UHC-HW-H') + w='8094 8190 500' + else + 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 + } + w +=']' + end + out('/W ['+w+']>>') + out('endobj') + #Font descriptor + newobj() + out('<>') + out('endobj') + end +end diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/makefont.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/makefont.rb new file mode 100644 index 000000000..bda7a70ef --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/makefont.rb @@ -0,0 +1,1787 @@ +#!/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 diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/rfpdf.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/rfpdf.rb new file mode 100644 index 000000000..5ad882903 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/rfpdf.rb @@ -0,0 +1,346 @@ +module RFPDF + COLOR_PALETTE = { + :black => [0x00, 0x00, 0x00], + :white => [0xff, 0xff, 0xff], + }.freeze + + # Draw a line from (x1, y1) to (x2, y2). + # + # Options are: + # * :line_color - Default value is COLOR_PALETTE[:black]. + # * :line_width - Default value is 0.5. + # + # 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 text at (x, y). + # + # Options are: + # * :font_color - Default value is COLOR_PALETTE[:black]. + # * :font_size - Default value is 10. + # * :font_style - Default value is nothing or ''. + # + # 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 text at (x, y) bounded by left_margin and right_margin. Both + # margins are measured from their corresponding edge. + # + # Options are: + # * :font_color - Default value is COLOR_PALETTE[:black]. + # * :font_size - Default value is 10. + # * :font_style - Default value is nothing or ''. + # + # 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 (x, y), w wide and h high. + # + # Options are: + # * :border - Draw a border, 0 = no, 1 = yes? Default value is 1. + # * :border_color - Default value is COLOR_PALETTE[:black]. + # * :border_width - Default value is 0.5. + # * :fill - Fill the box, 0 = no, 1 = yes? Default value is 1. + # * :fill_color - Default value is nothing or COLOR_PALETTE[:white]. + # + # 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 text at (x, y) in a box w wide and h high. + # + # Options are: + # * :align - Vertical alignment 'C' = center, 'L' = left, 'R' = right. Default value is 'C'. + # * :border - Draw a border, 0 = no, 1 = yes? Default value is 0. + # * :border_color - Default value is COLOR_PALETTE[:black]. + # * :border_width - Default value is 0.5. + # * :fill - Fill the box, 0 = no, 1 = yes? Default value is 1. + # * :fill_color - Default value is nothing or COLOR_PALETTE[:white]. + # * :font_color - Default value is COLOR_PALETTE[:black]. + # * :font_size - Default value is nothing or 8. + # * :font_style - 'B' = bold, 'I' = italic, 'U' = underline. Default value is nothing ''. + # * :padding - Default value is nothing or 2. + # * :valign - 'M' = middle, 'T' = top, 'B' = bottom. Default value is nothing or 'M'. + # + # 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 text at (x, y) as a title. + # + # Options are: + # * :font_color - Default value is COLOR_PALETTE[:black]. + # * :font_size - Default value is 18. + # * :font_style - Default value is nothing or ''. + # + # 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 COLOR_PALETTE[:black]. + # + # 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 COLOR_PALETTE[:white]. + # + # 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 COLOR_PALETTE[:white]. + # + # 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 COLOR_PALETTE[:white]. + # + # Options are: + # * :height - Line height. Default value is 20. + # + # 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 diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb new file mode 100644 index 000000000..185811202 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb @@ -0,0 +1,75 @@ +# Copyright (c) 2006 4ssoM LLC +# +# 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 + + 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 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(template).result(binding) + end + end + + end + +end \ No newline at end of file diff --git a/groups/vendor/plugins/rfpdf/test/test_helper.rb b/groups/vendor/plugins/rfpdf/test/test_helper.rb new file mode 100644 index 000000000..2e2ea3bc5 --- /dev/null +++ b/groups/vendor/plugins/rfpdf/test/test_helper.rb @@ -0,0 +1 @@ +#!/usr/bin/env ruby \ No newline at end of file diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/COPYING b/groups/vendor/plugins/ruby-net-ldap-0.0.4/COPYING new file mode 100644 index 000000000..2ff629a20 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/COPYING @@ -0,0 +1,272 @@ + 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. diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/ChangeLog b/groups/vendor/plugins/ruby-net-ldap-0.0.4/ChangeLog new file mode 100644 index 000000000..bd9b70e7d --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/ChangeLog @@ -0,0 +1,58 @@ += 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 diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/LICENCE b/groups/vendor/plugins/ruby-net-ldap-0.0.4/LICENCE new file mode 100644 index 000000000..953ea0bb9 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/LICENCE @@ -0,0 +1,55 @@ +Net::LDAP is copyrighted free software by Francis Cianfrocca +. 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. diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/README b/groups/vendor/plugins/ruby-net-ldap-0.0.4/README new file mode 100644 index 000000000..f61a7ff15 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/README @@ -0,0 +1,32 @@ += 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 diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ber.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ber.rb new file mode 100644 index 000000000..6589415dc --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ber.rb @@ -0,0 +1,294 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap.rb new file mode 100644 index 000000000..d741e722b --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap.rb @@ -0,0 +1,1311 @@ +# $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 entity, 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 entity, or less + # commonly, principal, 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 Distinguished Name, 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 Attributes. + # 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 sn, 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 sn attribute have exactly one value. In LDAP + # jargon, that means that sn must be present and + # single-valued. + # + # Another attribute is mail, 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 email.) mail differs + # from sn in that most directories permit any number of values for the + # mail attribute, including zero. + # + # + # === Tree-Base + # We said above that X.400 Distinguished Names are globally unique. + # 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 tree-base 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 search filters, + # 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 not 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.) + # + # Important: 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 all 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. + # The :simple_tls option is the simplest, easiest way to encrypt communications + # between Net::LDAP and LDAP servers. + # 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 simple TLS 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. + # + # [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.] + # + 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 self 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. + # Note: 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 each entry 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 :return_result 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 :return_result 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 :return_result => false, #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 :auth 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, provided 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 re-bind as that user with the + # password (or other authenticator) that you supply. + # + # #bind_as takes the same parameters as #search, with the addition of an + # authenticator. Currently, this authenticator must be :password. + # 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 + # + # (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.) + # + # 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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/dataset.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/dataset.rb new file mode 100644 index 000000000..1480a8f84 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/dataset.rb @@ -0,0 +1,108 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/entry.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/entry.rb new file mode 100644 index 000000000..8978545ee --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/entry.rb @@ -0,0 +1,165 @@ +# $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 + # values. 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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/filter.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/filter.rb new file mode 100644 index 000000000..4d06c26f3 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/filter.rb @@ -0,0 +1,387 @@ +# $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 :filter 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 present 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 mail. + # 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 sAMAccountName: + # 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 not supported + # due to limitations in the underlying LDAP protocol. + # This example selects any entry with a mail 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 objectclass + # attribute AND have a mail 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 objectclass + # attribute OR a mail 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 do not have an objectclass + # 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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/pdu.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/pdu.rb new file mode 100644 index 000000000..dbc0d6f10 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/pdu.rb @@ -0,0 +1,205 @@ +# $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 + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/psw.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/psw.rb new file mode 100644 index 000000000..89d1ffdf2 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/psw.rb @@ -0,0 +1,64 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldif.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldif.rb new file mode 100644 index 000000000..1641bda4b --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldif.rb @@ -0,0 +1,39 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testber.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testber.rb new file mode 100644 index 000000000..4fe2e3071 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testber.rb @@ -0,0 +1,42 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testdata.ldif b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testdata.ldif new file mode 100644 index 000000000..eb5610d5f --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testdata.ldif @@ -0,0 +1,101 @@ +# $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 + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testem.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testem.rb new file mode 100644 index 000000000..46b4909cb --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testem.rb @@ -0,0 +1,12 @@ +# $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' + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testfilter.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testfilter.rb new file mode 100644 index 000000000..b8fb40996 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testfilter.rb @@ -0,0 +1,37 @@ +# $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( "uidgeorge*" ) + 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 + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldap.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldap.rb new file mode 100644 index 000000000..bb70a0b20 --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldap.rb @@ -0,0 +1,190 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldif.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldif.rb new file mode 100644 index 000000000..73eca746f --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldif.rb @@ -0,0 +1,69 @@ +# $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 + + diff --git a/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testpsw.rb b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testpsw.rb new file mode 100644 index 000000000..6b1aa08be --- /dev/null +++ b/groups/vendor/plugins/ruby-net-ldap-0.0.4/tests/testpsw.rb @@ -0,0 +1,28 @@ +# $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 + +