diff --git a/issue_relations/Rakefile b/issue_relations/Rakefile new file mode 100644 index 000000000..cffd19f0c --- /dev/null +++ b/issue_relations/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/issue_relations/app/controllers/account_controller.rb b/issue_relations/app/controllers/account_controller.rb new file mode 100644 index 000000000..fb775d196 --- /dev/null +++ b/issue_relations/app/controllers/account_controller.rb @@ -0,0 +1,133 @@ +# 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 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] + before_filter :require_login, :except => [:show, :login, :lost_password, :register] + + # Show user's account + def show + @user = User.find(params[:id]) + @custom_values = @user.custom_values.find(:all, :include => :custom_field) + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Login request and validation + def login + if request.get? + # Logout user + self.logged_in_user = nil + else + # Authenticate user + user = User.try_to_login(params[:login], params[:password]) + if user + self.logged_in_user = user + redirect_back_or_default :controller => 'my', :action => 'page' + else + flash.now[:notice] = l(:notice_account_invalid_creditentials) + end + end + end + + # Log out current user and redirect to welcome page + def logout + self.logged_in_user = nil + redirect_to :controller => '' + end + + # Enable user to choose a new password + def lost_password + if params[:token] + @token = Token.find_by_action_and_value("recovery", params[:token]) + redirect_to :controller => '' and 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[:notice] = l(:notice_account_unknown_email) and return unless user + # user uses an external authentification + flash.now[:notice] = 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 + # send token to user via email + Mailer.set_language_if_valid(user.language) + 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 :controller => '' and return if $RDM_SELF_REGISTRATION == false + if params[:token] + token = Token.find_by_action_and_value("register", params[:token]) + redirect_to :controller => '' and return unless token and !token.expired? + user = token.user + redirect_to :controller => '' and return unless user.status == User::STATUS_REGISTERED + user.status = User::STATUS_ACTIVE + if user.save + token.destroy + flash[:notice] = l(:notice_account_activated) + redirect_to :action => 'login' + return + end + else + if request.get? + @user = User.new(:language => $RDM_DEFAULT_LANG) + @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"][x.id.to_s]) } + @user.custom_values = @custom_values + token = Token.new(:user => @user, :action => "register") + if @user.save and token.save + Mailer.set_language_if_valid(@user.language) + Mailer.deliver_register(token) + flash[:notice] = l(:notice_account_register_done) + redirect_to :controller => '' + end + end + end + end +end diff --git a/issue_relations/app/controllers/admin_controller.rb b/issue_relations/app/controllers/admin_controller.rb new file mode 100644 index 000000000..a9a6a8f0b --- /dev/null +++ b/issue_relations/app/controllers/admin_controller.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 AdminController < ApplicationController + layout 'base' + before_filter :require_admin + + helper :sort + include SortHelper + + def index + end + + def projects + sort_init 'name', 'asc' + sort_update + @project_count = Project.count + @project_pages = Paginator.new self, @project_count, + 15, + params['page'] + @projects = Project.find :all, :order => sort_clause, + :limit => @project_pages.items_per_page, + :offset => @project_pages.current.offset + + render :action => "projects", :layout => false if request.xhr? + end + + def mail_options + @actions = Permission.find(:all, :conditions => ["mail_option=?", true]) || [] + if request.post? + @actions.each { |a| + a.mail_enabled = (params[:action_ids] || []).include? a.id.to_s + a.save + } + flash.now[:notice] = l(:notice_successful_update) + end + end + + def info + @adapter_name = ActiveRecord::Base.connection.adapter_name + end +end diff --git a/issue_relations/app/controllers/application.rb b/issue_relations/app/controllers/application.rb new file mode 100644 index 000000000..da01e09c8 --- /dev/null +++ b/issue_relations/app/controllers/application.rb @@ -0,0 +1,132 @@ +# 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 ApplicationController < ActionController::Base + before_filter :check_if_login_required, :set_localization + + def logged_in_user=(user) + @logged_in_user = user + session[:user_id] = (user ? user.id : nil) + end + + def logged_in_user + if session[:user_id] + @logged_in_user ||= User.find(session[:user_id]) + else + nil + end + end + + # check if login is globally required to access the application + def check_if_login_required + require_login if $RDM_LOGIN_REQUIRED + end + + def set_localization + lang = begin + if self.logged_in_user and self.logged_in_user.language and !self.logged_in_user.language.empty? and GLoc.valid_languages.include? self.logged_in_user.language.to_sym + self.logged_in_user.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 + accept_lang + end + end + rescue + nil + end || $RDM_DEFAULT_LANG + set_language_if_valid(lang) + end + + def require_login + unless self.logged_in_user + store_location + redirect_to :controller => "account", :action => "login" + return false + end + true + end + + def require_admin + return unless require_login + unless self.logged_in_user.admin? + render :nothing => true, :status => 403 + return false + end + true + end + + # authorizes the user for the requested action. + def authorize(ctrl = params[:controller], action = params[:action]) + # check if action is allowed on public projects + if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ ctrl, action ] + return true + end + # if action is not public, force login + return unless require_login + # admin is always authorized + return true if self.logged_in_user.admin? + # if not admin, check membership permission + @user_membership ||= Member.find(:first, :conditions => ["user_id=? and project_id=?", self.logged_in_user.id, @project.id]) + if @user_membership and Permission.allowed_to_role( "%s/%s" % [ ctrl, action ], @user_membership.role_id ) + return true + end + render :nothing => true, :status => 403 + false + end + + # store current uri in session. + # return to this location by calling redirect_back_or_default + def store_location + session[:return_to] = request.request_uri + end + + # move to the last store_location call or to the passed default one + def redirect_back_or_default(default) + if session[:return_to].nil? + redirect_to default + else + redirect_to_url session[:return_to] + session[:return_to] = nil + end + end + + def render_404 + @html_title = "404" + render :template => "common/404", :layout => true, :status => 404 + return false + 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 +end \ No newline at end of file diff --git a/issue_relations/app/controllers/auth_sources_controller.rb b/issue_relations/app/controllers/auth_sources_controller.rb new file mode 100644 index 000000000..86b58d365 --- /dev/null +++ b/issue_relations/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 + rescue => text + flash[:notice] = text + end + flash[:notice] ||= l(:notice_successful_connection) + 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/issue_relations/app/controllers/custom_fields_controller.rb b/issue_relations/app/controllers/custom_fields_controller.rb new file mode 100644 index 000000000..bfa152fd1 --- /dev/null +++ b/issue_relations/app/controllers/custom_fields_controller.rb @@ -0,0 +1,71 @@ +# 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_field_pages, @custom_fields = paginate :custom_fields, :per_page => 15 + 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' + end + @trackers = Tracker.find(:all) + 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' + end + @trackers = Tracker.find(:all) + end + + def destroy + CustomField.find(params[:id]).destroy + redirect_to :action => 'list' + rescue + flash[:notice] = "Unable to delete custom field" + redirect_to :action => 'list' + end +end diff --git a/issue_relations/app/controllers/documents_controller.rb b/issue_relations/app/controllers/documents_controller.rb new file mode 100644 index 000000000..5ff3583d9 --- /dev/null +++ b/issue_relations/app/controllers/documents_controller.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 DocumentsController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def show + @attachments = @document.attachments.find(:all, :order => "created_on DESC") + 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 => 'projects', :action => 'list_documents', :id => @project + end + + def download + @attachment = @document.attachments.find(params[:attachment_id]) + @attachment.increment_download + send_file @attachment.diskfile, :filename => @attachment.filename + rescue + render_404 + end + + def add_attachment + # Save the attachments + params[:attachments].each { |a| + Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0 + } if params[:attachments] and params[:attachments].is_a? Array + 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 + @document = Document.find(params[:id]) + @project = @document.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/issue_relations/app/controllers/enumerations_controller.rb b/issue_relations/app/controllers/enumerations_controller.rb new file mode 100644 index 000000000..8e5be0a20 --- /dev/null +++ b/issue_relations/app/controllers/enumerations_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 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 destroy + Enumeration.find(params[:id]).destroy + flash[:notice] = l(:notice_successful_delete) + redirect_to :action => 'list' + rescue + flash[:notice] = "Unable to delete enumeration" + redirect_to :action => 'list' + end +end diff --git a/issue_relations/app/controllers/feeds_controller.rb b/issue_relations/app/controllers/feeds_controller.rb new file mode 100644 index 000000000..bfce26f8f --- /dev/null +++ b/issue_relations/app/controllers/feeds_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 FeedsController < ApplicationController + session :off + + def news + @news = News.find :all, :order => 'news.created_on DESC', :limit => 10, :include => [ :author, :project ] + @headers["Content-Type"] = "application/rss+xml" + end +end diff --git a/issue_relations/app/controllers/help_controller.rb b/issue_relations/app/controllers/help_controller.rb new file mode 100644 index 000000000..9a81eed21 --- /dev/null +++ b/issue_relations/app/controllers/help_controller.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. + +class HelpController < ApplicationController + + skip_before_filter :check_if_login_required + before_filter :load_help_config + + # displays help page for the requested controller/action + def index + # select help page to display + if params[:ctrl] and @help_config['pages'][params[:ctrl]] + if params[:page] and @help_config['pages'][params[:ctrl]][params[:page]] + template = @help_config['pages'][params[:ctrl]][params[:page]] + else + template = @help_config['pages'][params[:ctrl]]['index'] + end + end + # choose language according to available help translations + lang = (@help_config['langs'].include? current_language.to_s) ? current_language.to_s : @help_config['langs'].first + + if template + redirect_to "/manual/#{lang}/#{template}" + else + redirect_to "/manual/#{lang}/index.html" + end + end + +private + def load_help_config + @help_config = YAML::load(File.open("#{RAILS_ROOT}/config/help.yml")) + end +end diff --git a/issue_relations/app/controllers/issue_categories_controller.rb b/issue_relations/app/controllers/issue_categories_controller.rb new file mode 100644 index 000000000..7f2e4cbe2 --- /dev/null +++ b/issue_relations/app/controllers/issue_categories_controller.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 IssueCategoriesController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def edit + if request.post? and @category.update_attributes(params[:category]) + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + end + + def destroy + @category.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project + rescue + flash[:notice] = "Categorie can't be deleted" + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + +private + def find_project + @category = IssueCategory.find(params[:id]) + @project = @category.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/issue_relations/app/controllers/issue_statuses_controller.rb b/issue_relations/app/controllers/issue_statuses_controller.rb new file mode 100644 index 000000000..18ca9c76d --- /dev/null +++ b/issue_relations/app/controllers/issue_statuses_controller.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 IssueStatusesController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + @issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 10 + render :action => "list", :layout => false if request.xhr? + end + + def new + @issue_status = IssueStatus.new + end + + def create + @issue_status = IssueStatus.new(params[:issue_status]) + if @issue_status.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + else + render :action => 'new' + end + end + + def edit + @issue_status = IssueStatus.find(params[:id]) + end + + def update + @issue_status = IssueStatus.find(params[:id]) + if @issue_status.update_attributes(params[:issue_status]) + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + else + render :action => 'edit' + end + end + + def destroy + IssueStatus.find(params[:id]).destroy + redirect_to :action => 'list' + rescue + flash[:notice] = "Unable to delete issue status" + redirect_to :action => 'list' + end + + +end diff --git a/issue_relations/app/controllers/issues_controller.rb b/issue_relations/app/controllers/issues_controller.rb new file mode 100644 index 000000000..114b59b8a --- /dev/null +++ b/issue_relations/app/controllers/issues_controller.rb @@ -0,0 +1,159 @@ +# 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 IssuesController < ApplicationController + layout 'base', :except => :export_pdf + before_filter :find_project, :authorize + + helper :custom_fields + include CustomFieldsHelper + helper :ifpdf + include IfpdfHelper + + def show + @status_options = @issue.status.workflows.find(:all, :include => :new_status, :conditions => ["role_id=? and tracker_id=?", self.logged_in_user.role_for_project(@project.id), @issue.tracker.id]).collect{ |w| w.new_status } if self.logged_in_user + @custom_values = @issue.custom_values.find(:all, :include => :custom_field) + @journals_count = @issue.journals.count + @journals = @issue.journals.find(:all, :include => [:user, :details], :limit => 15, :order => "journals.created_on desc") + end + + def relations + + end + + def history + @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "journals.created_on desc") + @journals_count = @journals.length + end + + def export_pdf + @custom_values = @issue.custom_values.find(:all, :include => :custom_field) + @options_for_rfpdf ||= {} + @options_for_rfpdf[:file_name] = "#{@project.name}_#{@issue.long_id}.pdf" + end + + def edit + @priorities = Enumeration::get_values('IPRI') + 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 + begin + @issue.init_journal(self.logged_in_user) + # Retrieve custom fields and values + @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 + @issue.attributes = params[:issue] + if @issue.save + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @issue + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash[:notice] = l(:notice_locking_conflict) + end + end + end + + def add_note + unless params[:notes].empty? + journal = @issue.init_journal(self.logged_in_user, params[:notes]) + #@history = @issue.histories.build(params[:history]) + #@history.author_id = self.logged_in_user.id if self.logged_in_user + #@history.status = @issue.status + if @issue.save + flash[:notice] = l(:notice_successful_update) + Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled? + redirect_to :action => 'show', :id => @issue + return + end + end + show + render :action => 'show' + end + + def change_status + #@history = @issue.histories.build(params[:history]) + @status_options = @issue.status.workflows.find(:all, :conditions => ["role_id=? and tracker_id=?", self.logged_in_user.role_for_project(@project.id), @issue.tracker.id]).collect{ |w| w.new_status } if self.logged_in_user + @new_status = IssueStatus.find(params[:new_status_id]) + if params[:confirm] + begin + #@history.author_id = self.logged_in_user.id if self.logged_in_user + #@issue.status = @history.status + #@issue.fixed_version_id = (params[:issue][:fixed_version_id]) + #@issue.assigned_to_id = (params[:issue][:assigned_to_id]) + #@issue.done_ratio = (params[:issue][:done_ratio]) + #@issue.lock_version = (params[:issue][:lock_version]) + journal = @issue.init_journal(self.logged_in_user, params[:notes]) + @issue.status = @new_status + if @issue.update_attributes(params[:issue]) + flash[:notice] = l(:notice_successful_update) + Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled? + redirect_to :action => 'show', :id => @issue + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash[:notice] = l(:notice_locking_conflict) + end + end + @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user } + end + + def destroy + @issue.destroy + redirect_to :controller => 'projects', :action => 'list_issues', :id => @project + end + + def add_predecessor + @relation = IssueRelation.new(params[:relation]) + @relation.issue_to = @issue + if request.post? and @relation.save + redirect_to :action => :relations, :id => @issue and return + end + render :action => :relations + end + + def add_attachment + # Save the attachments + params[:attachments].each { |a| + @attachment = @issue.attachments.build(:file => a, :author => self.logged_in_user) unless a.size == 0 + @attachment.save + } if params[:attachments] and params[:attachments].is_a? Array + redirect_to :action => 'show', :id => @issue + end + + def destroy_attachment + @issue.attachments.find(params[:attachment_id]).destroy + redirect_to :action => 'show', :id => @issue + end + + # Send the file in stream mode + def download + @attachment = @issue.attachments.find(params[:attachment_id]) + send_file @attachment.diskfile, :filename => @attachment.filename + rescue + render_404 + end + +private + def find_project + @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) + @project = @issue.project + @html_title = "#{@project.name} - #{@issue.tracker.name} ##{@issue.id}" + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/issue_relations/app/controllers/members_controller.rb b/issue_relations/app/controllers/members_controller.rb new file mode 100644 index 000000000..f595f2cf6 --- /dev/null +++ b/issue_relations/app/controllers/members_controller.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 MembersController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def edit + if request.post? and @member.update_attributes(params[:member]) + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + end + + def destroy + @member.destroy + flash[:notice] = l(:notice_successful_delete) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + +private + def find_project + @member = Member.find(params[:id]) + @project = @member.project + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/issue_relations/app/controllers/my_controller.rb b/issue_relations/app/controllers/my_controller.rb new file mode 100644 index 000000000..ec6b88b4c --- /dev/null +++ b/issue_relations/app/controllers/my_controller.rb @@ -0,0 +1,131 @@ +# 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 + layout 'base' + before_filter :require_login + + BLOCKS = { 'issues_assigned_to_me' => :label_assigned_to_me_issues, + 'issues_reported_by_me' => :label_reported_issues, + 'latest_news' => :label_news_latest, + 'calendar' => :label_calendar, + 'documents' => :label_document_plural + }.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 = self.logged_in_user + @blocks = @user.pref[:my_page_layout] || { 'left' => ['issues_assigned_to_me'], 'right' => ['issues_reported_by_me'] } + end + + # Edit user's account + def account + @user = self.logged_in_user + @pref = @user.pref + @user.attributes = params[:user] + @user.pref.attributes = params[:pref] + if request.post? and @user.save + set_localization + flash.now[:notice] = l(:notice_account_updated) + self.logged_in_user.reload + end + end + + # Change user's password + def change_password + @user = self.logged_in_user + flash[:notice] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id + 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) + else + render :action => 'account' + return + end + else + flash[:notice] = l(:notice_account_wrong_password) + end + redirect_to :action => 'account' + end + + # User's page layout configuration + def page_layout + @user = self.logged_in_user + @blocks = @user.pref[:my_page_layout] || { 'left' => ['issues_assigned_to_me'], 'right' => ['issues_reported_by_me'] } + 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 + @user = self.logged_in_user + block = params[:block] + # 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 = self.logged_in_user + @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/issue_relations/app/controllers/news_controller.rb b/issue_relations/app/controllers/news_controller.rb new file mode 100644 index 000000000..0f2ae85c6 --- /dev/null +++ b/issue_relations/app/controllers/news_controller.rb @@ -0,0 +1,60 @@ +# 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_project, :authorize + + def show + 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 = logged_in_user + 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 :controller => 'projects', :action => 'list_news', :id => @project + end + +private + def find_project + @news = News.find(params[:id]) + @project = @news.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/issue_relations/app/controllers/projects_controller.rb b/issue_relations/app/controllers/projects_controller.rb new file mode 100644 index 000000000..165333e78 --- /dev/null +++ b/issue_relations/app/controllers/projects_controller.rb @@ -0,0 +1,534 @@ +# 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' + before_filter :find_project, :authorize, :except => [ :index, :list, :add ] + before_filter :require_admin, :only => [ :add, :destroy ] + + helper :sort + include SortHelper + helper :custom_fields + include CustomFieldsHelper + helper :ifpdf + include IfpdfHelper + helper IssuesHelper + helper :queries + include QueriesHelper + + def index + list + render :action => 'list' unless request.xhr? + end + + # Lists public projects + def list + sort_init 'name', 'asc' + sort_update + @project_count = Project.count(["is_public=?", true]) + @project_pages = Paginator.new self, @project_count, + 15, + params['page'] + @projects = Project.find :all, :order => sort_clause, + :conditions => ["is_public=?", true], + :limit => @project_pages.items_per_page, + :offset => @project_pages.current.offset + + render :action => "list", :layout => false if request.xhr? + end + + # Add a new project + def add + @custom_fields = IssueCustomField.find(:all) + @root_projects = Project.find(:all, :conditions => "parent_id is null") + @project = Project.new(params[:project]) + if request.get? + @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project) } + else + @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] + @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) } + @project.custom_values = @custom_values + if params[:repository_enabled] && params[:repository_enabled] == "1" + @project.repository = Repository.new + @project.repository.attributes = params[:repository] + end + 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) + @members = @project.members.find(:all, :include => [:user, :role]) + @subprojects = @project.children if @project.children_count > 0 + @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "news.created_on DESC") + @trackers = Tracker.find(:all) + @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN issue_statuses ON issue_statuses.id = issues.status_id", :conditions => ["project_id=? and issue_statuses.is_closed=?", @project.id, false]) + @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id]) + end + + def settings + @root_projects = Project::find(:all, :conditions => ["parent_id is null and id <> ?", @project.id]) + @custom_fields = IssueCustomField.find(:all) + @issue_category ||= IssueCategory.new + @member ||= @project.members.new + @roles = Role.find(:all) + @users = User.find(:all) - @project.members.find(:all, :include => :user).collect{|m| m.user } + @custom_values ||= ProjectCustomField.find(:all).collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } + end + + # Edit @project + def edit + if request.post? + @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] + if params[:custom_fields] + @custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) } + @project.custom_values = @custom_values + end + if params[:repository_enabled] + case params[:repository_enabled] + when "0" + @project.repository = nil + when "1" + @project.repository ||= Repository.new + @project.repository.attributes = params[:repository] + end + 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 + + # Delete @project + def destroy + if request.post? and params[:confirm] + @project.destroy + redirect_to :controller => 'admin', :action => 'projects' + end + end + + # Add a new issue category to @project + def add_issue_category + if request.post? + @issue_category = @project.issue_categories.build(params[:issue_category]) + if @issue_category.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'settings', :id => @project + else + settings + render :action => 'settings' + 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', :id => @project + end + end + + # Add a new member to @project + def add_member + @member = @project.members.build(params[:member]) + if request.post? + if @member.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'settings', :id => @project + else + settings + render :action => 'settings' + end + end + end + + # Show members list of @project + def list_members + @members = @project.members + end + + # Add a new document to @project + def add_document + @categories = Enumeration::get_values('DCAT') + @document = @project.documents.build(params[:document]) + if request.post? and @document.save + # Save the attachments + params[:attachments].each { |a| + Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0 + } if params[:attachments] and params[:attachments].is_a? Array + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list_documents', :id => @project + end + end + + # Show documents list of @project + def list_documents + @documents = @project.documents.find :all, :include => :category + end + + # Add a new issue to @project + def add_issue + @tracker = Tracker.find(params[:tracker_id]) + @priorities = Enumeration::get_values('IPRI') + @issue = Issue.new(:project => @project, :tracker => @tracker) + if request.get? + @issue.start_date = Date.today + @custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } + else + @issue.attributes = params[:issue] + @issue.author_id = self.logged_in_user.id if self.logged_in_user + # Multiple file upload + @attachments = [] + params[:attachments].each { |a| + @attachments << Attachment.new(:container => @issue, :file => a, :author => logged_in_user) unless a.size == 0 + } if params[:attachments] and params[:attachments].is_a? Array + @custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } + @issue.custom_values = @custom_values + if @issue.save + @attachments.each(&:save) + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_issue_add(@issue) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled? + redirect_to :action => 'list_issues', :id => @project + end + end + end + + # Show filtered/sorted issues list of @project + def list_issues + sort_init 'issues.id', 'desc' + sort_update + + retrieve_query + + @results_per_page_options = [ 15, 25, 50, 100 ] + if params[:per_page] and @results_per_page_options.include? params[:per_page].to_i + @results_per_page = params[:per_page].to_i + session[:results_per_page] = @results_per_page + else + @results_per_page = session[:results_per_page] || 25 + end + + if @query.valid? + @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) + @issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page'] + @issues = Issue.find :all, :order => sort_clause, + :include => [ :author, :status, :tracker, :project ], + :conditions => @query.statement, + :limit => @issue_pages.items_per_page, + :offset => @issue_pages.current.offset + end + @trackers = Tracker.find :all + render :layout => false if request.xhr? + end + + # Export filtered/sorted issues list to CSV + def export_issues_csv + sort_init 'issues.id', 'desc' + sort_update + + retrieve_query + render :action => 'list_issues' and return unless @query.valid? + + @issues = Issue.find :all, :order => sort_clause, + :include => [ :author, :status, :tracker, :project, :custom_values ], + :conditions => @query.statement + + ic = Iconv.new('ISO-8859-1', 'UTF-8') + export = StringIO.new + CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| + # csv header fields + headers = [ "#", l(:field_status), l(:field_tracker), l(:field_subject), l(:field_author), l(:field_created_on), l(:field_updated_on) ] + for custom_field in @project.all_custom_fields + headers << custom_field.name + end + csv << headers.collect {|c| ic.iconv(c) } + # csv lines + @issues.each do |issue| + fields = [issue.id, issue.status.name, issue.tracker.name, issue.subject, issue.author.display_name, l_datetime(issue.created_on), l_datetime(issue.updated_on)] + for custom_field in @project.all_custom_fields + fields << (show_value issue.custom_value_for(custom_field)) + end + csv << fields.collect {|c| ic.iconv(c.to_s) } + end + end + export.rewind + send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv') + end + + # Export filtered/sorted issues to PDF + def export_issues_pdf + sort_init 'issues.id', 'desc' + sort_update + + retrieve_query + render :action => 'list_issues' and return unless @query.valid? + + @issues = Issue.find :all, :order => sort_clause, + :include => [ :author, :status, :tracker, :project, :custom_values ], + :conditions => @query.statement + + @options_for_rfpdf ||= {} + @options_for_rfpdf[:file_name] = "export.pdf" + render :layout => false + end + + def move_issues + @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids] + redirect_to :action => 'list_issues', :id => @project and return unless @issues + @projects = [] + # find projects to which the user is allowed to move the issue + @logged_in_user.memberships.each {|m| @projects << m.project if Permission.allowed_to_role("projects/move_issues", m.role_id)} + # issue can be moved to any tracker + @trackers = Tracker.find(:all) + if request.post? and params[:new_project_id] and params[:new_tracker_id] + new_project = Project.find(params[:new_project_id]) + new_tracker = Tracker.find(params[:new_tracker_id]) + @issues.each { |i| + # project dependent properties + unless i.project_id == new_project.id + i.category = nil + i.fixed_version = nil + end + # move the issue + i.project = new_project + i.tracker = new_tracker + i.save + } + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list_issues', :id => @project + end + end + + def add_query + @query = Query.new(params[:query]) + @query.project = @project + @query.user = logged_in_user + + params[:fields].each do |field| + @query.add_filter(field, params[:operators][field], params[:values][field]) + end if params[:fields] + + if request.post? and @query.save + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'reports', :action => 'issue_report', :id => @project + end + render :layout => false if request.xhr? + end + + # Add a news to @project + def add_news + @news = News.new(:project => @project) + if request.post? + @news.attributes = params[:news] + @news.author_id = self.logged_in_user.id if self.logged_in_user + if @news.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list_news', :id => @project + end + end + end + + # Show news list of @project + def list_news + @news_pages, @news = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "news.created_on DESC" + render :action => "list_news", :layout => false if request.xhr? + end + + def add_file + if request.post? + @version = @project.versions.find_by_id(params[:version_id]) + # Save the attachments + params[:attachments].each { |a| + Attachment.create(:container => @version, :file => a, :author => logged_in_user) unless a.size == 0 + } if params[:attachments] and params[:attachments].is_a? Array + redirect_to :controller => 'projects', :action => 'list_files', :id => @project + end + @versions = @project.versions + end + + def list_files + @versions = @project.versions + end + + # Show changelog for @project + def changelog + @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true]) + 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 ||= [] + @fixed_issues = @project.issues.find(:all, + :include => [ :fixed_version, :status, :tracker ], + :conditions => [ "issue_statuses.is_closed=? and issues.tracker_id in (#{@selected_tracker_ids.join(',')}) and issues.fixed_version_id is not null", true], + :order => "versions.effective_date DESC, issues.id DESC" + ) unless @selected_tracker_ids.empty? + @fixed_issues ||= [] + end + + def activity + 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 + + @date_from = Date.civil(@year, @month, 1) + @date_to = (@date_from >> 1)-1 + + @events_by_day = {} + + unless params[:show_issues] == "0" + @project.issues.find(:all, :include => [:author, :status], :conditions => ["issues.created_on>=? and issues.created_on<=?", @date_from, @date_to] ).each { |i| + @events_by_day[i.created_on.to_date] ||= [] + @events_by_day[i.created_on.to_date] << i + } + @show_issues = 1 + end + + unless params[:show_news] == "0" + @project.news.find(:all, :conditions => ["news.created_on>=? and news.created_on<=?", @date_from, @date_to], :include => :author ).each { |i| + @events_by_day[i.created_on.to_date] ||= [] + @events_by_day[i.created_on.to_date] << i + } + @show_news = 1 + end + + unless params[:show_files] == "0" + Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN versions ON versions.id = attachments.container_id", :conditions => ["attachments.container_type='Version' and versions.project_id=? and attachments.created_on>=? and attachments.created_on<=?", @project.id, @date_from, @date_to], :include => :author ).each { |i| + @events_by_day[i.created_on.to_date] ||= [] + @events_by_day[i.created_on.to_date] << i + } + @show_files = 1 + end + + unless params[:show_documents] == "0" + @project.documents.find(:all, :conditions => ["documents.created_on>=? and documents.created_on<=?", @date_from, @date_to] ).each { |i| + @events_by_day[i.created_on.to_date] ||= [] + @events_by_day[i.created_on.to_date] << i + } + Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN documents ON documents.id = attachments.container_id", :conditions => ["attachments.container_type='Document' and documents.project_id=? and attachments.created_on>=? and attachments.created_on<=?", @project.id, @date_from, @date_to], :include => :author ).each { |i| + @events_by_day[i.created_on.to_date] ||= [] + @events_by_day[i.created_on.to_date] << i + } + @show_documents = 1 + end + + render :layout => false if request.xhr? + end + + def calendar + if params[:year] and params[:year].to_i > 1900 + @year = params[:year].to_i + if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13 + @month = params[:month].to_i + end + end + @year ||= Date.today.year + @month ||= Date.today.month + + @date_from = Date.civil(@year, @month, 1) + @date_to = (@date_from >> 1)-1 + # start on monday + @date_from = @date_from - (@date_from.cwday-1) + # finish on sunday + @date_to = @date_to + (7-@date_to.cwday) + + @issues = @project.issues.find(:all, :include => [:tracker, :status, :assigned_to, :priority], :conditions => ["((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?))", @date_from, @date_to, @date_from, @date_to]) + render :layout => false if request.xhr? + end + + def gantt + 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 << 1).month + @year_from ||= (Date.today << 1).year + end + + @zoom = (params[:zoom].to_i > 0 and params[:zoom].to_i < 5) ? params[:zoom].to_i : 2 + @months = (params[:months].to_i > 0 and params[:months].to_i < 25) ? params[:months].to_i : 6 + + @date_from = Date.civil(@year_from, @month_from, 1) + @date_to = (@date_from >> @months) - 1 + @issues = @project.issues.find(:all, :order => "start_date, due_date", :include => [:tracker, :status, :assigned_to, :priority], :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)", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]) + + if params[:output]=='pdf' + @options_for_rfpdf ||= {} + @options_for_rfpdf[:file_name] = "gantt.pdf" + render :template => "projects/gantt.rfpdf", :layout => false + 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]) + @html_title = @project.name + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Retrieve query from session or build a new query + def retrieve_query + if params[:query_id] + @query = @project.queries.find(params[:query_id]) + else + if params[:set_filter] or !session[:query] or session[:query].project_id != @project.id + # 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] = @query + else + @query = session[:query] + end + end + end +end diff --git a/issue_relations/app/controllers/queries_controller.rb b/issue_relations/app/controllers/queries_controller.rb new file mode 100644 index 000000000..7f7f01fd3 --- /dev/null +++ b/issue_relations/app/controllers/queries_controller.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 QueriesController < ApplicationController + layout 'base' + before_filter :require_login, :find_query + + 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] + + if @query.save + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => @query + end + end + end + + def destroy + @query.destroy if request.post? + redirect_to :controller => 'reports', :action => 'issue_report', :id => @project + end + +private + def find_query + @query = Query.find(params[:id]) + @project = @query.project + # check if user is allowed to manage queries (same permission as add_query) + authorize('projects', 'add_query') + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/issue_relations/app/controllers/reports_controller.rb b/issue_relations/app/controllers/reports_controller.rb new file mode 100644 index 000000000..b825a8ac6 --- /dev/null +++ b/issue_relations/app/controllers/reports_controller.rb @@ -0,0 +1,167 @@ +# 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' + before_filter :find_project, :authorize + + def issue_report + @statuses = IssueStatus.find :all + + case params[:detail] + when "tracker" + @field = "tracker_id" + @rows = Tracker.find :all + @data = issues_by_tracker + @report_title = l(:field_tracker) + 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 "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" + else + @queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)] + @trackers = Tracker.find(:all) + @priorities = Enumeration::get_values('IPRI') + @categories = @project.issue_categories + @authors = @project.members.collect { |m| m.user } + issues_by_tracker + issues_by_priority + issues_by_category + issues_by_author + 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 + issues i, issue_statuses s, trackers 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_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 + issues i, issue_statuses s, enumerations 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 + issues i, issue_statuses s, issue_categories 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_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 + issues i, issue_statuses s, users 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 +end diff --git a/issue_relations/app/controllers/repositories_controller.rb b/issue_relations/app/controllers/repositories_controller.rb new file mode 100644 index 000000000..a10dfcc22 --- /dev/null +++ b/issue_relations/app/controllers/repositories_controller.rb @@ -0,0 +1,74 @@ +# 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 RepositoriesController < ApplicationController + layout 'base' + before_filter :find_project, :authorize + + def show + @entries = @repository.scm.entries('') + show_error and return unless @entries + @latest_revision = @entries.revisions.latest + end + + def browse + @entries = @repository.scm.entries(@path, @rev) + show_error and return unless @entries + end + + def revisions + @entry = @repository.scm.entry(@path, @rev) + @revisions = @repository.scm.revisions(@path, @rev) + show_error and return unless @entry && @revisions + end + + def entry + if 'raw' == params[:format] + content = @repository.scm.cat(@path, @rev) + show_error and return unless content + send_data content, :filename => @path.split('/').last + end + end + + def revision + @revisions = @repository.scm.revisions '', @rev, @rev, :with_paths => true + show_error and return unless @revisions + @revision = @revisions.first + end + + def diff + @rev_to = params[:rev_to] || (@rev-1) + @diff = @repository.scm.diff(params[:path], @rev, @rev_to) + show_error and return unless @diff + end + +private + def find_project + @project = Project.find(params[:id]) + @repository = @project.repository + @path = params[:path].squeeze('/').gsub(/^\//, '') if params[:path] + @path ||= '' + @rev = params[:rev].to_i if params[:rev] and params[:rev].to_i > 0 + rescue ActiveRecord::RecordNotFound + render_404 + end + + def show_error + flash.now[:notice] = l(:notice_scm_error) + render :nothing => true, :layout => true + end +end diff --git a/issue_relations/app/controllers/roles_controller.rb b/issue_relations/app/controllers/roles_controller.rb new file mode 100644 index 000000000..a4bee5dd1 --- /dev/null +++ b/issue_relations/app/controllers/roles_controller.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. + +class RolesController < ApplicationController + layout 'base' + before_filter :require_admin + + def index + list + render :action => 'list' unless request.xhr? + end + + def list + @role_pages, @roles = paginate :roles, :per_page => 10 + render :action => "list", :layout => false if request.xhr? + end + + def new + @role = Role.new(params[:role]) + if request.post? + @role.permissions = Permission.find(params[:permission_ids]) if params[:permission_ids] + if @role.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + end + end + @permissions = Permission.find(:all, :conditions => ["is_public=?", false], :order => 'sort ASC') + end + + def edit + @role = Role.find(params[:id]) + if request.post? and @role.update_attributes(params[:role]) + @role.permissions = Permission.find(params[:permission_ids] || []) + Permission.allowed_to_role_expired + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'list' + end + @permissions = Permission.find(:all, :conditions => ["is_public=?", false], :order => 'sort ASC') + end + + def destroy + @role = Role.find(params[:id]) + unless @role.members.empty? + flash[:notice] = 'Some members have this role. Can\'t delete it.' + else + @role.destroy + end + 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) + end + end + @roles = Role.find :all + @trackers = Tracker.find :all + @statuses = IssueStatus.find(:all, :include => :workflows) + end +end diff --git a/issue_relations/app/controllers/trackers_controller.rb b/issue_relations/app/controllers/trackers_controller.rb new file mode 100644 index 000000000..bbfb4f48b --- /dev/null +++ b/issue_relations/app/controllers/trackers_controller.rb @@ -0,0 +1,61 @@ +# 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 ], :redirect_to => { :action => :list } + + def list + @tracker_pages, @trackers = paginate :trackers, :per_page => 10 + render :action => "list", :layout => false if request.xhr? + end + + def new + @tracker = Tracker.new(params[:tracker]) + if request.post? and @tracker.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'list' + end + 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 destroy + @tracker = Tracker.find(params[:id]) + unless @tracker.issues.empty? + flash[:notice] = "This tracker contains issues and can\'t be deleted." + else + @tracker.destroy + end + redirect_to :action => 'list' + end + +end diff --git a/issue_relations/app/controllers/users_controller.rb b/issue_relations/app/controllers/users_controller.rb new file mode 100644 index 000000000..4c403a8d6 --- /dev/null +++ b/issue_relations/app/controllers/users_controller.rb @@ -0,0 +1,113 @@ +# 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 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 + @user_count = User.count + @user_pages = Paginator.new self, @user_count, + 15, + params['page'] + @users = User.find :all,:order => sort_clause, + :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 => $RDM_DEFAULT_LANG) + @custom_values = UserCustomField.find(:all).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).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } + @user.custom_values = @custom_values + if @user.save + 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).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).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) + redirect_to :action => 'list' + end + end + @auth_sources = AuthSource.find(:all) + @roles = Role.find :all + @projects = Project.find(:all) - @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 + + def destroy + User.find(params[:id]).destroy + redirect_to :action => 'list' + rescue + flash[:notice] = "Unable to delete user" + redirect_to :action => 'list' + end +end diff --git a/issue_relations/app/controllers/versions_controller.rb b/issue_relations/app/controllers/versions_controller.rb new file mode 100644 index 000000000..5c2dcc7f6 --- /dev/null +++ b/issue_relations/app/controllers/versions_controller.rb @@ -0,0 +1,58 @@ +# 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' + before_filter :find_project, :authorize + + def edit + if request.post? and @version.update_attributes(params[:version]) + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + end + + def destroy + @version.destroy + redirect_to :controller => 'projects', :action => 'settings', :id => @project + rescue + flash[:notice] = "Unable to delete version" + redirect_to :controller => 'projects', :action => 'settings', :id => @project + end + + def download + @attachment = @version.attachments.find(params[:attachment_id]) + @attachment.increment_download + send_file @attachment.diskfile, :filename => @attachment.filename + 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 + +private + def find_project + @version = Version.find(params[:id]) + @project = @version.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/issue_relations/app/controllers/welcome_controller.rb b/issue_relations/app/controllers/welcome_controller.rb new file mode 100644 index 000000000..ce45076d5 --- /dev/null +++ b/issue_relations/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 logged_in_user + @projects = Project.latest logged_in_user + end +end diff --git a/issue_relations/app/helpers/account_helper.rb b/issue_relations/app/helpers/account_helper.rb new file mode 100644 index 000000000..e18ab6ff4 --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/admin_helper.rb b/issue_relations/app/helpers/admin_helper.rb new file mode 100644 index 000000000..db2777392 --- /dev/null +++ b/issue_relations/app/helpers/admin_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 AdminHelper +end diff --git a/issue_relations/app/helpers/application_helper.rb b/issue_relations/app/helpers/application_helper.rb new file mode 100644 index 000000000..0f0d577ee --- /dev/null +++ b/issue_relations/app/helpers/application_helper.rb @@ -0,0 +1,192 @@ +# 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 ApplicationHelper + + # Return current logged in user or nil + def loggedin? + @logged_in_user + end + + # Return true if user is logged in and is admin, otherwise false + def admin_loggedin? + @logged_in_user and @logged_in_user.admin? + end + + # Return true if user is authorized for controller/action, otherwise false + def authorize_for(controller, action) + # check if action is allowed on public projects + if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ controller, action ] + return true + end + # check if user is authorized + if @logged_in_user and (@logged_in_user.admin? or Permission.allowed_to_role( "%s/%s" % [ controller, action ], @logged_in_user.role_for_project(@project.id) ) ) + return true + end + return false + 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], options[:action]) + end + + # Display a link to user's account page + def link_to_user(user) + link_to user.display_name, :controller => 'account', :action => 'show', :id => user + end + + def image_to_function(name, function, html_options = {}) + html_options.symbolize_keys! + tag(:input, html_options.merge({ + :type => "image", :src => image_path(name), + :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" + })) + end + + def format_date(date) + l_date(date) if date + end + + def format_time(time) + l_datetime(time) if time + 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, options={}, html_options={}) + html = '' + html << link_to_remote(('« ' + l(:label_previous)), + {:update => "content", :url => { :page => paginator.current.previous }}, + {:href => url_for(:action => 'list', :params => params.merge({:page => paginator.current.previous}))}) + ' ' if paginator.current.previous + + html << (pagination_links_each(paginator, options) do |n| + link_to_remote(n.to_s, + {:url => {:action => 'list', :params => params.merge({:page => n})}, :update => 'content'}, + {:href => url_for(:action => 'list', :params => params.merge({:page => n}))}) + end || '') + + html << ' ' + link_to_remote((l(:label_next) + ' »'), + {:update => "content", :url => { :page => paginator.current.next }}, + {:href => url_for(:action => 'list', :params => params.merge({:page => paginator.current.next}))}) if paginator.current.next + html + end + + def textilizable(text) + $RDM_TEXTILE_DISABLED ? simple_format(auto_link(h(text))) : RedCloth.new(h(text)).to_html + 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? + 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? + full_messages << "« " + v.custom_field.name + " » " + l(msg) + end + end + end + content_tag("div", + content_tag( + options[:header_tag] || "h2", 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 + [["(auto)", ""]] + (GLoc.valid_languages.sort {|x,y| x.to_s <=> y.to_s }).collect {|lang| [ l_lang_name(lang.to_s, lang), lang.to_s]} + 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].store :class, "tabular" + form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) + 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 calendar_for(field_id) + 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 +end + +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(("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_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/issue_relations/app/helpers/auth_sources_helper.rb b/issue_relations/app/helpers/auth_sources_helper.rb new file mode 100644 index 000000000..d47e9856a --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/custom_fields_helper.rb b/issue_relations/app/helpers/custom_fields_helper.rb new file mode 100644 index 000000000..75b8a45cd --- /dev/null +++ b/issue_relations/app/helpers/custom_fields_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. + +module CustomFieldsHelper + + # 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 "string", "int" + text_field 'custom_value', 'value', :name => field_name, :id => field_id + 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, :cols => 60, :rows => 3 + when "bool" + check_box 'custom_value', 'value', :name => field_name, :id => field_id + when "list" + select 'custom_value', 'value', custom_field.possible_values.split('|'), { :include_blank => true }, :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 + case field_format + when "date" + value.empty? ? "" : l_date(value.to_date) + 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/issue_relations/app/helpers/documents_helper.rb b/issue_relations/app/helpers/documents_helper.rb new file mode 100644 index 000000000..c9897647d --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/enumerations_helper.rb b/issue_relations/app/helpers/enumerations_helper.rb new file mode 100644 index 000000000..11a216a82 --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/feeds_helper.rb b/issue_relations/app/helpers/feeds_helper.rb new file mode 100644 index 000000000..850bd44f5 --- /dev/null +++ b/issue_relations/app/helpers/feeds_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 FeedsHelper +end diff --git a/issue_relations/app/helpers/help_helper.rb b/issue_relations/app/helpers/help_helper.rb new file mode 100644 index 000000000..bb629316c --- /dev/null +++ b/issue_relations/app/helpers/help_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 HelpHelper +end diff --git a/issue_relations/app/helpers/ifpdf_helper.rb b/issue_relations/app/helpers/ifpdf_helper.rb new file mode 100644 index 000000000..a0dab0f94 --- /dev/null +++ b/issue_relations/app/helpers/ifpdf_helper.rb @@ -0,0 +1,48 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'iconv' + +module IfpdfHelper + + class IFPDF < FPDF + + attr_accessor :footer_date + + def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='') + @ic ||= Iconv.new('ISO-8859-1', 'UTF-8') + txt = begin + @ic.iconv(txt) + rescue + txt + end + super w,h,txt,border,ln,align,fill,link + end + + def Footer + SetFont('Helvetica', '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/issue_relations/app/helpers/issue_categories_helper.rb b/issue_relations/app/helpers/issue_categories_helper.rb new file mode 100644 index 000000000..997d830d7 --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/issue_statuses_helper.rb b/issue_relations/app/helpers/issue_statuses_helper.rb new file mode 100644 index 000000000..17704b7ba --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/issues_helper.rb b/issue_relations/app/helpers/issues_helper.rb new file mode 100644 index 000000000..875e20001 --- /dev/null +++ b/issue_relations/app/helpers/issues_helper.rb @@ -0,0 +1,78 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module IssuesHelper + + 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 + 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?) + value = content_tag("i", h(value)) if value + end + + if detail.value and !detail.value.empty? + if old_value + label + " " + l(:text_journal_changed, old_value, value) + else + label + " " + l(:text_journal_set_to, value) + end + else + label + " " + l(:text_journal_deleted) + " (#{old_value})" + end + end + + def relation_types_for_select + IssueRelation::TYPES.sort {|a,b| a[1][:order]<=>b[1][:order]}.collect { |k| [ l(k[1][:name]), k[0] ] } + end +end diff --git a/issue_relations/app/helpers/members_helper.rb b/issue_relations/app/helpers/members_helper.rb new file mode 100644 index 000000000..8bf90913d --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/my_helper.rb b/issue_relations/app/helpers/my_helper.rb new file mode 100644 index 000000000..9098f67bc --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/news_helper.rb b/issue_relations/app/helpers/news_helper.rb new file mode 100644 index 000000000..f4a633f4b --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/projects_helper.rb b/issue_relations/app/helpers/projects_helper.rb new file mode 100644 index 000000000..0c85ce24c --- /dev/null +++ b/issue_relations/app/helpers/projects_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 ProjectsHelper +end diff --git a/issue_relations/app/helpers/queries_helper.rb b/issue_relations/app/helpers/queries_helper.rb new file mode 100644 index 000000000..1c0b59570 --- /dev/null +++ b/issue_relations/app/helpers/queries_helper.rb @@ -0,0 +1,6 @@ +module QueriesHelper + + def operators_for_select(filter_type) + Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]} + end +end diff --git a/issue_relations/app/helpers/reports_helper.rb b/issue_relations/app/helpers/reports_helper.rb new file mode 100644 index 000000000..ed7fd7884 --- /dev/null +++ b/issue_relations/app/helpers/reports_helper.rb @@ -0,0 +1,32 @@ +# 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 + } unless criteria.nil? + a = a + row["total"].to_i if match == 1 + } unless data.nil? + a + end + +end diff --git a/issue_relations/app/helpers/repositories_helper.rb b/issue_relations/app/helpers/repositories_helper.rb new file mode 100644 index 000000000..2c7dcdd53 --- /dev/null +++ b/issue_relations/app/helpers/repositories_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 RepositoriesHelper +end diff --git a/issue_relations/app/helpers/roles_helper.rb b/issue_relations/app/helpers/roles_helper.rb new file mode 100644 index 000000000..8ae339053 --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/sort_helper.rb b/issue_relations/app/helpers/sort_helper.rb new file mode 100644 index 000000000..300fbfe54 --- /dev/null +++ b/issue_relations/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=nil) + 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 = 'desc' # changed for desc order by default + end + caption = titleize(Inflector::humanize(column)) unless caption + params = {:params => {:sort_key => column, :sort_order => order}} + link_to_remote(caption, + {:update => "content", :url => { :sort_key => column, :sort_order => order}}, + {:href => url_for(:params => { :sort_key => column, :sort_order => order})}) + + (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 = {}) + if options[:caption] + caption = options[:caption] + options.delete(:caption) + else + caption = titleize(Inflector::humanize(column)) + end + options[:title]= "Sort by #{caption}" unless options[:title] + content_tag('th', sort_link(column, caption), 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/issue_relations/app/helpers/trackers_helper.rb b/issue_relations/app/helpers/trackers_helper.rb new file mode 100644 index 000000000..839327efe --- /dev/null +++ b/issue_relations/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/issue_relations/app/helpers/users_helper.rb b/issue_relations/app/helpers/users_helper.rb new file mode 100644 index 000000000..035db3d00 --- /dev/null +++ b/issue_relations/app/helpers/users_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 UsersHelper +end diff --git a/issue_relations/app/helpers/versions_helper.rb b/issue_relations/app/helpers/versions_helper.rb new file mode 100644 index 000000000..e2724fe84 --- /dev/null +++ b/issue_relations/app/helpers/versions_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 VersionsHelper +end diff --git a/issue_relations/app/helpers/welcome_helper.rb b/issue_relations/app/helpers/welcome_helper.rb new file mode 100644 index 000000000..cace5f542 --- /dev/null +++ b/issue_relations/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/issue_relations/app/models/attachment.rb b/issue_relations/app/models/attachment.rb new file mode 100644 index 000000000..cdf5a3e47 --- /dev/null +++ b/issue_relations/app/models/attachment.rb @@ -0,0 +1,89 @@ +# 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 "digest/md5" + +class Attachment < ActiveRecord::Base + belongs_to :container, :polymorphic => true + belongs_to :author, :class_name => "User", :foreign_key => "author_id" + + @@max_size = $RDM_ATTACHMENT_MAX_SIZE || 5*1024*1024 + cattr_reader :max_size + + validates_presence_of :container, :filename + validates_inclusion_of :filesize, :in => 1..@@max_size + + def file=(incomming_file) + unless incomming_file.nil? + @temp_file = incomming_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 + 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 + 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 + "#{$RDM_STORAGE_PATH}/#{self.disk_filename}" + end + + def increment_download + increment!(:downloads) + end + + # returns last created projects + def self.most_downloaded + find(:all, :limit => 5, :order => "downloads DESC") + 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, underscore or periods with underscore + @filename = just_filename.gsub(/[^\w\.\-]/,'_') + end + +end diff --git a/issue_relations/app/models/auth_source.rb b/issue_relations/app/models/auth_source.rb new file mode 100644 index 000000000..47eec106d --- /dev/null +++ b/issue_relations/app/models/auth_source.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. + +class AuthSource < ActiveRecord::Base + has_many :users + + validates_presence_of :name + validates_uniqueness_of :name + + 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/issue_relations/app/models/auth_source_ldap.rb b/issue_relations/app/models/auth_source_ldap.rb new file mode 100644 index 000000000..895cf1c63 --- /dev/null +++ b/issue_relations/app/models/auth_source_ldap.rb @@ -0,0 +1,79 @@ +# 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 + + def after_initialize + self.port = 389 if self.port == 0 + end + + def authenticate(login, password) + 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, + :attributes=> ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]) 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 ] + 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) + Net::LDAP.new( {:host => self.host, + :port => self.port, + :auth => { :method => :simple, :username => Iconv.new('iso-8859-15', 'utf-8').iconv(ldap_user), :password => Iconv.new('iso-8859-15', 'utf-8').iconv(ldap_password) }} + ) + 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/issue_relations/app/models/comment.rb b/issue_relations/app/models/comment.rb new file mode 100644 index 000000000..27e5c511e --- /dev/null +++ b/issue_relations/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, :comment +end diff --git a/issue_relations/app/models/custom_field.rb b/issue_relations/app/models/custom_field.rb new file mode 100644 index 000000000..3626165b5 --- /dev/null +++ b/issue_relations/app/models/custom_field.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 CustomField < ActiveRecord::Base + has_many :custom_values, :dependent => :delete_all + + FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 }, + "text" => { :name => :label_text, :order => 2 }, + "int" => { :name => :label_integer, :order => 3 }, + "list" => { :name => :label_list, :order => 4 }, + "date" => { :name => :label_date, :order => 5 }, + "bool" => { :name => :label_boolean, :order => 6 } + }.freeze + + validates_presence_of :name, :field_format + validates_uniqueness_of :name + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys + validates_presence_of :possible_values, :if => Proc.new { |field| field.field_format == "list" } + + # 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/issue_relations/app/models/custom_value.rb b/issue_relations/app/models/custom_value.rb new file mode 100644 index 000000000..015ccd244 --- /dev/null +++ b/issue_relations/app/models/custom_value.rb @@ -0,0 +1,38 @@ +# 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 + +protected + def validate + errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.empty? + errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.empty? 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 and value.length > 0 + errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length + case custom_field.field_format + when "int" + errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[0-9]*$/ + when "date" + errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.empty? + when "list" + errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.split('|').include? value or value.empty? + end + end +end + diff --git a/issue_relations/app/models/document.rb b/issue_relations/app/models/document.rb new file mode 100644 index 000000000..40a9765a4 --- /dev/null +++ b/issue_relations/app/models/document.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 Document < ActiveRecord::Base + belongs_to :project + belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" + has_many :attachments, :as => :container, :dependent => :destroy + + validates_presence_of :project, :title, :category +end diff --git a/issue_relations/app/models/enumeration.rb b/issue_relations/app/models/enumeration.rb new file mode 100644 index 000000000..0d6554f82 --- /dev/null +++ b/issue_relations/app/models/enumeration.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. + +class Enumeration < ActiveRecord::Base + before_destroy :check_integrity + + validates_presence_of :opt, :name + validates_uniqueness_of :name, :scope => [:opt] + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + OPTIONS = { + "IPRI" => :enumeration_issue_priorities, + "DCAT" => :enumeration_doc_categories + }.freeze + + def self.get_values(option) + find(:all, :conditions => ['opt=?', option]) + end + + def option_name + OPTIONS[self.opt] + 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]) + end + end +end diff --git a/issue_relations/app/models/issue.rb b/issue_relations/app/models/issue.rb new file mode 100644 index 000000000..b59d9ef18 --- /dev/null +++ b/issue_relations/app/models/issue.rb @@ -0,0 +1,104 @@ +# 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 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 :relations_to, :class_name => 'IssueRelation', :dependent => :destroy + has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :destroy + + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :custom_fields, :through => :custom_values + + validates_presence_of :subject, :description, :priority, :tracker, :author, :status + validates_inclusion_of :done_ratio, :in => 0..100 + validates_associated :custom_values, :on => :update + + # set default status for new issues + def before_validation + self.status = IssueStatus.default if new_record? + 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 + end + + #def before_create + # build_history + #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| + @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) unless @custom_values_before_change[c.custom_field_id]==c.value + } + @current_journal.save unless @current_journal.details.empty? and @current_journal.notes.empty? + end + end + + def long_id + "%05d" % self.id + 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 + @custom_values_before_change = {} + self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } + @current_journal + end + +private + # Creates an history for the issue + #def build_history + # @history = self.histories.build + # @history.status = self.status + # @history.author = self.author + #end +end diff --git a/issue_relations/app/models/issue_category.rb b/issue_relations/app/models/issue_category.rb new file mode 100644 index 000000000..74adb8f52 --- /dev/null +++ b/issue_relations/app/models/issue_category.rb @@ -0,0 +1,29 @@ +# 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 + before_destroy :check_integrity + belongs_to :project + + validates_presence_of :name + validates_uniqueness_of :name, :scope => [:project_id] + +private + def check_integrity + raise "Can't delete category" if Issue.find(:first, :conditions => ["category_id=?", self.id]) + end +end diff --git a/issue_relations/app/models/issue_custom_field.rb b/issue_relations/app/models/issue_custom_field.rb new file mode 100644 index 000000000..209ae206b --- /dev/null +++ b/issue_relations/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 => "custom_fields_projects", :foreign_key => "custom_field_id" + has_and_belongs_to_many :trackers, :join_table => "custom_fields_trackers", :foreign_key => "custom_field_id" + has_many :issues, :through => :issue_custom_values + + def type_name + :label_issue_plural + end +end + diff --git a/issue_relations/app/models/issue_history.rb b/issue_relations/app/models/issue_history.rb new file mode 100644 index 000000000..4b6682600 --- /dev/null +++ b/issue_relations/app/models/issue_history.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 IssueHistory < ActiveRecord::Base + belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :issue + + validates_presence_of :status +end diff --git a/issue_relations/app/models/issue_status.rb b/issue_relations/app/models/issue_status.rb new file mode 100644 index 000000000..b821df258 --- /dev/null +++ b/issue_relations/app/models/issue_status.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 IssueStatus < ActiveRecord::Base + before_destroy :check_integrity + has_many :workflows, :foreign_key => "old_status_id" + + validates_presence_of :name + validates_uniqueness_of :name + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + validates_length_of :html_color, :is => 6 + validates_format_of :html_color, :with => /^[a-f0-9]*$/i + + def before_save + IssueStatus.update_all "is_default=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 + def new_statuses_allowed_to(role, tracker) + statuses = [] + for workflow in self.workflows + statuses << workflow.new_status if workflow.role_id == role.id and workflow.tracker_id == tracker.id + end unless role.nil? or tracker.nil? + statuses + end + +private + def check_integrity + raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) or IssueHistory.find(:first, :conditions => ["status_id=?", self.id]) + end +end diff --git a/issue_relations/app/models/journal.rb b/issue_relations/app/models/journal.rb new file mode 100644 index 000000000..18a6ec083 --- /dev/null +++ b/issue_relations/app/models/journal.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 Journal < ActiveRecord::Base + belongs_to :journalized, :polymorphic => true + belongs_to :user + has_many :details, :class_name => "JournalDetail", :dependent => :delete_all +end diff --git a/issue_relations/app/models/journal_detail.rb b/issue_relations/app/models/journal_detail.rb new file mode 100644 index 000000000..784e98bf7 --- /dev/null +++ b/issue_relations/app/models/journal_detail.rb @@ -0,0 +1,20 @@ +# 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 +end diff --git a/issue_relations/app/models/mailer.rb b/issue_relations/app/models/mailer.rb new file mode 100644 index 000000000..968beb49e --- /dev/null +++ b/issue_relations/app/models/mailer.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 Mailer < ActionMailer::Base + + helper IssuesHelper + + def issue_add(issue) + # Sends to all project members + @recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }.compact + @from = $RDM_MAIL_FROM + @subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}" + @body['issue'] = issue + end + + def issue_edit(journal) + # Sends to all project members + issue = journal.journalized + @recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }.compact + @from = $RDM_MAIL_FROM + @subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}" + @body['issue'] = issue + @body['journal']= journal + end + + def lost_password(token) + @recipients = token.user.mail + @from = $RDM_MAIL_FROM + @subject = l(:mail_subject_lost_password) + @body['token'] = token + end + + def register(token) + @recipients = token.user.mail + @from = $RDM_MAIL_FROM + @subject = l(:mail_subject_register) + @body['token'] = token + end +end diff --git a/issue_relations/app/models/member.rb b/issue_relations/app/models/member.rb new file mode 100644 index 000000000..1214b6443 --- /dev/null +++ b/issue_relations/app/models/member.rb @@ -0,0 +1,29 @@ +# 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 name + self.user.display_name + end +end diff --git a/issue_relations/app/models/news.rb b/issue_relations/app/models/news.rb new file mode 100644 index 000000000..89e94f1ce --- /dev/null +++ b/issue_relations/app/models/news.rb @@ -0,0 +1,29 @@ +# 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 + + # 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.created_on DESC") + end +end diff --git a/issue_relations/app/models/permission.rb b/issue_relations/app/models/permission.rb new file mode 100644 index 000000000..65b9253c7 --- /dev/null +++ b/issue_relations/app/models/permission.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. + +class Permission < ActiveRecord::Base + has_and_belongs_to_many :roles + + validates_presence_of :controller, :action, :description + + GROUPS = { + 100 => :label_project, + 200 => :label_member_plural, + 300 => :label_version_plural, + 400 => :label_issue_category_plural, + 600 => :label_query_plural, + 1000 => :label_issue_plural, + 1100 => :label_news_plural, + 1200 => :label_document_plural, + 1300 => :label_attachment_plural, + 1400 => :label_repository + }.freeze + + @@cached_perms_for_public = nil + @@cached_perms_for_roles = nil + + def name + self.controller + "/" + self.action + end + + def group_id + (self.sort / 100)*100 + end + + def self.allowed_to_public(action) + @@cached_perms_for_public ||= find(:all, :conditions => ["is_public=?", true]).collect {|p| "#{p.controller}/#{p.action}"} + @@cached_perms_for_public.include? action + end + + def self.allowed_to_role(action, role) + @@cached_perms_for_roles ||= + begin + perms = {} + find(:all, :include => :roles).each {|p| perms.store "#{p.controller}/#{p.action}", p.roles.collect {|r| r.id } } + perms + end + allowed_to_public(action) or (@@cached_perms_for_roles[action] and @@cached_perms_for_roles[action].include? role) + end + + def self.allowed_to_role_expired + @@cached_perms_for_roles = nil + end +end diff --git a/issue_relations/app/models/project.rb b/issue_relations/app/models/project.rb new file mode 100644 index 000000000..7db061a62 --- /dev/null +++ b/issue_relations/app/models/project.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 Project < ActiveRecord::Base + has_many :versions, :dependent => :destroy, :order => "versions.effective_date DESC, versions.name DESC" + has_many :members, :dependent => :delete_all, :include => :user, :conditions => "users.status=#{User::STATUS_ACTIVE}" + has_many :users, :through => :members + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_many :issues, :dependent => :destroy, :order => "issues.created_on DESC", :include => [:status, :tracker] + 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 => "issue_categories.name" + has_one :repository, :dependent => :destroy + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_projects', :association_foreign_key => 'custom_field_id' + acts_as_tree :order => "name", :counter_cache => true + + validates_presence_of :name, :description + validates_uniqueness_of :name + validates_associated :custom_values, :on => :update + validates_associated :repository + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + + # 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 => "projects.created_on DESC") + end + + def self.visible_by(user=nil) + if user && !user.memberships.empty? + return ["projects.is_public = ? or projects.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')})", true] + else + return ["projects.is_public = ?", true] + end + 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) + tracker.custom_fields.find(:all, :include => :projects, + :conditions => ["is_for_all=? or project_id=?", true, self.id]) + #(CustomField.for_all + custom_fields).uniq + end + + def all_custom_fields + @all_custom_fields ||= IssueCustomField.find(:all, :include => :projects, + :conditions => ["is_for_all=? or project_id=?", true, self.id]) + 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 projects_count > 0 + end +end diff --git a/issue_relations/app/models/project_custom_field.rb b/issue_relations/app/models/project_custom_field.rb new file mode 100644 index 000000000..baa533812 --- /dev/null +++ b/issue_relations/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/issue_relations/app/models/query.rb b/issue_relations/app/models/query.rb new file mode 100644 index 000000000..4ac34bd6d --- /dev/null +++ b/issue_relations/app/models/query.rb @@ -0,0 +1,166 @@ +# 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 Query < ActiveRecord::Base + belongs_to :project + belongs_to :user + serialize :filters + + attr_protected :project, :user + + validates_presence_of :name, :on => :save + + @@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, + ">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 => [ "=", "!", "!*", "*" ], + :date => [ "t+", "t+", "t", ">t-", " [ ">t-", " [ "~", "!~" ] } + + cattr_reader :operators_by_filter_type + + def initialize(attributes = nil) + super attributes + self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } + self.is_public = true + end + + def validate + filters.each_key do |field| + errors.add field.gsub(/\_id$/, ""), :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"].include? operator_for(field) + end if filters + end + + def available_filters + return @available_filters if @available_filters + @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all).collect{|s| [s.name, s.id.to_s] } }, + "tracker_id" => { :type => :list, :order => 2, :values => Tracker.find(:all).collect{|s| [s.name, s.id.to_s] } }, + "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } }, + "subject" => { :type => :text, :order => 7 }, + "created_on" => { :type => :date_past, :order => 8 }, + "updated_on" => { :type => :date_past, :order => 9 }, + "start_date" => { :type => :date, :order => 10 }, + "due_date" => { :type => :date, :order => 11 } } + unless project.nil? + # project specific filters + @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => @project.users.collect{|s| [s.name, s.id.to_s] } } + @available_filters["author_id"] = { :type => :list, :order => 5, :values => @project.users.collect{|s| [s.name, s.id.to_s] } } + @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } + # 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 statement + sql = "1=1" + sql << " AND issues.project_id=%d" % project.id if project + filters.each_key do |field| + v = values_for field + next unless v and !v.empty? + sql = sql + " AND " unless sql.empty? + case operator_for field + when "=" + sql = sql + "issues.#{field} IN (" + v.each(&:to_i).join(",") + ")" + when "!" + sql = sql + "issues.#{field} NOT IN (" + v.each(&:to_i).join(",") + ")" + when "!*" + sql = sql + "issues.#{field} IS NULL" + when "*" + sql = sql + "issues.#{field} IS NOT NULL" + when "o" + sql = sql + "issue_statuses.is_closed=#{connection.quoted_false}" if field == "status_id" + when "c" + sql = sql + "issue_statuses.is_closed=#{connection.quoted_true}" if field == "status_id" + when ">t-" + sql = sql + "issues.#{field} >= '%s'" % connection.quoted_date(Date.today - v.first.to_i) + when "t+" + sql = sql + "issues.#{field} >= '" + (Date.today + v.first.to_i).strftime("%Y-%m-%d") + "'" + when " /^(http|https|svn|file):\/\/.+/i + + @scm = nil + + def scm + @scm ||= SvnRepos::Base.new url + end +end diff --git a/issue_relations/app/models/role.rb b/issue_relations/app/models/role.rb new file mode 100644 index 000000000..aea402f46 --- /dev/null +++ b/issue_relations/app/models/role.rb @@ -0,0 +1,32 @@ +# 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 + before_destroy :check_integrity + has_and_belongs_to_many :permissions + has_many :workflows, :dependent => :delete_all + has_many :members + + validates_presence_of :name + validates_uniqueness_of :name + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + +private + def check_integrity + raise "Can't delete role" if Member.find(:first, :conditions =>["role_id=?", self.id]) + end +end diff --git a/issue_relations/app/models/svn_repos.rb b/issue_relations/app/models/svn_repos.rb new file mode 100644 index 000000000..7c6f5e01a --- /dev/null +++ b/issue_relations/app/models/svn_repos.rb @@ -0,0 +1,214 @@ +# 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 'rexml/document' + +module SvnRepos + + class CommandFailed < StandardError #:nodoc: + end + + class Base + @url = nil + @login = nil + @password = nil + + def initialize(url, login=nil, password=nil) + @url = url + @login = login if login && !login.empty? + @password = (password || "") if @login + 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) + e = entries(path, identifier) + e ? e.first : 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 = 'HEAD' unless identifier and identifier > 0 + entries = Entries.new + cmd = "svn list --xml #{target(path)}@#{identifier}" + shellout(cmd) do |io| + begin + doc = REXML::Document.new(io) + 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), + :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "anonymous") + }) + }) + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + entries.sort_by_name + rescue Errno::ENOENT => e + raise CommandFailed + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path ||= '' + identifier_from = 'HEAD' 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 = "svn log --xml -r #{identifier_from}:#{identifier_to} " + 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 + } + end + paths.sort! { |x,y| x[:path] <=> y[:path] } + + revisions << Revision.new({:identifier => logentry.attributes['revision'], + :author => (logentry.elements['author'] ? logentry.elements['author'].text : "anonymous"), + :time => Time.parse(logentry.elements['date'].text), + :message => logentry.elements['msg'].text, + :paths => paths + }) + end + rescue + end + end + return nil if $? && $?.exitstatus != 0 + revisions + rescue Errno::ENOENT => e + raise CommandFailed + end + + def diff(path, identifier_from, identifier_to=nil) + path ||= '' + if identifier_to and identifier_to.to_i > 0 + identifier_to = identifier_to.to_i + else + identifier_to = identifier_from.to_i - 1 + end + cmd = "svn diff -r " + cmd << "#{identifier_to}:" + cmd << "#{identifier_from}" + cmd << "#{target(path)}@#{identifier_from}" + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + return nil if $? && $?.exitstatus != 0 + diff + rescue Errno::ENOENT => e + raise CommandFailed + end + + def cat(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "svn cat #{target(path)}@#{identifier}" + cat = nil + shellout(cmd) do |io| + cat = io.read + end + return nil if $? && $?.exitstatus != 0 + cat + rescue Errno::ENOENT => e + raise CommandFailed + end + + private + def target(path) + " \"" << "#{@url}/#{path}".gsub(/["'?<>\*]/, '') << "\"" + end + + def logger + RAILS_DEFAULT_LOGGER + end + + def shellout(cmd, &block) + logger.debug "Shelling out: #{cmd}" if logger && logger.debug? + IO.popen(cmd) do |io| + block.call(io) if block_given? + 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}) + 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 + end + + class Revisions < Array + def latest + sort {|x,y| x.time <=> y.time}.last + end + end + + class Revision + attr_accessor :identifier, :author, :time, :message, :paths + def initialize(attributes={}) + self.identifier = attributes[:identifier] + self.author = attributes[:author] + self.time = attributes[:time] + self.message = attributes[:message] || "" + self.paths = attributes[:paths] + end + end +end \ No newline at end of file diff --git a/issue_relations/app/models/token.rb b/issue_relations/app/models/token.rb new file mode 100644 index 000000000..98745d29e --- /dev/null +++ b/issue_relations/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 ["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/issue_relations/app/models/tracker.rb b/issue_relations/app/models/tracker.rb new file mode 100644 index 000000000..8790bf725 --- /dev/null +++ b/issue_relations/app/models/tracker.rb @@ -0,0 +1,32 @@ +# 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 + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_trackers', :association_foreign_key => 'custom_field_id' + + validates_presence_of :name + validates_uniqueness_of :name + validates_format_of :name, :with => /^[\w\s\'\-]*$/i + +private + def check_integrity + raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) + end +end diff --git a/issue_relations/app/models/user.rb b/issue_relations/app/models/user.rb new file mode 100644 index 000000000..b798860d2 --- /dev/null +++ b/issue_relations/app/models/user.rb @@ -0,0 +1,130 @@ +# 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 "digest/sha1" + +class User < ActiveRecord::Base + has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :dependent => :delete_all + has_many :projects, :through => :memberships + has_many :custom_values, :dependent => :delete_all, :as => :customized + has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' + 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 + validates_uniqueness_of :login, :mail + # Login must contain lettres, numbers, underscores only + validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i + validates_format_of :login, :with => /^[a-z0-9_\-@\.]+$/i + validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i + # Password length between 4 and 12 + validates_length_of :password, :in => 4..12, :allow_nil => true + validates_confirmation_of :password, :allow_nil => true + validates_associated :custom_values, :on => :update + + # Account statuses + STATUS_ACTIVE = 1 + STATUS_REGISTERED = 2 + STATUS_LOCKED = 3 + + def before_save + # update hashed_password if password was set + self.hashed_password = User.hash_password(self.password) if self.password + end + + # Returns the user that matches provided login and password, or nil + def self.try_to_login(login, password) + 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 = $RDM_DEFAULT_LANG + if onthefly.save + user = find(:first, :conditions => ["login=?", login]) + logger.info("User '#{user.login}' created on the fly.") if logger + 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 display_name + firstname + " " + lastname + end + + def name + display_name + 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 role_for_project(project_id) + @role_for_projects ||= + begin + roles = {} + self.memberships.each { |m| roles.store m.project_id, m.role_id } + roles + end + @role_for_projects[project_id] + end + + def pref + self.preference ||= UserPreference.new(:user => self) + end + +private + # Return password digest + def self.hash_password(clear_password) + Digest::SHA1.hexdigest(clear_password || "") + end +end diff --git a/issue_relations/app/models/user_custom_field.rb b/issue_relations/app/models/user_custom_field.rb new file mode 100644 index 000000000..866234a7f --- /dev/null +++ b/issue_relations/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/issue_relations/app/models/user_preference.rb b/issue_relations/app/models/user_preference.rb new file mode 100644 index 000000000..5240c9757 --- /dev/null +++ b/issue_relations/app/models/user_preference.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 UserPreference < ActiveRecord::Base + belongs_to :user + serialize :others, Hash + + attr_protected :others + + def initialize(attributes = nil) + super + self.others ||= {} + end + + def [](attr_name) + if attribute_present? attr_name + super + else + others[attr_name] + end + end + + def []=(attr_name, value) + if attribute_present? attr_name + super + else + others.store attr_name, value + end + end +end diff --git a/issue_relations/app/models/version.rb b/issue_relations/app/models/version.rb new file mode 100644 index 000000000..71a8a8807 --- /dev/null +++ b/issue_relations/app/models/version.rb @@ -0,0 +1,32 @@ +# 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_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date + +private + def check_integrity + raise "Can't delete version" if self.fixed_issues.find(:first) + end +end diff --git a/issue_relations/app/models/workflow.rb b/issue_relations/app/models/workflow.rb new file mode 100644 index 000000000..22c873fc7 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/account/login.rhtml b/issue_relations/app/views/account/login.rhtml new file mode 100644 index 000000000..8f092b525 --- /dev/null +++ b/issue_relations/app/views/account/login.rhtml @@ -0,0 +1,18 @@ +
+ +
\ No newline at end of file diff --git a/issue_relations/app/views/account/lost_password.rhtml b/issue_relations/app/views/account/lost_password.rhtml new file mode 100644 index 000000000..3f32e7153 --- /dev/null +++ b/issue_relations/app/views/account/lost_password.rhtml @@ -0,0 +1,14 @@ +
+ +
\ No newline at end of file diff --git a/issue_relations/app/views/account/password_recovery.rhtml b/issue_relations/app/views/account/password_recovery.rhtml new file mode 100644 index 000000000..39a8071a9 --- /dev/null +++ b/issue_relations/app/views/account/password_recovery.rhtml @@ -0,0 +1,21 @@ +
+ +
\ No newline at end of file diff --git a/issue_relations/app/views/account/register.rhtml b/issue_relations/app/views/account/register.rhtml new file mode 100644 index 000000000..3101bd5df --- /dev/null +++ b/issue_relations/app/views/account/register.rhtml @@ -0,0 +1,46 @@ +

<%=l(:label_register)%>

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

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

+ +

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

+ +

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

+<%= check_box 'user', 'mail_notification' %>

+ +
+ +<%= submit_tag l(:button_submit) %> +<%= end_form_tag %> + +<% 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 %> diff --git a/issue_relations/app/views/account/show.rhtml b/issue_relations/app/views/account/show.rhtml new file mode 100644 index 000000000..c686b709c --- /dev/null +++ b/issue_relations/app/views/account/show.rhtml @@ -0,0 +1,26 @@ +

<%= @user.display_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 %> +
+

+ +

<%=l(:label_project_plural)%>

+

+<% for membership in @user.memberships %> + <%= membership.project.name %> (<%= membership.role.name %>, <%= format_date(membership.created_on) %>) +
+<% end %> +

+ +

<%=l(:label_activity)%>

+

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

\ No newline at end of file diff --git a/issue_relations/app/views/admin/index.rhtml b/issue_relations/app/views/admin/index.rhtml new file mode 100644 index 000000000..901134c27 --- /dev/null +++ b/issue_relations/app/views/admin/index.rhtml @@ -0,0 +1,41 @@ +

<%=l(:label_administration)%>

+ +

+<%= 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(:field_mail_notification), :controller => 'admin', :action => 'mail_options' %> +

+ +

+<%= link_to l(:label_authentication), :controller => 'auth_sources' %> +

+ +

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

\ No newline at end of file diff --git a/issue_relations/app/views/admin/info.rhtml b/issue_relations/app/views/admin/info.rhtml new file mode 100644 index 000000000..4777a151e --- /dev/null +++ b/issue_relations/app/views/admin/info.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_information_plural)%>

+ +

<%=l(:field_version)%>: <%= RDM_APP_NAME %> <%= RDM_APP_VERSION %>

+ +<%=l(:label_environment)%>: +
    +<% Rails::Info.properties.each do |name, value| %> +
  • <%= name %>: <%= value %>
  • +<% end %> +
\ No newline at end of file diff --git a/issue_relations/app/views/admin/mail_options.rhtml b/issue_relations/app/views/admin/mail_options.rhtml new file mode 100644 index 000000000..54e2daf3e --- /dev/null +++ b/issue_relations/app/views/admin/mail_options.rhtml @@ -0,0 +1,24 @@ +

<%=l(:field_mail_notification)%>

+ +<%= start_form_tag ({}, :id => 'mail_options_form')%> + +
+

<%=l(:text_select_mail_notifications)%>

+ +<% actions = @actions.group_by {|p| p.group_id } %> +<% actions.keys.sort.each do |group_id| %> +
<%= l(Permission::GROUPS[group_id]) %> +<% actions[group_id].each do |p| %> +
<%= check_box_tag "action_ids[]", p.id, p.mail_enabled? %> + <%= l(p.description.to_sym) %> +
+<% end %> +
+<% end %> + +
+

<%= check_all_links 'mail_options_form' %>

+
+ +<%= submit_tag l(:button_save) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/admin/projects.rhtml b/issue_relations/app/views/admin/projects.rhtml new file mode 100644 index 000000000..b30cf2af5 --- /dev/null +++ b/issue_relations/app/views/admin/projects.rhtml @@ -0,0 +1,33 @@ +
+<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_project_plural)%>

+ + + + <%= sort_header_tag('name', :caption => l(:label_project)) %> + + + + <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + + + +<% for project in @projects %> + "> + + +<% end %> + +
<%=l(:field_description)%><%=l(:field_is_public)%><%=l(:label_subproject_plural)%>
<%= link_to project.name, :controller => 'projects', :action => 'settings', :id => project %> + <%=h project.description %> + <%= image_tag 'true.png' if project.is_public? %> + <%= project.children_count %> + <%= format_date(project.created_on) %> + + <%= button_to l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => "button-small" %> +
+ +

<%= pagination_links_full @project_pages %> +[ <%= @project_pages.current.first_item %> - <%= @project_pages.current.last_item %> / <%= @project_count %> ]

\ No newline at end of file diff --git a/issue_relations/app/views/auth_sources/_form.rhtml b/issue_relations/app/views/auth_sources/_form.rhtml new file mode 100644 index 000000000..b6365dce5 --- /dev/null +++ b/issue_relations/app/views/auth_sources/_form.rhtml @@ -0,0 +1,45 @@ +<%= error_messages_for 'auth_source' %> + +
+ +

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

+ +

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

+ +

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

+ +

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

+ +

+<%= password_field '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/issue_relations/app/views/auth_sources/edit.rhtml b/issue_relations/app/views/auth_sources/edit.rhtml new file mode 100644 index 000000000..149463e7f --- /dev/null +++ b/issue_relations/app/views/auth_sources/edit.rhtml @@ -0,0 +1,7 @@ +

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

+ +<%= start_form_tag({:action => 'update', :id => @auth_source}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<%= end_form_tag %> + diff --git a/issue_relations/app/views/auth_sources/list.rhtml b/issue_relations/app/views/auth_sources/list.rhtml new file mode 100644 index 000000000..f486f45b7 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/auth_sources/new.rhtml b/issue_relations/app/views/auth_sources/new.rhtml new file mode 100644 index 000000000..29d66327b --- /dev/null +++ b/issue_relations/app/views/auth_sources/new.rhtml @@ -0,0 +1,6 @@ +

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

+ +<%= start_form_tag({:action => 'create'}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/common/404.rhtml b/issue_relations/app/views/common/404.rhtml new file mode 100644 index 000000000..a81eeba02 --- /dev/null +++ b/issue_relations/app/views/common/404.rhtml @@ -0,0 +1,4 @@ +

404

+ +

<%= l(:notice_file_not_found) %>

+

Back

diff --git a/issue_relations/app/views/custom_fields/_form.rhtml b/issue_relations/app/views/custom_fields/_form.rhtml new file mode 100644 index 000000000..cf658fb2a --- /dev/null +++ b/issue_relations/app/views/custom_fields/_form.rhtml @@ -0,0 +1,70 @@ +<%= 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)%>)

+

<%= f.text_area :possible_values, :rows => 5, :cols => 60 %>
(<%=l(:text_possible_values_info)%>)

+
+<%= javascript_tag "toggle_custom_field_format();" %> + + +
+<% 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 %>

+ +<% when "UserCustomField" %> +

<%= f.check_box :is_required %>

+ +<% when "ProjectCustomField" %> +

<%= f.check_box :is_required %>

+ +<% end %> +
diff --git a/issue_relations/app/views/custom_fields/edit.rhtml b/issue_relations/app/views/custom_fields/edit.rhtml new file mode 100644 index 000000000..ef056fa41 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/custom_fields/list.rhtml b/issue_relations/app/views/custom_fields/list.rhtml new file mode 100644 index 000000000..982e66aab --- /dev/null +++ b/issue_relations/app/views/custom_fields/list.rhtml @@ -0,0 +1,38 @@ +

<%=l(:label_custom_field_plural)%>

+ + + + + + + + + + + + +<% for custom_field in @custom_fields %> + "> + + + + + + + + +<% end %> + +
<%=l(:field_name)%><%=l(:field_type)%><%=l(:field_field_format)%><%=l(:field_is_required)%><%=l(:field_is_for_all)%><%=l(:label_used_by)%>
<%= link_to custom_field.name, :action => 'edit', :id => custom_field %><%= l(custom_field.type_name) %><%= 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? %> + <%= button_to l(:button_delete), { :action => 'destroy', :id => custom_field }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +<%= pagination_links_full @custom_field_pages %> + +
+<%=l(:label_custom_field_new)%>: +
    +
  • <%= link_to l(:label_issue_plural), :action => 'new', :type => 'IssueCustomField' %>
  • +
  • <%= link_to l(:label_project_plural), :action => 'new', :type => 'ProjectCustomField' %>
  • +
  • <%= link_to l(:label_user_plural), :action => 'new', :type => 'UserCustomField' %>
  • +
diff --git a/issue_relations/app/views/custom_fields/new.rhtml b/issue_relations/app/views/custom_fields/new.rhtml new file mode 100644 index 000000000..2e8aa2750 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/documents/_document.rhtml b/issue_relations/app/views/documents/_document.rhtml new file mode 100644 index 000000000..55864ee82 --- /dev/null +++ b/issue_relations/app/views/documents/_document.rhtml @@ -0,0 +1,3 @@ +

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

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

+

+ +

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

+ +

+<%= text_area 'document', 'description', :cols => 60, :rows => 15 %>

+ +
+ +<% unless $RDM_TEXTILE_DISABLED %> +<%= javascript_include_tag 'jstoolbar' %> + +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/documents/edit.rhtml b/issue_relations/app/views/documents/edit.rhtml new file mode 100644 index 000000000..3db4bcc6a --- /dev/null +++ b/issue_relations/app/views/documents/edit.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_document)%>

+ +<%= start_form_tag({:action => 'edit', :id => @document}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<%= end_form_tag %> + + diff --git a/issue_relations/app/views/documents/show.rhtml b/issue_relations/app/views/documents/show.rhtml new file mode 100644 index 000000000..dab360eda --- /dev/null +++ b/issue_relations/app/views/documents/show.rhtml @@ -0,0 +1,37 @@ +
+<%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+ +

<%= @document.title %>

+ +

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

+<%= textilizable @document.description %> +
+ +

<%= l(:label_attachment_plural) %>

+
    +<% for attachment in @attachments %> +
  • +
    + <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy_attachment', :id => @document, :attachment_id => attachment}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
    + <%= link_to attachment.filename, :action => 'download', :id => @document, :attachment_id => attachment %> + (<%= human_size attachment.filesize %>)
    + <%= attachment.author.display_name %>, <%= format_date(attachment.created_on) %>
    + <%= lwr(:label_download, attachment.downloads) %> +
  • +<% end %> +
+
+ + +<% if authorize_for('documents', 'add_attachment') %> + <%= start_form_tag ({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :class => "tabular") %> +

+ <%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= human_size(Attachment.max_size) %>)

+ <%= submit_tag l(:button_add) %> + <%= end_form_tag %> +<% end %> diff --git a/issue_relations/app/views/enumerations/_form.rhtml b/issue_relations/app/views/enumerations/_form.rhtml new file mode 100644 index 000000000..637605939 --- /dev/null +++ b/issue_relations/app/views/enumerations/_form.rhtml @@ -0,0 +1,9 @@ +<%= error_messages_for 'enumeration' %> +
+ +<%= hidden_field 'enumeration', 'opt' %> + +

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

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

<%=l(:label_enumerations)%>

+ +<%= start_form_tag({:action => 'update', :id => @enumeration}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<%= end_form_tag %> + +<%= start_form_tag :action => 'destroy', :id => @enumeration %> + <%= submit_tag l(:button_delete) %> +<%= end_form_tag %> \ No newline at end of file diff --git a/issue_relations/app/views/enumerations/list.rhtml b/issue_relations/app/views/enumerations/list.rhtml new file mode 100644 index 000000000..18508125f --- /dev/null +++ b/issue_relations/app/views/enumerations/list.rhtml @@ -0,0 +1,19 @@ +

<%=l(:label_enumerations)%>

+  +<% Enumeration::OPTIONS.each do |option, name| %> + + <% if params[:opt]==option %> + +

<%= l(name) %>

+
    + <% for value in Enumeration.find(:all, :conditions => ["opt = ?", option]) %> +
  • <%= link_to value.name, :action => 'edit', :id => value %>
  • + <% end %> +
+

<%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option }, :class => "icon icon-add" %>

  + + <% else %> +

<%= link_to l(name), :opt => option %>

+ <% end %> + +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/enumerations/new.rhtml b/issue_relations/app/views/enumerations/new.rhtml new file mode 100644 index 000000000..0a773519d --- /dev/null +++ b/issue_relations/app/views/enumerations/new.rhtml @@ -0,0 +1,6 @@ +

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

+ +<%= start_form_tag({:action => 'create'}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/feeds/news.rxml b/issue_relations/app/views/feeds/news.rxml new file mode 100644 index 000000000..50d4a9aba --- /dev/null +++ b/issue_relations/app/views/feeds/news.rxml @@ -0,0 +1,20 @@ +xml.instruct! +xml.rss "version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" do + xml.channel do + xml.title "#{$RDM_HEADER_TITLE}: #{l(:label_news_latest)}" + xml.link url_for(:controller => '', :only_path => false) + xml.pubDate CGI.rfc1123_date(@news.first.created_on) + xml.description "#{$RDM_HEADER_TITLE}: #{l(:label_news_latest)}" + @news.each do |news| + xml.item do + xml.title "#{news.project.name}: #{news.title}" + news_url = url_for(:controller => 'news' , :action => 'show', :id => news, :only_path => false) + xml.link news_url + xml.description h(news.summary) + xml.pubDate CGI.rfc1123_date(news.created_on) + xml.guid news_url + xml.author h(news.author.name) + end + end + end +end \ No newline at end of file diff --git a/issue_relations/app/views/issue_categories/_form.rhtml b/issue_relations/app/views/issue_categories/_form.rhtml new file mode 100644 index 000000000..765b8f53d --- /dev/null +++ b/issue_relations/app/views/issue_categories/_form.rhtml @@ -0,0 +1,7 @@ +<%= error_messages_for 'issue_category' %> + + +

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

+ + diff --git a/issue_relations/app/views/issue_categories/edit.rhtml b/issue_relations/app/views/issue_categories/edit.rhtml new file mode 100644 index 000000000..053facbf7 --- /dev/null +++ b/issue_relations/app/views/issue_categories/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_category)%>

+ +<%= start_form_tag({:action => 'edit', :id => @category}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/issue_statuses/_form.rhtml b/issue_relations/app/views/issue_statuses/_form.rhtml new file mode 100644 index 000000000..f3b1cf2ca --- /dev/null +++ b/issue_relations/app/views/issue_statuses/_form.rhtml @@ -0,0 +1,18 @@ +<%= error_messages_for 'issue_status' %> + +
+ +

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

+ +

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

+ +

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

+ +

+#<%= text_field 'issue_status', 'html_color', :maxlength => 6 %>

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

<%=l(:label_issue_status)%>

+ +<%= start_form_tag({:action => 'update', :id => @issue_status}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/issue_statuses/list.rhtml b/issue_relations/app/views/issue_statuses/list.rhtml new file mode 100644 index 000000000..bde9b1e23 --- /dev/null +++ b/issue_relations/app/views/issue_statuses/list.rhtml @@ -0,0 +1,28 @@ +
+<%= 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)%>
<%= link_to status.name, :action => 'edit', :id => status %>
<%= image_tag 'true.png' if status.is_default? %><%= image_tag 'true.png' if status.is_closed? %> + <%= button_to l(:button_delete), { :action => 'destroy', :id => status }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +<%= pagination_links_full @issue_status_pages %> \ No newline at end of file diff --git a/issue_relations/app/views/issue_statuses/new.rhtml b/issue_relations/app/views/issue_statuses/new.rhtml new file mode 100644 index 000000000..2dacb1e21 --- /dev/null +++ b/issue_relations/app/views/issue_statuses/new.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_issue_status_new)%>

+ +<%= start_form_tag({:action => 'create'}, :class => "tabular") %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/issues/_add_shortcut.rhtml b/issue_relations/app/views/issues/_add_shortcut.rhtml new file mode 100644 index 000000000..f48907777 --- /dev/null +++ b/issue_relations/app/views/issues/_add_shortcut.rhtml @@ -0,0 +1,5 @@ +<% if authorize_for('projects', 'add_issue') %> +<%= start_form_tag({ :controller => 'projects', :action => 'add_issue', :id => @project }, :method => 'get') %> +<%= l(:label_issue_new) %>: <%= select_tag 'tracker_id', ("" + options_from_collection_for_select(trackers, 'id', 'name')), :onchange => "if (this.value!='') {this.form.submit();}" %> +<%= end_form_tag %> +<% end %> diff --git a/issue_relations/app/views/issues/_history.rhtml b/issue_relations/app/views/issues/_history.rhtml new file mode 100644 index 000000000..da58b7d6c --- /dev/null +++ b/issue_relations/app/views/issues/_history.rhtml @@ -0,0 +1,11 @@ +<% for journal in journals %> +

<%= format_time(journal.created_on) %> - <%= journal.user.name %>

+
    + <% for detail in journal.details %> +
  • <%= show_detail(detail) %>
  • + <% end %> +
+ <% if journal.notes? %> + <%= simple_format auto_link h(journal.notes) %> + <% end %> +<% end %> diff --git a/issue_relations/app/views/issues/_list_simple.rhtml b/issue_relations/app/views/issues/_list_simple.rhtml new file mode 100644 index 000000000..10d880f43 --- /dev/null +++ b/issue_relations/app/views/issues/_list_simple.rhtml @@ -0,0 +1,25 @@ +<% if issues.length > 0 %> + + + + + + + + <% for issue in issues %> + "> + + + + + <% end %> + +
#<%=l(:field_tracker)%><%=l(:field_subject)%>
+ <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> +

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

+
+<% else %> + <%=l(:label_no_data)%> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/issues/_pdf.rfpdf b/issue_relations/app/views/issues/_pdf.rfpdf new file mode 100644 index 000000000..1f6a12283 --- /dev/null +++ b/issue_relations/app/views/issues/_pdf.rfpdf @@ -0,0 +1,100 @@ +<% pdf.SetFont('Arial','B',11) + pdf.Cell(190,10, "#{issue.project.name} - #{issue.tracker.name} # #{issue.long_id} - #{issue.subject}") + pdf.Ln + + y0 = pdf.GetY + + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_status) + ":","LT") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, issue.status.name,"RT") + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_priority) + ":","LT") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, issue.priority.name,"RT") + pdf.Ln + + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_author) + ":","L") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, issue.author.name,"R") + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_category) + ":","L") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, (issue.category ? issue.category.name : "-"),"R") + pdf.Ln + + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_created_on) + ":","L") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, format_date(issue.created_on),"R") + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_assigned_to) + ":","L") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, (issue.assigned_to ? issue.assigned_to.name : "-"),"R") + pdf.Ln + + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_updated_on) + ":","LB") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, format_date(issue.updated_on),"RB") + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_due_date) + ":","LB") + pdf.SetFont('Arial','',9) + pdf.Cell(60,5, format_date(issue.due_date),"RB") + pdf.Ln + + for custom_value in issue.custom_values + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, custom_value.custom_field.name + ":","L") + pdf.SetFont('Arial','',9) + pdf.MultiCell(155,5, (show_value custom_value),"R") + end + + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_subject) + ":","LTB") + pdf.SetFont('Arial','',9) + pdf.Cell(155,5, issue.subject,"RTB") + pdf.Ln + + pdf.SetFont('Arial','B',9) + pdf.Cell(35,5, l(:field_description) + ":") + pdf.SetFont('Arial','',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 + + pdf.SetFont('Arial','B',9) + pdf.Cell(190,5, l(:label_history), "B") + pdf.Ln + for journal in issue.journals.find(:all, :include => :user, :order => "journals.created_on desc") + pdf.SetFont('Arial','B',8) + pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name) + pdf.Ln + pdf.SetFont('Arial','I',8) + for detail in journal.details + pdf.Cell(190,5, "- " + show_detail(detail, true)) + pdf.Ln + end + if journal.notes? + pdf.SetFont('Arial','',8) + pdf.MultiCell(190,5, journal.notes) + end + pdf.Ln + end + + pdf.SetFont('Arial','B',9) + pdf.Cell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in issue.attachments + pdf.SetFont('Arial','',8) + pdf.Cell(80,5, attachment.filename) + pdf.Cell(20,5, human_size(attachment.filesize),0,0,"R") + pdf.Cell(20,5, format_date(attachment.created_on),0,0,"R") + pdf.Cell(70,5, attachment.author.name,0,0,"R") + pdf.Ln + end +%> \ No newline at end of file diff --git a/issue_relations/app/views/issues/_tooltip.rhtml b/issue_relations/app/views/issues/_tooltip.rhtml new file mode 100644 index 000000000..d7b555e4a --- /dev/null +++ b/issue_relations/app/views/issues/_tooltip.rhtml @@ -0,0 +1,6 @@ +<%= link_to "#{issue.tracker.name} ##{issue.id}", { :controller => 'issues', :action => 'show', :id => issue } %>: <%=h issue.subject %>
+
+<%= l(:field_start_date) %>: <%= format_date(issue.start_date) %>
+<%= l(:field_due_date) %>: <%= format_date(issue.due_date) %>
+<%= l(:field_assigned_to) %>: <%= issue.assigned_to ? issue.assigned_to.name : "-" %>
+<%= l(:field_priority) %>: <%= issue.priority.name %> diff --git a/issue_relations/app/views/issues/change_status.rhtml b/issue_relations/app/views/issues/change_status.rhtml new file mode 100644 index 000000000..38ca82ea2 --- /dev/null +++ b/issue_relations/app/views/issues/change_status.rhtml @@ -0,0 +1,37 @@ +

<%=l(:label_issue)%> #<%= @issue.id %>: <%=h @issue.subject %>

+ +<%= error_messages_for 'issue' %> +<%= start_form_tag({:action => 'change_status', :id => @issue}, :class => "tabular") %> + +<%= hidden_field_tag 'confirm', 1 %> +<%= hidden_field_tag 'new_status_id', @new_status.id %> + +
+

<%= @new_status.name %>

+ +

+

+ + +

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

+ + +

+

+ +

+<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10 %>

+ +
+ +<%= hidden_field 'issue', 'lock_version' %> +<%= submit_tag l(:button_save) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/issues/edit.rhtml b/issue_relations/app/views/issues/edit.rhtml new file mode 100644 index 000000000..da3805c29 --- /dev/null +++ b/issue_relations/app/views/issues/edit.rhtml @@ -0,0 +1,56 @@ +

<%= @issue.tracker.name %> #<%= @issue.id %> - <%=h @issue.subject %>

+ +<% labelled_tabular_form_for :issue, @issue, :url => {:action => 'edit'} do |f| %> +<%= error_messages_for 'issue' %> +
+ +
+

<%= @issue.status.name %>

+

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

+

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

+

<%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}) %>

+
+ +
+

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

+
+ +
+

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

+

<%= f.text_area :description, :cols => 60, :rows => [[10, @issue.description.length / 50].max, 100].min, :required => true %>

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

<%= custom_field_tag_with_label @custom_value %>

+<% end %> + +

<%= f.select :fixed_version_id, (@project.versions.collect {|v| [v.name, v.id]}), { :include_blank => true } %> +

+
+ +
+<%= f.hidden_field :lock_version %> +<%= submit_tag l(:button_save) %> +<% end %> + +<% unless $RDM_TEXTILE_DISABLED %> +<%= javascript_include_tag 'jstoolbar' %> + +<% end %> + +<% 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 %> \ No newline at end of file diff --git a/issue_relations/app/views/issues/export_pdf.rfpdf b/issue_relations/app/views/issues/export_pdf.rfpdf new file mode 100644 index 000000000..a8622dd51 --- /dev/null +++ b/issue_relations/app/views/issues/export_pdf.rfpdf @@ -0,0 +1,9 @@ +<% pdf=IfpdfHelper::IFPDF.new + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + pdf.AddPage + + render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => @issue } +%> + +<%= pdf.Output %> \ No newline at end of file diff --git a/issue_relations/app/views/issues/history.rhtml b/issue_relations/app/views/issues/history.rhtml new file mode 100644 index 000000000..2443cc739 --- /dev/null +++ b/issue_relations/app/views/issues/history.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_history)%>

+
+<%= render :partial => 'history', :locals => { :journals => @journals } %> +
+
+

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

\ No newline at end of file diff --git a/issue_relations/app/views/issues/show.rhtml b/issue_relations/app/views/issues/show.rhtml new file mode 100644 index 000000000..27c5e2bee --- /dev/null +++ b/issue_relations/app/views/issues/show.rhtml @@ -0,0 +1,119 @@ +
+<%= l(:label_export_to) %><%= link_to 'PDF', {:action => 'export_pdf', :id => @issue}, :class => 'icon icon-pdf' %> +
+ +

<%= @issue.tracker.name %> #<%= @issue.id %> - <%=h @issue.subject %>

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +<% 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_priority)%> :<%= @issue.priority.name %>
<%=l(:field_assigned_to)%> :<%= @issue.assigned_to ? @issue.assigned_to.name : "-" %><%=l(:field_category)%> :<%=h @issue.category ? @issue.category.name : "-" %>
<%=l(:field_author)%> :<%= link_to_user @issue.author %><%=l(:field_start_date)%> :<%= format_date(@issue.start_date) %>
<%=l(:field_created_on)%> :<%= format_date(@issue.created_on) %><%=l(:field_due_date)%> :<%= format_date(@issue.due_date) %>
<%=l(:field_updated_on)%> :<%= format_date(@issue.updated_on) %><%=l(:field_done_ratio)%> :<%= @issue.done_ratio %> %
Relations : + <% @issue.relations_to.each do |relation| %> + -> <%= relation.issue_to_id %> (<%= relation.relation_type %>)
+ <% end %> + <% @issue.relations_from.each do |relation| %> + <%= relation.issue_id %> (<%= relation.relation_type %>) ->
+ <% end %> +
<%= custom_value.custom_field.name %> :<%=h show_value custom_value %>
+
+
+ +<%=l(:field_description)%> :

+<%= textilizable @issue.description %> +
+ +
+<%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon icon-edit' %> +<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+ +<% if authorize_for('issues', 'change_status') and @status_options and !@status_options.empty? %> + <%= start_form_tag ({:controller => 'issues', :action => 'change_status', :id => @issue}) %> + <%=l(:label_change_status)%> : + + <%= submit_tag l(:button_change) %> + <%= end_form_tag %> +<% end %> +  +
+ +
+

<%=l(:label_history)%> +<% if @journals_count > @journals.length %>(<%= l(:label_last_changes, @journals.length) %>)<% end %>

+<%= render :partial => 'history', :locals => { :journals => @journals } %> +<% if @journals_count > @journals.length %> +

[ <%= link_to l(:label_change_view_all), :action => 'history', :id => @issue %> ]

+<% end %> +
+ +
+

<%=l(:label_attachment_plural)%>

+ +<% for attachment in @issue.attachments %> + + + + + + +<% end %> +
<%= link_to attachment.filename, { :action => 'download', :id => @issue, :attachment_id => attachment }, :class => 'icon icon-attachment' %> (<%= human_size(attachment.filesize) %>)<%= format_date(attachment.created_on) %><%= attachment.author.display_name %>
<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy_attachment', :id => @issue, :attachment_id => attachment }, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %>
+
+<% if authorize_for('issues', 'add_attachment') %> + <%= start_form_tag ({ :controller => 'issues', :action => 'add_attachment', :id => @issue }, :multipart => true, :class => "tabular") %> +

+ <%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= human_size(Attachment.max_size) %>)

+ <%= submit_tag l(:button_add) %> + <%= end_form_tag %> +<% end %> +
+ +<% if authorize_for('issues', 'add_note') %> +
+

<%= l(:label_add_note) %>

+ <%= start_form_tag ({:controller => 'issues', :action => 'add_note', :id => @issue}, :class => "tabular" ) %> +

+ <%= text_area_tag 'notes', '', :cols => 60, :rows => 10 %>

+ <%= submit_tag l(:button_add) %> + <%= end_form_tag %> +
+<% end %> diff --git a/issue_relations/app/views/layouts/base.rhtml b/issue_relations/app/views/layouts/base.rhtml new file mode 100644 index 000000000..4768f29dd --- /dev/null +++ b/issue_relations/app/views/layouts/base.rhtml @@ -0,0 +1,145 @@ + + + +<%= $RDM_HEADER_TITLE + (@html_title ? ": #{@html_title}" : "") %> + + + + +<%= stylesheet_link_tag "application" %> +<%= stylesheet_link_tag "print", :media => "print" %> +<%= javascript_include_tag :defaults %> +<%= javascript_include_tag 'menu' %> +<%= stylesheet_link_tag 'jstoolbar' %> +<%= yield :header_tags %> + + + +
+ + + + + + <% if admin_loggedin? %> + + + + + <% end %> + + <% unless @project.nil? || @project.id.nil? %> + + <% end %> + + +
+ + <% unless @project.nil? || @project.id.nil? %> +

<%= @project.name %>

+ + <% end %> + + <% if loggedin? and @logged_in_user.memberships.length > 0 %> +

<%=l(:label_my_projects) %>

+ + <% end %> +
+ +
+ <% if flash[:notice] %>

<%= flash[:notice] %>

<% end %> + <%= @content_for_layout %> +
+ + + +
+ + \ No newline at end of file diff --git a/issue_relations/app/views/mailer/_issue.rhtml b/issue_relations/app/views/mailer/_issue.rhtml new file mode 100644 index 000000000..c123ae30c --- /dev/null +++ b/issue_relations/app/views/mailer/_issue.rhtml @@ -0,0 +1,7 @@ +<%=l(:label_issue)%> #<%= issue.id %> - <%= issue.subject %> +<%=l(:field_author)%>: <%= issue.author.display_name %> +<%=l(:field_status)%>: <%= issue.status.name %> + +<%= issue.description %> + +http://<%= $RDM_HOST_NAME %>/issues/show/<%= issue.id %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_add_de.rhtml b/issue_relations/app/views/mailer/issue_add_de.rhtml new file mode 100644 index 000000000..9efec9ad9 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_add_de.rhtml @@ -0,0 +1,3 @@ +Issue #<%= @issue.id %> has been reported. +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_add_en.rhtml b/issue_relations/app/views/mailer/issue_add_en.rhtml new file mode 100644 index 000000000..9efec9ad9 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_add_en.rhtml @@ -0,0 +1,3 @@ +Issue #<%= @issue.id %> has been reported. +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_add_es.rhtml b/issue_relations/app/views/mailer/issue_add_es.rhtml new file mode 100644 index 000000000..9efec9ad9 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_add_es.rhtml @@ -0,0 +1,3 @@ +Issue #<%= @issue.id %> has been reported. +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_add_fr.rhtml b/issue_relations/app/views/mailer/issue_add_fr.rhtml new file mode 100644 index 000000000..628ecae12 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_add_fr.rhtml @@ -0,0 +1,3 @@ +Une nouvelle demande (#<%= @issue.id %>) a été soumise. +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_edit_de.rhtml b/issue_relations/app/views/mailer/issue_edit_de.rhtml new file mode 100644 index 000000000..d22d6c534 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_edit_de.rhtml @@ -0,0 +1,8 @@ +Issue #<%= @issue.id %> has been updated. +<%= @journal.user.name %> +<% for detail in @journal.details %> +<%= show_detail(detail, true) %> +<% end %> +<%= @journal.notes if @journal.notes? %> +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_edit_en.rhtml b/issue_relations/app/views/mailer/issue_edit_en.rhtml new file mode 100644 index 000000000..d22d6c534 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_edit_en.rhtml @@ -0,0 +1,8 @@ +Issue #<%= @issue.id %> has been updated. +<%= @journal.user.name %> +<% for detail in @journal.details %> +<%= show_detail(detail, true) %> +<% end %> +<%= @journal.notes if @journal.notes? %> +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_edit_es.rhtml b/issue_relations/app/views/mailer/issue_edit_es.rhtml new file mode 100644 index 000000000..d22d6c534 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_edit_es.rhtml @@ -0,0 +1,8 @@ +Issue #<%= @issue.id %> has been updated. +<%= @journal.user.name %> +<% for detail in @journal.details %> +<%= show_detail(detail, true) %> +<% end %> +<%= @journal.notes if @journal.notes? %> +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/issue_edit_fr.rhtml b/issue_relations/app/views/mailer/issue_edit_fr.rhtml new file mode 100644 index 000000000..9edacb703 --- /dev/null +++ b/issue_relations/app/views/mailer/issue_edit_fr.rhtml @@ -0,0 +1,8 @@ +La demande #<%= @issue.id %> a été mise à jour. +<%= @journal.user.name %> - <%= format_date(@journal.created_on) %> +<% for detail in @journal.details %> +<%= show_detail(detail, true) %> +<% end %> +<%= journal.notes if journal.notes? %> +---------------------------------------- +<%= render :file => "_issue", :use_full_path => true, :locals => { :issue => @issue } %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/lost_password_de.rhtml b/issue_relations/app/views/mailer/lost_password_de.rhtml new file mode 100644 index 000000000..2593edbda --- /dev/null +++ b/issue_relations/app/views/mailer/lost_password_de.rhtml @@ -0,0 +1,3 @@ +To change your password, use the following link: + +http://<%= $RDM_HOST_NAME %>/account/lost_password?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/lost_password_en.rhtml b/issue_relations/app/views/mailer/lost_password_en.rhtml new file mode 100644 index 000000000..2593edbda --- /dev/null +++ b/issue_relations/app/views/mailer/lost_password_en.rhtml @@ -0,0 +1,3 @@ +To change your password, use the following link: + +http://<%= $RDM_HOST_NAME %>/account/lost_password?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/lost_password_es.rhtml b/issue_relations/app/views/mailer/lost_password_es.rhtml new file mode 100644 index 000000000..2593edbda --- /dev/null +++ b/issue_relations/app/views/mailer/lost_password_es.rhtml @@ -0,0 +1,3 @@ +To change your password, use the following link: + +http://<%= $RDM_HOST_NAME %>/account/lost_password?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/lost_password_fr.rhtml b/issue_relations/app/views/mailer/lost_password_fr.rhtml new file mode 100644 index 000000000..30996f118 --- /dev/null +++ b/issue_relations/app/views/mailer/lost_password_fr.rhtml @@ -0,0 +1,3 @@ +Pour changer votre mot de passe, utilisez le lien suivant: + +http://<%= $RDM_HOST_NAME %>/account/lost_password?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/register_de.rhtml b/issue_relations/app/views/mailer/register_de.rhtml new file mode 100644 index 000000000..2c0341b24 --- /dev/null +++ b/issue_relations/app/views/mailer/register_de.rhtml @@ -0,0 +1,3 @@ +To activate your redMine account, use the following link: + +http://<%= $RDM_HOST_NAME %>/account/register?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/register_en.rhtml b/issue_relations/app/views/mailer/register_en.rhtml new file mode 100644 index 000000000..2c0341b24 --- /dev/null +++ b/issue_relations/app/views/mailer/register_en.rhtml @@ -0,0 +1,3 @@ +To activate your redMine account, use the following link: + +http://<%= $RDM_HOST_NAME %>/account/register?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/register_es.rhtml b/issue_relations/app/views/mailer/register_es.rhtml new file mode 100644 index 000000000..2c0341b24 --- /dev/null +++ b/issue_relations/app/views/mailer/register_es.rhtml @@ -0,0 +1,3 @@ +To activate your redMine account, use the following link: + +http://<%= $RDM_HOST_NAME %>/account/register?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/mailer/register_fr.rhtml b/issue_relations/app/views/mailer/register_fr.rhtml new file mode 100644 index 000000000..3f5d0ccaf --- /dev/null +++ b/issue_relations/app/views/mailer/register_fr.rhtml @@ -0,0 +1,3 @@ +Pour activer votre compte sur redMine, utilisez le lien suivant: + +http://<%= $RDM_HOST_NAME %>/account/register?token=<%= @token.value %> \ No newline at end of file diff --git a/issue_relations/app/views/my/_block.rhtml b/issue_relations/app/views/my/_block.rhtml new file mode 100644 index 000000000..3f72bdaf1 --- /dev/null +++ b/issue_relations/app/views/my/_block.rhtml @@ -0,0 +1,16 @@ +
+ +
+ <%= link_to_remote "", { + :url => { :action => "remove_block", :block => block_name }, + :complete => "removeBlock('block_#{block_name}')", + :loading => "Element.show('indicator')", + :loaded => "Element.hide('indicator')" }, + :class => "close-icon" + %> +
+ +
+ <%= render :partial => "my/blocks/#{block_name}", :locals => { :user => user } %> +
+
\ No newline at end of file diff --git a/issue_relations/app/views/my/account.rhtml b/issue_relations/app/views/my/account.rhtml new file mode 100644 index 000000000..23b236e29 --- /dev/null +++ b/issue_relations/app/views/my/account.rhtml @@ -0,0 +1,47 @@ +

<%=l(:label_my_account)%>

+ +

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

+ +<%= error_messages_for 'user' %> + +
+

<%=l(:label_information_plural)%>

+ +<% labelled_tabular_form_for :user, @user, :url => { :action => "account" } do |f| %> + +

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

+

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

+

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

+

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

+

<%= f.check_box :mail_notification %>

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

<%= pref_fields.check_box :hide_mail %>

+<% end %> + +
<%= submit_tag l(:button_save) %>
+<% end %> +
+ + +<% unless @user.auth_source_id %> +
+

<%=l(:field_password)%>

+ + <%= start_form_tag({:action => 'change_password'}, :class => "tabular") %> + +

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

+ +

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

+ +

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

+ +
<%= submit_tag l(:button_save) %>
+ <%= end_form_tag %> +
+<% end %> diff --git a/issue_relations/app/views/my/blocks/_calendar.rhtml b/issue_relations/app/views/my/blocks/_calendar.rhtml new file mode 100644 index 000000000..dfff5b78a --- /dev/null +++ b/issue_relations/app/views/my/blocks/_calendar.rhtml @@ -0,0 +1,47 @@ +

<%= l(:label_calendar) %>

+ +<% +@date_from = Date.today - (Date.today.cwday-1) +@date_to = Date.today + (7-Date.today.cwday) +@issues = Issue.find :all, + :conditions => ["issues.project_id in (#{@user.projects.collect{|m| m.id}.join(',')}) AND ((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?))", @date_from, @date_to, @date_from, @date_to], + :include => [:project, :tracker] unless @user.projects.empty? +@issues ||= [] +%> + + + + +<% 1.upto(7) do |d| %> + +<% end %> + + + +<% day = @date_from +while day <= @date_to + if day.cwday == 1 %> + + <% end %> + + <%= '' if day.cwday >= 7 and day!=@date_to %> + <% + day = day + 1 +end %> + + +
<%= day_name(d) %>
<%= day.cweek %>"> +

<%= day==Date.today ? "#{day.day}" : day.day %>

+ <% day_issues = [] + @issues.each { |i| day_issues << i if i.start_date == day or i.due_date == day } + day_issues.each do |i| %> + <%= if day == i.start_date and 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 %> + <%= link_to "#{i.tracker.name} ##{i.id}", :controller => 'issues', :action => 'show', :id => i %>: <%=h i.subject.sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)') %>
+ <% end %> +
\ No newline at end of file diff --git a/issue_relations/app/views/my/blocks/_documents.rhtml b/issue_relations/app/views/my/blocks/_documents.rhtml new file mode 100644 index 000000000..eb8c16a58 --- /dev/null +++ b/issue_relations/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 => 'documents.created_on DESC', + :conditions => "documents.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/issue_relations/app/views/my/blocks/_issues_assigned_to_me.rhtml b/issue_relations/app/views/my/blocks/_issues_assigned_to_me.rhtml new file mode 100644 index 000000000..2a4e2a05d --- /dev/null +++ b/issue_relations/app/views/my/blocks/_issues_assigned_to_me.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_assigned_to_me_issues)%>

+<% assigned_issues = Issue.find(:all, + :conditions => ["assigned_to_id=?", user.id], + :limit => 10, + :include => [ :status, :project, :tracker ], + :order => 'issues.updated_on DESC') %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => assigned_issues } %> +<% if assigned_issues.length > 0 %> +

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

+<% end %> diff --git a/issue_relations/app/views/my/blocks/_issues_reported_by_me.rhtml b/issue_relations/app/views/my/blocks/_issues_reported_by_me.rhtml new file mode 100644 index 000000000..9b40b3606 --- /dev/null +++ b/issue_relations/app/views/my/blocks/_issues_reported_by_me.rhtml @@ -0,0 +1,10 @@ +

<%=l(:label_reported_issues)%>

+<% reported_issues = Issue.find(:all, + :conditions => ["author_id=?", user.id], + :limit => 10, + :include => [ :status, :project, :tracker ], + :order => 'issues.updated_on DESC') %> +<%= render :partial => 'issues/list_simple', :locals => { :issues => reported_issues } %> +<% if reported_issues.length > 0 %> +

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

+<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/my/blocks/_latest_news.rhtml b/issue_relations/app/views/my/blocks/_latest_news.rhtml new file mode 100644 index 000000000..4bad9a542 --- /dev/null +++ b/issue_relations/app/views/my/blocks/_latest_news.rhtml @@ -0,0 +1,8 @@ +

<%=l(:label_news_latest)%>

+ +<%= render (:partial => 'news/news', + :collection => News.find(:all, + :limit => 10, + :order => 'news.created_on DESC', + :conditions => "news.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/issue_relations/app/views/my/page.rhtml b/issue_relations/app/views/my/page.rhtml new file mode 100644 index 000000000..989d01397 --- /dev/null +++ b/issue_relations/app/views/my/page.rhtml @@ -0,0 +1,30 @@ +
+ <%= link_to l(:label_personalize_page), :action => 'page_layout' %> +
+ +

<%=l(:label_my_page)%>

+ +
+ <% @blocks['top'].each do |b| %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['top'] %> +
+ +
+ <% @blocks['left'].each do |b| %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['left'] %> +
+ +
+ <% @blocks['right'].each do |b| %> +
+ <%= render :partial => "my/blocks/#{b}", :locals => { :user => @user } %> +
+ <% end if @blocks['right'] %> +
+ diff --git a/issue_relations/app/views/my/page_layout.rhtml b/issue_relations/app/views/my/page_layout.rhtml new file mode 100644 index 000000000..d3346bd7d --- /dev/null +++ b/issue_relations/app/views/my/page_layout.rhtml @@ -0,0 +1,113 @@ + + +
+ +<%= start_form_tag({:action => "add_block"}, :id => "block-form") %> +<%= 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();", + :loading => "Element.show('indicator')", + :loaded => "Element.hide('indicator')" + %> +<%= end_form_tag %> | +<%= link_to l(:button_save), :action => 'page_layout_save' %> | +<%= link_to l(:button_cancel), :action => 'page' %> +
+ +

<%=l(:label_my_page)%>

+ +
+ <% @blocks['top'].each do |b| %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['top'] %> +
+ +
+ <% @blocks['left'].each do |b| %> + <%= render :partial => 'block', :locals => {:user => @user, :block_name => b} %> + <% end if @blocks['left'] %> +
+ +
+ <% @blocks['right'].each do |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, + :complete => visual_effect(:highlight, 'list-top'), + :url => { :action => "order_blocks", :group => "top" }, + :loading => "Element.show('indicator')", + :loaded => "Element.hide('indicator')" + %> + + +<%= sortable_element 'list-left', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :complete => visual_effect(:highlight, 'list-left'), + :url => { :action => "order_blocks", :group => "left" }, + :loading => "Element.show('indicator')", + :loaded => "Element.hide('indicator')" %> + +<%= sortable_element 'list-right', + :tag => 'div', + :only => 'mypage-box', + :handle => "handle", + :dropOnEmpty => true, + :containment => ['list-top', 'list-left', 'list-right'], + :constraint => false, + :complete => visual_effect(:highlight, 'list-right'), + :url => { :action => "order_blocks", :group => "right" }, + :loading => "Element.show('indicator')", + :loaded => "Element.hide('indicator')" %> + +<%= javascript_tag "updateSelect()" %> \ No newline at end of file diff --git a/issue_relations/app/views/news/_form.rhtml b/issue_relations/app/views/news/_form.rhtml new file mode 100644 index 000000000..2dcdc9f80 --- /dev/null +++ b/issue_relations/app/views/news/_form.rhtml @@ -0,0 +1,20 @@ +<%= 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 %>

+
+ +<% unless $RDM_TEXTILE_DISABLED %> +<%= javascript_include_tag 'jstoolbar' %> + +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/news/_news.rhtml b/issue_relations/app/views/news/_news.rhtml new file mode 100644 index 000000000..75a80d634 --- /dev/null +++ b/issue_relations/app/views/news/_news.rhtml @@ -0,0 +1,4 @@ +

<%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %>
+<% unless news.summary.empty? %><%=h news.summary %>
<% end %> +<%= news.author.name %>, <%= format_time(news.created_on) %>
+<%= news.comments_count %> <%= lwr(:label_comment, news.comments_count).downcase %>

diff --git a/issue_relations/app/views/news/edit.rhtml b/issue_relations/app/views/news/edit.rhtml new file mode 100644 index 000000000..5e015c4c7 --- /dev/null +++ b/issue_relations/app/views/news/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_news)%>

+ +<% labelled_tabular_form_for :news, @news, :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/issue_relations/app/views/news/show.rhtml b/issue_relations/app/views/news/show.rhtml new file mode 100644 index 000000000..c3661b1f8 --- /dev/null +++ b/issue_relations/app/views/news/show.rhtml @@ -0,0 +1,33 @@ +
+<%= link_to_if_authorized l(:button_edit), {:controller => 'news', :action => 'edit', :id => @news}, :class => 'icon icon-edit' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy', :id => @news}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+ +

<%=h @news.title %>

+ +

<% unless @news.summary.empty? %><%=h @news.summary %>
<% end %> +<%= @news.author.display_name %>, <%= format_time(@news.created_on) %>

+
+<%= textilizable auto_link @news.description %> +
+ +
+

<%= l(:label_comment_plural) %>

+<% @news.comments.each do |comment| %> + <% next if comment.new_record? %> +

<%= format_time(comment.created_on) %> - <%= comment.author.name %>

+
+ <%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+ <%= simple_format(auto_link(h comment.comment))%> +<% end if @news.comments_count > 0 %> +
+ +<% if authorize_for 'news', 'add_comment' %> +<%= start_form_tag :action => 'add_comment', :id => @news %> +<%= error_messages_for 'comment' %> +


+<%= text_area 'comment', 'comment', :cols => 60, :rows => 6 %>

+<%= submit_tag l(:button_add) %> +<%= end_form_tag %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/_form.rhtml b/issue_relations/app/views/projects/_form.rhtml new file mode 100644 index 000000000..14c7a26ee --- /dev/null +++ b/issue_relations/app/views/projects/_form.rhtml @@ -0,0 +1,44 @@ +<%= error_messages_for 'project' %> + +
+ +

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

+ +<% if admin_loggedin? and !@root_projects.empty? %> +

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

+<% end %> + +

<%= f.text_area :description, :required => true, :cols => 60, :rows => 3 %>

+

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

+

<%= f.check_box :is_public %>

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

<%= custom_field_tag_with_label @custom_value %>

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

+<% for custom_field in @custom_fields %> + <%= check_box_tag "custom_field_ids[]", custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %> + <%= custom_field.name %> +<% end %>

+<% end %> + +
+ +

<%= check_box_tag "repository_enabled", 1, !@project.repository.nil?, :onclick => "Element.toggle('repository');" %> <%= l(:label_repository) %>

+<%= hidden_field_tag "repository_enabled", 0 %> +
+<% fields_for :repository, @project.repository, { :builder => TabularFormBuilder, :lang => current_language} do |repository| %> +

<%= repository.text_field :url, :size => 60, :required => true %>
(http://, https://, svn://, file:///)

+<% end %> +
+<%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %> +
+ +<% 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 %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/activity.rhtml b/issue_relations/app/views/projects/activity.rhtml new file mode 100644 index 000000000..cb7a3bfe1 --- /dev/null +++ b/issue_relations/app/views/projects/activity.rhtml @@ -0,0 +1,59 @@ +

<%=l(:label_activity)%>: <%= "#{month_name(@month).downcase} #{@year}" %>

+ +
+
+<%= start_form_tag %> +

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

+

+ <%= check_box_tag 'show_issues', 1, @show_issues %><%= hidden_field_tag 'show_issues', 0, :id => nil %> <%=l(:label_issue_plural)%>
+ <%= check_box_tag 'show_news', 1, @show_news %><%= hidden_field_tag 'show_news', 0, :id => nil %> <%=l(:label_news_plural)%>
+ <%= check_box_tag 'show_files', 1, @show_files %><%= hidden_field_tag 'show_files', 0, :id => nil %> <%=l(:label_attachment_plural)%>
+ <%= check_box_tag 'show_documents', 1, @show_documents %><%= hidden_field_tag 'show_documents', 0, :id => nil %> <%=l(:label_document_plural)%> +

+

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

+<%= end_form_tag %> +
+ +<% @events_by_day.keys.sort {|x,y| y <=> x }.each do |day| %> +

<%= day_name(day.cwday) %> <%= day.day %>

+
    + <% @events_by_day[day].sort {|x,y| y.created_on <=> x.created_on }.each do |e| %> +
  • + <% if e.is_a? Issue %> + <%= e.created_on.strftime("%H:%M") %> <%= link_to "#{e.tracker.name} ##{e.id}", :controller => 'issues', :action => 'show', :id => e %> (<%= e.status.name %>): <%=h e.subject %>
    + <%= e.author.name %> + <% elsif e.is_a? News %> + <%= e.created_on.strftime("%H:%M") %> <%=l(:label_news)%>: <%= link_to h(e.title), :controller => 'news', :action => 'show', :id => e %>
    + <% unless e.summary.empty? %><%=h e.summary %>
    <% end %> + <%= e.author.name %> + <% elsif (e.is_a? Attachment) and (e.container.is_a? Version) %> + <%= e.created_on.strftime("%H:%M") %> <%=l(:label_attachment)%> (<%=h e.container.name %>): <%= link_to e.filename, :controller => 'projects', :action => 'list_files', :id => @project %>
    + <%= e.author.name %> + <% elsif (e.is_a? Attachment) and (e.container.is_a? Document) %> + <%= e.created_on.strftime("%H:%M") %> <%=l(:label_attachment)%>: <%= e.filename %> (<%= link_to h(e.container.title), :controller => 'documents', :action => 'show', :id => e.container %>)
    + <%= e.author.name %> + <% elsif e.is_a? Document %> + <%= e.created_on.strftime("%H:%M") %> <%=l(:label_document)%>: <%= link_to h(e.title), :controller => 'documents', :action => 'show', :id => e %>
    + <% end %> +

  • + + <% end %> +
+<% end %> +<% if @events_by_day.empty? %>

<%= l(:label_no_data) %>

<% end %> + +
+<%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), + {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }}, + {:href => url_for(:action => 'activity', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))} + %> +
+
+<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'), + {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }}, + {:href => url_for(:action => 'activity', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))} + %>  +
+
+
\ No newline at end of file diff --git a/issue_relations/app/views/projects/add.rhtml b/issue_relations/app/views/projects/add.rhtml new file mode 100644 index 000000000..bafbf9145 --- /dev/null +++ b/issue_relations/app/views/projects/add.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_project_new)%>

+ +<% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/add_document.rhtml b/issue_relations/app/views/projects/add_document.rhtml new file mode 100644 index 000000000..b570eabbd --- /dev/null +++ b/issue_relations/app/views/projects/add_document.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_document_new)%>

+ +<%= start_form_tag( { :action => 'add_document', :id => @project }, :class => "tabular", :multipart => true) %> +<%= render :partial => 'documents/form' %> + +
+

+<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= human_size(Attachment.max_size) %>)

+
+ +<%= submit_tag l(:button_create) %> +<%= end_form_tag %> + + diff --git a/issue_relations/app/views/projects/add_file.rhtml b/issue_relations/app/views/projects/add_file.rhtml new file mode 100644 index 000000000..baffbe8e8 --- /dev/null +++ b/issue_relations/app/views/projects/add_file.rhtml @@ -0,0 +1,15 @@ +

<%=l(:label_attachment_new)%>

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

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

+ +

+<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= human_size(Attachment.max_size) %>)

+
+<%= submit_tag l(:button_add) %> +<%= end_form_tag %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/add_issue.rhtml b/issue_relations/app/views/projects/add_issue.rhtml new file mode 100644 index 000000000..951b53bb9 --- /dev/null +++ b/issue_relations/app/views/projects/add_issue.rhtml @@ -0,0 +1,57 @@ +

<%=l(:label_issue_new)%>: <%= @tracker.name %>

+ +<% labelled_tabular_form_for :issue, @issue, :url => {:action => 'add_issue'}, :html => {:multipart => true} do |f| %> +<%= error_messages_for 'issue' %> +
+ +<%= hidden_field_tag 'tracker_id', @tracker.id %> + +
+

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

+

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

+

<%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>

+
+
+

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

+
+ +
+

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

+

<%= f.text_area :description, :cols => 60, :rows => 10, :required => true %>

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

<%= custom_field_tag_with_label @custom_value %>

+<% end %> + +

+<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= human_size(Attachment.max_size) %>)

+ +
+ +
+<%= submit_tag l(:button_create) %> +<% end %> + +<% unless $RDM_TEXTILE_DISABLED %> +<%= javascript_include_tag 'jstoolbar' %> + +<% end %> + +<% 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 %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/add_news.rhtml b/issue_relations/app/views/projects/add_news.rhtml new file mode 100644 index 000000000..a6ecd3da7 --- /dev/null +++ b/issue_relations/app/views/projects/add_news.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_news_new)%>

+ +<% labelled_tabular_form_for :news, @news, :url => { :action => "add_news" } do |f| %> +<%= render :partial => 'news/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/add_query.rhtml b/issue_relations/app/views/projects/add_query.rhtml new file mode 100644 index 000000000..174a1bc2f --- /dev/null +++ b/issue_relations/app/views/projects/add_query.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_query_new) %>

+ +<%= start_form_tag :action => 'add_query', :id => @project %> + <%= render :partial => 'queries/form', :locals => {:query => @query} %> + <%= submit_tag l(:button_create) %> +<%= end_form_tag %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/add_version.rhtml b/issue_relations/app/views/projects/add_version.rhtml new file mode 100644 index 000000000..c038b7de9 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/projects/calendar.rhtml b/issue_relations/app/views/projects/calendar.rhtml new file mode 100644 index 000000000..809d593d6 --- /dev/null +++ b/issue_relations/app/views/projects/calendar.rhtml @@ -0,0 +1,74 @@ +

<%= l(:label_calendar) %>

+ + <%= start_form_tag :action => 'calendar', :id => @project %> + + + + + + +
+ <%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), + {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }}, + {:href => url_for(:action => 'calendar', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))} + %> + + <%= select_month(@month, :prefix => "month", :discard_type => true) %> + <%= select_year(@year, :prefix => "year", :discard_type => true) %> + <%= submit_tag l(:button_submit), :class => "button-small" %> + + <%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'), + {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }}, + {:href => url_for(:action => 'calendar', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))} + %>  +
+ <%= end_form_tag %> +
+ + + + + +<% 1.upto(7) do |d| %> + +<% end %> + + + + +<% day = @date_from +while day <= @date_to + if day.cwday == 1 %> + + <% end %> + + <%= '' if day.cwday >= 7 and day!=@date_to %> + <% + day = day + 1 +end %> + + +
<%= day_name(d) %>
<%= day.cweek %>" style="width:14%; <%= Date.today == day ? 'background:#FDFED0;' : '' %>"> +

<%= day==Date.today ? "#{day.day}" : day.day %>

+ <% day_issues = [] + @issues.each { |i| day_issues << i if i.start_date == day or i.due_date == day } + day_issues.each do |i| %> +
+ <%= if day == i.start_date and 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 %> + <%= link_to "#{i.tracker.name} ##{i.id}", { :controller => 'issues', :action => 'show', :id => i } %>: <%=h i.subject.sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)') %> + + <%= render :partial => "issues/tooltip", :locals => { :issue => i }%> + +
+ <% end %> +
+ +<%= 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) %>
\ No newline at end of file diff --git a/issue_relations/app/views/projects/changelog.rhtml b/issue_relations/app/views/projects/changelog.rhtml new file mode 100644 index 000000000..250957a8c --- /dev/null +++ b/issue_relations/app/views/projects/changelog.rhtml @@ -0,0 +1,28 @@ +

<%=l(:label_change_log)%>

+ +
+ +
+<%= start_form_tag %> +<%=l(:label_tracker_plural)%>
+<% @trackers.each do |tracker| %> + <%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %> + <%= tracker.name %>
+<% end %> +

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

+<%= end_form_tag %> +
+ +<% ver_id = nil + @fixed_issues.each do |issue| %> + <% unless ver_id == issue.fixed_version_id %> + <% if ver_id %><% end %> +

<%= issue.fixed_version.name %>

+

<%= format_date(issue.fixed_version.effective_date) %>
+ <%=h issue.fixed_version.description %>

+
    + <% ver_id = issue.fixed_version_id + end %> +
  • <%= link_to "#{issue.tracker.name} #{issue.id}", :controller => 'issues', :action => 'show', :id => issue %>: <%=h issue.subject %>
  • +<% end %> +
\ No newline at end of file diff --git a/issue_relations/app/views/projects/destroy.rhtml b/issue_relations/app/views/projects/destroy.rhtml new file mode 100644 index 000000000..d11705be4 --- /dev/null +++ b/issue_relations/app/views/projects/destroy.rhtml @@ -0,0 +1,14 @@ +

<%=l(:label_confirmation)%>

+
+
+

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

+ +

+ <%= start_form_tag({:controller => 'projects', :action => 'destroy', :id => @project}) %> + <%= hidden_field_tag "confirm", 1 %> + <%= submit_tag l(:button_delete) %> + <%= end_form_tag %> +

+
+
\ No newline at end of file diff --git a/issue_relations/app/views/projects/export_issues_pdf.rfpdf b/issue_relations/app/views/projects/export_issues_pdf.rfpdf new file mode 100644 index 000000000..09592d391 --- /dev/null +++ b/issue_relations/app/views/projects/export_issues_pdf.rfpdf @@ -0,0 +1,10 @@ +<% pdf=IfpdfHelper::IFPDF.new + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + @issues.each {|i| + pdf.AddPage + render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => i } + } +%> + +<%= pdf.Output %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/gantt.rfpdf b/issue_relations/app/views/projects/gantt.rfpdf new file mode 100644 index 000000000..8b8afb85d --- /dev/null +++ b/issue_relations/app/views/projects/gantt.rfpdf @@ -0,0 +1,168 @@ +<% +pdf=IfpdfHelper::IFPDF.new +pdf.AliasNbPages +pdf.footer_date = format_date(Date.today) +pdf.AddPage("L") +pdf.SetFont('Arial','B',12) +pdf.SetX(15) +pdf.Cell(70, 20, @project.name) +pdf.Ln +pdf.SetFont('Arial','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.SetFont('Arial','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)[0,1], "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.SetFont('Arial','B',7) +@issues.each do |i| + pdf.SetY(top) + pdf.SetX(15) + pdf.Cell(subject_width-15, 5, "#{i.tracker.name} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR") + + pdf.SetY(top) + pdf.SetX(subject_width) + pdf.Cell(g_width, 5, "", "LR") + + 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.SetY(top+1.5) + 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}%") + + 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/issue_relations/app/views/projects/gantt.rhtml b/issue_relations/app/views/projects/gantt.rhtml new file mode 100644 index 000000000..f20e92827 --- /dev/null +++ b/issue_relations/app/views/projects/gantt.rhtml @@ -0,0 +1,215 @@ +
+<%= l(:label_export_to) %> +<%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :output => 'pdf'}, :class => 'icon icon-pdf' %> +
+ +

<%= l(:label_gantt) %>

+ + + + + + +
+<%= start_form_tag %> +

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

+<%= end_form_tag %> +
+<%= if @zoom < 4 + link_to image_tag('zoom_in.png'), {:zoom => (@zoom+1), :year => @year_from, :month => @month_from, :months => @months} + 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 + else + image_tag 'zoom_out_g.png' + end %> +
+
+ +<% zoom = 1 +@zoom.times { zoom = zoom * 2 } + +subject_width = 260 +header_heigth = 18 + +headers_heigth = header_heigth +show_weeks = false +show_days = false + +if @zoom >1 + show_weeks = true + headers_heigth = 2*header_heigth + if @zoom > 2 + show_days = true + headers_heigth = 3*header_heigth + end +end + +g_width = (@date_to - @date_from + 1)*zoom +g_height = [(20 * @issues.length + 6)+150, 206].max +t_height = g_height + headers_heigth +%> + + + + + + +
+ +
+
+
+<% +# +# Tasks subjects +# +top = headers_heigth + 8 +@issues.each do |i| %> +
+ <%= link_to "#{i.tracker.name} ##{i.id}", { :controller => 'issues', :action => 'show', :id => i }, :title => "#{i.subject}" %>: + <%=h i.subject.sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)') %> +
+<% 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 }, :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)[0,1] %> +
+ <% + left = left + width+1 + wday = wday + 1 + wday = 1 if wday > 7 + end +end %> + +<% +# +# Today red line +# +if Date.today >= @date_from and Date.today <= @date_to %> +
 
+<% end %> + +<% +# +# Tasks +# +top = headers_heigth + 10 +@issues.each do |i| %> + <% + 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 :partial => "issues/tooltip", :locals => { :issue => i }%> +
+ <% top = top + 20 +end %> +
+
+ + + + + + +
<%= link_to ('« ' + l(:label_previous)), :year => (@date_from << @months).year, :month => (@date_from << @months).month, :zoom => @zoom, :months => @months %><%= link_to (l(:label_next) + ' »'), :year => (@date_from >> @months).year, :month => (@date_from >> @months).month, :zoom => @zoom, :months => @months %>
\ No newline at end of file diff --git a/issue_relations/app/views/projects/list.rhtml b/issue_relations/app/views/projects/list.rhtml new file mode 100644 index 000000000..0723a7081 --- /dev/null +++ b/issue_relations/app/views/projects/list.rhtml @@ -0,0 +1,21 @@ +

<%=l(:label_public_projects)%>

+ + + + <%= sort_header_tag('name', :caption => l(:label_project)) %> + + <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + + +<% for project in @projects %> + "> + + + + +<% end %> + +
<%=l(:field_description)%>
<%= link_to project.name, :action => 'show', :id => project %><%=h project.description %><%= format_date(project.created_on) %>
+ +<%= pagination_links_full @project_pages %> +[ <%= @project_pages.current.first_item %> - <%= @project_pages.current.last_item %> / <%= @project_count %> ] \ No newline at end of file diff --git a/issue_relations/app/views/projects/list_documents.rhtml b/issue_relations/app/views/projects/list_documents.rhtml new file mode 100644 index 000000000..ab48badae --- /dev/null +++ b/issue_relations/app/views/projects/list_documents.rhtml @@ -0,0 +1,13 @@ +
+<%= link_to_if_authorized l(:label_document_new), {:controller => 'projects', :action => 'add_document', :id => @project}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_document_plural)%>

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

<%= l(:label_no_data) %>

<% end %> + +<% documents = @documents.group_by {|d| d.category } %> +<% documents.each do |category, docs| %> +

<%= category.name %>

+ <%= render :partial => 'documents/document', :collection => docs %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/list_files.rhtml b/issue_relations/app/views/projects/list_files.rhtml new file mode 100644 index 000000000..660d8bb07 --- /dev/null +++ b/issue_relations/app/views/projects/list_files.rhtml @@ -0,0 +1,44 @@ +
+<%= 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') %> + + + + + + + + + + <% 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)%><%=l(:field_filename)%><%=l(:label_date)%><%=l(:field_filesize)%>D/LMD5
<%= version.name %>
<%= link_to file.filename, :controller => 'versions', :action => 'download', :id => version, :attachment_id => file %><%= format_date(file.created_on) %><%= human_size(file.filesize) %><%= file.downloads %><%= file.digest %> +
+ <%= link_to_if_authorized '', {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+
\ No newline at end of file diff --git a/issue_relations/app/views/projects/list_issues.rhtml b/issue_relations/app/views/projects/list_issues.rhtml new file mode 100644 index 000000000..2d7fc1f54 --- /dev/null +++ b/issue_relations/app/views/projects/list_issues.rhtml @@ -0,0 +1,84 @@ +<% if @query.new_record? %> +
+ <%= render :partial => 'issues/add_shortcut', :locals => {:trackers => @trackers } %> +
+

<%=l(:label_issue_plural)%>

+ + <%= start_form_tag({:action => 'list_issues'}, :id => 'query_form') %> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> + <%= end_form_tag %> +
+ <%= link_to_remote l(:button_apply), + { :url => { :controller => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1 }, + :update => "content", + :with => "Form.serialize('query_form')" + }, :class => 'icon icon-edit' %> + + <%= link_to l(:button_clear), {:controller => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1}, :class => 'icon icon-del' %> + <% if authorize_for('projects', 'add_query') %> + + <%= link_to_remote l(:button_save), + { :url => { :controller => 'projects', :action => "add_query", :id => @project }, + :method => 'get', + :update => "content", + :with => "Form.serialize('query_form')" + }, :class => 'icon icon-save' %> + <% end %> +
+
+<% else %> +
+ <%= render :partial => 'issues/add_shortcut', :locals => {:trackers => @trackers } %> + <% if authorize_for('projects', 'add_query') %> + <%= 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), :post => true, :class => 'icon icon-del' %> + <% end %> +
+

<%= @query.name %>

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

<%= l(:label_no_data) %>

+<% else %> +  +<%= start_form_tag({:controller => 'projects', :action => 'move_issues', :id => @project}, :id => 'issues_form' ) %> + + + + <%= sort_header_tag('issues.id', :caption => '#') %> + <%= sort_header_tag('issues.tracker_id', :caption => l(:field_tracker)) %> + <%= sort_header_tag('issue_statuses.name', :caption => l(:field_status)) %> + + <%= sort_header_tag('users.lastname', :caption => l(:field_author)) %> + <%= sort_header_tag('issues.created_on', :caption => l(:field_created_on)) %> + <%= sort_header_tag('issues.updated_on', :caption => l(:field_updated_on)) %> + + + <% for issue in @issues %> + "> + + + + + + + + + + <% end %> + +
<%=l(:field_subject)%>
<%= check_box_tag "issue_ids[]", issue.id, false, :id => "issue_#{issue.id}" %><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %><%= issue.tracker.name %>
<%= issue.status.name %>
<%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %><%= issue.author.display_name %><%= format_time(issue.created_on) %><%= format_time(issue.updated_on) %>
+
+<%= l(:label_export_to) %> +<%= link_to 'CSV', {:action => 'export_issues_csv', :id => @project}, :class => 'icon icon-csv' %>, +<%= link_to 'PDF', {:action => 'export_issues_pdf', :id => @project}, :class => 'icon icon-pdf' %> +
+

<%= submit_tag l(:button_move), :class => "button-small" %> +  +<%= pagination_links_full @issue_pages %> +[ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ] +

+<%= end_form_tag %> +<% end %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/projects/list_members.rhtml b/issue_relations/app/views/projects/list_members.rhtml new file mode 100644 index 000000000..655abb280 --- /dev/null +++ b/issue_relations/app/views/projects/list_members.rhtml @@ -0,0 +1,11 @@ +

<%=l(:label_member_plural)%>

+ +<% members = @members.group_by {|m| m.role } %> +<% members.each do |role, member| %> +

<%= role.name %>

+
    +<% member.each do |m| %> +
  • <%= link_to m.user.display_name, :controller => 'account', :action => 'show', :id => m.user %> (<%= format_date m.created_on %>)
  • +<% end %> +
+<% end %> diff --git a/issue_relations/app/views/projects/list_news.rhtml b/issue_relations/app/views/projects/list_news.rhtml new file mode 100644 index 000000000..6cdf75c4a --- /dev/null +++ b/issue_relations/app/views/projects/list_news.rhtml @@ -0,0 +1,9 @@ +
+<%= link_to_if_authorized l(:label_news_new), {:controller => 'projects', :action => 'add_news', :id => @project}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_news_plural)%>

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

<%= l(:label_no_data) %>

<% end %> +<%= render :partial => 'news/news', :collection => @news %> +<%= pagination_links_full @news_pages %> diff --git a/issue_relations/app/views/projects/move_issues.rhtml b/issue_relations/app/views/projects/move_issues.rhtml new file mode 100644 index 000000000..772cdeb0d --- /dev/null +++ b/issue_relations/app/views/projects/move_issues.rhtml @@ -0,0 +1,24 @@ +

<%=l(:button_move)%>

+ + +<%= start_form_tag({:action => 'move_issues', :id => @project}, :class => "tabular") %> + +
+

+<% for issue in @issues %> + <%= link_to issue.long_id, :controller => 'issues', :action => 'show', :id => issue %> - <%=h issue.subject %> + <%= hidden_field_tag "issue_ids[]", issue.id %>
+<% end %> +(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)

+ +  + + +

+<%= select_tag "new_project_id", options_from_collection_for_select(@projects, "id", "name", @project.id) %>

+ +

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

+
+<%= submit_tag l(:button_move) %> +<%= end_form_tag %> diff --git a/issue_relations/app/views/projects/settings.rhtml b/issue_relations/app/views/projects/settings.rhtml new file mode 100644 index 000000000..605946576 --- /dev/null +++ b/issue_relations/app/views/projects/settings.rhtml @@ -0,0 +1,105 @@ +

<%=l(:label_settings)%>

+ +<% if authorize_for('projects', 'edit') %> + <% 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 %> +
  +<% end %> + +
+

<%=l(:label_member_plural)%>

+<%= error_messages_for 'member' %> + +<% for member in @project.members.find(:all, :include => :user) %> + <% unless member.new_record? %> + + + + + + <% end %> +<% end %> +
<%= member.user.display_name %> + <% if authorize_for('members', 'edit') %> + <%= start_form_tag :controller => 'members', :action => 'edit', :id => member %> + + <%= submit_tag l(:button_change), :class => "button-small" %> + <%= end_form_tag %> + <% end %> + + <%= link_to_if_authorized l(:button_delete), {:controller => 'members', :action => 'destroy', :id => member}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+<% if authorize_for('projects', 'add_member') %> +
+
+ <%= start_form_tag :controller => 'projects', :action => 'add_member', :id => @project %> + + + <%= submit_tag l(:button_add) %> + <%= end_form_tag %> +<% end %> +
+ +
+

<%=l(:label_version_plural)%>

+ +<% for version in @project.versions %> + + + + + + +<% end %> +
<%=h version.name %><%= format_date(version.effective_date) %><%=h version.description %> + <%= 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), :post => true, :class => 'icon icon-del' %> +
+<% if authorize_for('projects', 'add_version') %> +
+ <%= link_to l(:label_version_new), :controller => 'projects', :action => 'add_version', :id => @project %> +<% end %> +
+ + +
+

<%=l(:label_issue_category_plural)%>

+ +<% for @category in @project.issue_categories %> + <% unless @category.new_record? %> + + + + + + <% end %> +<% end %> +
+ <%= start_form_tag :controller => 'issue_categories', :action => 'edit', :id => @category %> + <%= text_field 'category', 'name', :size => 25 %> + + <% if authorize_for('issue_categories', 'edit') %> + <%= submit_tag l(:button_save), :class => "button-small" %> + <%= end_form_tag %> + <% end %> + + <%= link_to_if_authorized l(:button_delete), {:controller => 'issue_categories', :action => 'destroy', :id => @category}, :confirm => l(:text_are_you_sure), :post => true, :class => 'icon icon-del' %> +
+<% if authorize_for('projects', 'add_issue_category') %> +
+ <%= start_form_tag :action => 'add_issue_category', :id => @project %> +
+ <%= error_messages_for 'issue_category' %> + <%= text_field 'issue_category', 'name', :size => 25 %> + <%= submit_tag l(:button_create) %> + <%= end_form_tag %> +<% end %> +
diff --git a/issue_relations/app/views/projects/show.rhtml b/issue_relations/app/views/projects/show.rhtml new file mode 100644 index 000000000..4c9e4e04a --- /dev/null +++ b/issue_relations/app/views/projects/show.rhtml @@ -0,0 +1,55 @@ +

<%=l(:label_overview)%>

+ +
+ <%= simple_format(auto_link(h(@project.description))) %> +
    + <% unless @project.homepage.empty? %>
  • <%=l(:field_homepage)%>: <%= auto_link @project.homepage %>
  • <% end %> +
  • <%=l(:field_created_on)%>: <%= format_date(@project.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 %> +
+ +
+
+ <%= render :partial => 'issues/add_shortcut', :locals => {:trackers => @trackers } %> +
+

<%=l(:label_tracker_plural)%>

+
    + <% for tracker in @trackers %> +
  • <%= link_to tracker.name, :controller => 'projects', :action => 'list_issues', :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 => 'projects', :action => 'list_issues', :id => @project, :set_filter => 1 %>

+
+
+ +
+
+

<%=l(:label_member_plural)%>

+ <% for member in @members %> + <%= link_to_user member.user %> (<%= member.role.name %>)
+ <% end %> +
+ + <% if @subprojects %> +
+

<%=l(:label_subproject_plural)%>

+ <% for subproject in @subprojects %> + <%= link_to subproject.name, :action => 'show', :id => subproject %>
+ <% end %> +
+ <% end %> + +
+

<%=l(:label_news_latest)%>

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

<%= link_to l(:label_news_view_all), :controller => 'projects', :action => 'list_news', :id => @project %>

+
+
\ No newline at end of file diff --git a/issue_relations/app/views/queries/_filters.rhtml b/issue_relations/app/views/queries/_filters.rhtml new file mode 100644 index 000000000..3b73a9d7a --- /dev/null +++ b/issue_relations/app/views/queries/_filters.rhtml @@ -0,0 +1,100 @@ + + +
<%= l(:label_filter_plural) %> + + + + + +
+ +<% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %> + <% field = filter[0] + options = filter[1] %> + id="tr_<%= field %>"> + + + + +<% 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;" %> + +
+ <% case options[:type] + when :list, :list_optional, :list_status %> + + <%= link_to_function image_tag('expand.png'), "toggle_multi_select('#{field}');" %> + <% when :date, :date_past %> + <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %> + <% when :text %> + <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %> + <% end %> +
+ +
+
+<%= 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| [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" %> +
+
\ No newline at end of file diff --git a/issue_relations/app/views/queries/_form.rhtml b/issue_relations/app/views/queries/_form.rhtml new file mode 100644 index 000000000..d50b1e9b9 --- /dev/null +++ b/issue_relations/app/views/queries/_form.rhtml @@ -0,0 +1,12 @@ +<%= error_messages_for 'query' %> + + +
+
+

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

+
+ +<%= render :partial => 'queries/filters', :locals => {:query => query}%> +
+ \ No newline at end of file diff --git a/issue_relations/app/views/queries/edit.rhtml b/issue_relations/app/views/queries/edit.rhtml new file mode 100644 index 000000000..71f146f1b --- /dev/null +++ b/issue_relations/app/views/queries/edit.rhtml @@ -0,0 +1,6 @@ +

<%= l(:label_query) %>

+ +<%= start_form_tag :action => 'edit', :id => @query %> + <%= render :partial => 'form', :locals => {:query => @query} %> + <%= submit_tag l(:button_save) %> +<%= end_form_tag %> \ No newline at end of file diff --git a/issue_relations/app/views/reports/_details.rhtml b/issue_relations/app/views/reports/_details.rhtml new file mode 100644 index 000000000..e52538f73 --- /dev/null +++ b/issue_relations/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 => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id %><%= link_to (aggregate data, { field_name => row.id, "status_id" => status.id }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "status_id" => status.id, + "#{field_name}" => row.id %><%= link_to (aggregate data, { field_name => row.id, "closed" => 0 }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "o" %><%= link_to (aggregate data, { field_name => row.id, "closed" => 1 }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "c" %><%= link_to (aggregate data, { field_name => row.id }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "*" %>
+<% end + reset_cycle %> \ No newline at end of file diff --git a/issue_relations/app/views/reports/_simple.rhtml b/issue_relations/app/views/reports/_simple.rhtml new file mode 100644 index 000000000..1e66210f9 --- /dev/null +++ b/issue_relations/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 => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id %><%= link_to (aggregate data, { field_name => row.id, "closed" => 0 }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "o" %><%= link_to (aggregate data, { field_name => row.id, "closed" => 1 }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "c" %><%= link_to (aggregate data, { field_name => row.id }), + :controller => 'projects', :action => 'list_issues', :id => @project, + :set_filter => 1, + "#{field_name}" => row.id, + "status_id" => "*" %>
+<% end + reset_cycle %> \ No newline at end of file diff --git a/issue_relations/app/views/reports/issue_report.rhtml b/issue_relations/app/views/reports/issue_report.rhtml new file mode 100644 index 000000000..31d670761 --- /dev/null +++ b/issue_relations/app/views/reports/issue_report.rhtml @@ -0,0 +1,32 @@ +

<%=l(:label_report_plural)%>

+ +

<%= l(:label_query_plural) %>

+
+<%= link_to_if_authorized l(:label_query_new), {:controller => 'projects', :action => 'add_query', :id => @project}, :class => 'icon icon-add' %> +
+ +<% if @queries.empty? %>

<%=l(:label_no_data)%>

<% end %> +
    +<% @queries.each do |query| %> +
  • <%= link_to query.name, :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => query %>
  • +<% end %> +
+ +
+

<%=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_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_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_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/issue_relations/app/views/reports/issue_report_details.rhtml b/issue_relations/app/views/reports/issue_report_details.rhtml new file mode 100644 index 000000000..e37d38649 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/repositories/_dir_list.rhtml b/issue_relations/app/views/repositories/_dir_list.rhtml new file mode 100644 index 000000000..717802b4a --- /dev/null +++ b/issue_relations/app/views/repositories/_dir_list.rhtml @@ -0,0 +1,23 @@ + + + + + + + + + +<% total_size = 0 +@entries.each do |entry| %> + + + + + + + +<% total_size += entry.size +end %> + +
<%= l(:field_name) %><%= l(:field_filesize) %><%= l(:label_revision) %><%= l(:field_author) %><%= l(:label_date) %>
<%= link_to h(entry.name), { :action => (entry.is_dir? ? 'browse' : 'revisions'), :id => @project, :path => entry.path, :rev => @rev }, :class => ("icon " + (entry.is_dir? ? 'icon-folder' : 'icon-file')) %><%= human_size(entry.size) unless entry.is_dir? %><%= link_to entry.lastrev.identifier, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier %><%=h entry.lastrev.author %><%= format_time(entry.lastrev.time) %>
+

<%= l(:label_total) %>: <%= human_size(total_size) %>

\ No newline at end of file diff --git a/issue_relations/app/views/repositories/_navigation.rhtml b/issue_relations/app/views/repositories/_navigation.rhtml new file mode 100644 index 000000000..3ae0f7612 --- /dev/null +++ b/issue_relations/app/views/repositories/_navigation.rhtml @@ -0,0 +1,18 @@ +<%= 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| + 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 => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %> +<% end %> + +<%= "@ #{revision}" if revision %> \ No newline at end of file diff --git a/issue_relations/app/views/repositories/browse.rhtml b/issue_relations/app/views/repositories/browse.rhtml new file mode 100644 index 000000000..cdd4e6882 --- /dev/null +++ b/issue_relations/app/views/repositories/browse.rhtml @@ -0,0 +1,14 @@ +
+<%= start_form_tag %> +

<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<%= submit_tag 'OK' %>

+<%= end_form_tag %> +
+ +

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

+ +<%= render :partial => 'dir_list' %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/repositories/diff.rhtml b/issue_relations/app/views/repositories/diff.rhtml new file mode 100644 index 000000000..0060e7867 --- /dev/null +++ b/issue_relations/app/views/repositories/diff.rhtml @@ -0,0 +1,57 @@ +

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

+ + + + +<% parsing = false +line_num_l = 0 +line_num_r = 0 %> +<% @diff.each do |line| %> +<% + if line =~ /^@@ (\+|\-)(\d+),\d+ (\+|\-)(\d+),\d+ @@/ + line_num_l = $2.to_i + line_num_r = $4.to_i + if parsing %> + + <% end + parsing = true + next + end + next unless parsing +%> + + + +<% case line[0, 1] + when " " %> + + + + + + + + +<% end %> + +
@<%= @rev %>@<%= @rev_to %>
 
<%= line_num_l %><%= line_num_r %> +<% line_num_l = line_num_l + 1 + line_num_r = line_num_r + 1 + + when "-" %> +<%= line_num_r %> +<% line_num_r = line_num_r + 1 + + when "+" %> +<%= line_num_l %> +<% line_num_l = line_num_l + 1 + + else + next + end %> + +<%= h(line[1..-1]).gsub(/\s/, " ") %>
+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/repositories/revision.rhtml b/issue_relations/app/views/repositories/revision.rhtml new file mode 100644 index 000000000..f616a0180 --- /dev/null +++ b/issue_relations/app/views/repositories/revision.rhtml @@ -0,0 +1,38 @@ +
+<%= start_form_tag %> +

<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<%= submit_tag 'OK' %>

+<%= end_form_tag %> +
+ +

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

+ +

<%= @revision.author %>, <%= format_time(@revision.time) %>

+<%= textilizable @revision.message %> + +
+
<%= l(:label_added) %> 
+
<%= l(:label_modified) %> 
+
<%= l(:label_deleted) %> 
+
+ +

<%= l(:label_attachment_plural) %>

+ + +<% @revision.paths.each do |path| %> + + + + +<% end %> + +
<%= path[:path] %>
+<% if path[:action] == "M" %> +<%= link_to 'View diff', :action => 'diff', :id => @project, :path => path[:path].gsub(/^\//, ''), :rev => @revision.identifier %> +<% end %> +
+

<%= lwr(:label_modification, @revision.paths.length) %>

+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/repositories/revisions.rhtml b/issue_relations/app/views/repositories/revisions.rhtml new file mode 100644 index 000000000..6ab8df400 --- /dev/null +++ b/issue_relations/app/views/repositories/revisions.rhtml @@ -0,0 +1,41 @@ +
+<%= start_form_tag %> +

<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<%= submit_tag 'OK' %>

+<%= end_form_tag %> +
+ +

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

+ +<% if @entry.is_file? %> +

<%=h @entry.name %>

+

<%= link_to 'Download', {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }, :class => "icon file" %> (<%= human_size @entry.size %>)

+<% end %> + +

Revisions

+ + + + + + + + + + +<% @revisions.each do |revision| %> + + + + + + + +<% end %> + +
#<%= l(:field_author) %><%= l(:label_date) %><%= l(:field_description) %>
<%= link_to revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier %><%=h revision.author %><%= format_time(revision.time) %><%= textilizable(revision.message) %><%= link_to 'Diff', :action => 'diff', :id => @project, :path => @path, :rev => revision.identifier if @entry.is_file? && revision != @revisions.last %>
+

<%= lwr(:label_modification, @revisions.length) %>

+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/repositories/show.rhtml b/issue_relations/app/views/repositories/show.rhtml new file mode 100644 index 000000000..9c2adc929 --- /dev/null +++ b/issue_relations/app/views/repositories/show.rhtml @@ -0,0 +1,17 @@ +

<%= l(:label_repository) %>

+ +

<%= l(:label_revision_plural) %>

+<% if @latest_revision %> +

<%= l(:label_latest_revision) %>: + <%= link_to @latest_revision.identifier, :action => 'revision', :id => @project, :rev => @latest_revision.identifier %>
+ <%= @latest_revision.author %>, <%= format_time(@latest_revision.time) %>

+<% end %> +

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

+ + +

<%= l(:label_browse) %>

+<%= render :partial => 'dir_list' %> + +<% content_for :header_tags do %> +<%= stylesheet_link_tag "scm" %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/roles/_form.rhtml b/issue_relations/app/views/roles/_form.rhtml new file mode 100644 index 000000000..df4a58dd3 --- /dev/null +++ b/issue_relations/app/views/roles/_form.rhtml @@ -0,0 +1,20 @@ +<%= error_messages_for 'role' %> +
+ +

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

+ +

<%=l(:label_permissions)%>

+<% permissions = @permissions.group_by {|p| p.group_id } %> +<% permissions.keys.sort.each do |group_id| %> +
<%= l(Permission::GROUPS[group_id]) %> +<% permissions[group_id].each do |p| %> +
<%= check_box_tag "permission_ids[]", p.id, (@role.permissions.include? p) %> + <%= l(p.description.to_sym) %> +
+<% end %> +
+<% end %> +
+<%= check_all_links 'role_form' %> + +
diff --git a/issue_relations/app/views/roles/edit.rhtml b/issue_relations/app/views/roles/edit.rhtml new file mode 100644 index 000000000..ffe117cef --- /dev/null +++ b/issue_relations/app/views/roles/edit.rhtml @@ -0,0 +1,6 @@ +

<%=l(:label_role)%>

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

<%=l(:label_role_plural)%>

+ + + + + + + +<% for role in @roles %> + "> + + +<% end %> + +
<%=l(:label_role)%>
<%= link_to role.name, :action => 'edit', :id => role %> + <%= button_to l(:button_delete), { :action => 'destroy', :id => role }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +<%= pagination_links_full @role_pages %> \ No newline at end of file diff --git a/issue_relations/app/views/roles/new.rhtml b/issue_relations/app/views/roles/new.rhtml new file mode 100644 index 000000000..a73c36cb1 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/roles/workflow.rhtml b/issue_relations/app/views/roles/workflow.rhtml new file mode 100644 index 000000000..b544ab4ce --- /dev/null +++ b/issue_relations/app/views/roles/workflow.rhtml @@ -0,0 +1,69 @@ +

<%=l(:label_workflow)%>

+ +

<%=l(:text_workflow_edit)%>:

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


+

+
+ +
+


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

+
+<%= end_form_tag %> + + + +<% unless @tracker.nil? or @role.nil? %> +
+ <%= form_tag ({:action => 'workflow', :role_id => @role, :tracker_id => @tracker }, :id => 'workflow_form' ) %> + + + + + + + + <% for new_status in @statuses %> + + <% end %> + + + <% for old_status in @statuses %> + + + <% for new_status in @statuses %> + + <% end %> + + + <% end %> +
<%=l(:label_current_status)%><%=l(:label_new_statuses_allowed)%>
<%= new_status.name %>
<%= old_status.name %>
+ + checked="checked"<%end%> + <%if old_status==new_status%>disabled<%end%> + > +
+
+

+<%=l(:button_check_all)%> | +<%=l(:button_uncheck_all)%> +

+
+<%= submit_tag l(:button_save) %> +<%= end_form_tag %> + +<% end %> +
\ No newline at end of file diff --git a/issue_relations/app/views/trackers/_form.rhtml b/issue_relations/app/views/trackers/_form.rhtml new file mode 100644 index 000000000..625c0d636 --- /dev/null +++ b/issue_relations/app/views/trackers/_form.rhtml @@ -0,0 +1,7 @@ +<%= error_messages_for 'tracker' %> +
+ +

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

+

<%= f.check_box :is_in_chlog %>

+ +
diff --git a/issue_relations/app/views/trackers/edit.rhtml b/issue_relations/app/views/trackers/edit.rhtml new file mode 100644 index 000000000..d8411099c --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/trackers/list.rhtml b/issue_relations/app/views/trackers/list.rhtml new file mode 100644 index 000000000..644b1324e --- /dev/null +++ b/issue_relations/app/views/trackers/list.rhtml @@ -0,0 +1,24 @@ +
+<%= link_to l(:label_tracker_new), {:action => 'new'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_tracker_plural)%>

+ + + + + + + +<% for tracker in @trackers %> + "> + + + +<% end %> + +
<%=l(:label_tracker)%>
<%= link_to tracker.name, :action => 'edit', :id => tracker %> + <%= button_to l(:button_delete), { :action => 'destroy', :id => tracker }, :confirm => l(:text_are_you_sure), :class => "button-small" %> +
+ +<%= pagination_links_full @tracker_pages %> \ No newline at end of file diff --git a/issue_relations/app/views/trackers/new.rhtml b/issue_relations/app/views/trackers/new.rhtml new file mode 100644 index 000000000..b318a5dc4 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/users/_form.rhtml b/issue_relations/app/views/users/_form.rhtml new file mode 100644 index 000000000..e486c1042 --- /dev/null +++ b/issue_relations/app/views/users/_form.rhtml @@ -0,0 +1,37 @@ +<%= 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 %>

+

<%= f.check_box :mail_notification %>

+
+ +
+

<%=l(:label_authentication)%>

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

<%= f.select :auth_source_id, [[l(:label_internal), ""]] + @auth_sources.collect { |a| [a.name, a.id] } %>

+<% end %> +

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

+

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

+
+ + +<% 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 %> \ No newline at end of file diff --git a/issue_relations/app/views/users/_memberships.rhtml b/issue_relations/app/views/users/_memberships.rhtml new file mode 100644 index 000000000..709aeb7a3 --- /dev/null +++ b/issue_relations/app/views/users/_memberships.rhtml @@ -0,0 +1,29 @@ +
+

<%= l(:label_project_plural) %>

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

+ + + <%= 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), :post => true, :class => 'icon icon-del' %> +

+<%= end_form_tag %> +<% end %> +
+

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

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

<%=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) %> +<% end %> \ No newline at end of file diff --git a/issue_relations/app/views/users/edit.rhtml b/issue_relations/app/views/users/edit.rhtml new file mode 100644 index 000000000..0da99d0d2 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/users/list.rhtml b/issue_relations/app/views/users/list.rhtml new file mode 100644 index 000000000..8e660329c --- /dev/null +++ b/issue_relations/app/views/users/list.rhtml @@ -0,0 +1,51 @@ +
+<%= link_to l(:label_user_new), {:action => 'add'}, :class => 'icon icon-add' %> +
+ +

<%=l(:label_user_plural)%>

+ + + + <%= 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('admin', :caption => l(:field_admin)) %> + <%= sort_header_tag('status', :caption => l(:field_status)) %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on)) %> + + + +<% for user in @users %> + "> + + + + + + + + + + +<% end %> + +
<%=l(:field_mail)%>
<%= link_to user.login, :action => 'edit', :id => user %><%= user.firstname %><%= user.lastname %><%= user.mail %><%= image_tag 'true.png' if user.admin? %><%= image_tag 'locked.png' if user.locked? %><%= image_tag 'user_new.png' if user.registered? %><%= format_time(user.created_on) %><%= format_time(user.last_login_on) unless user.last_login_on.nil? %> + <%= start_form_tag :action => 'edit', :id => user %> + <% if user.locked? %> + <%= hidden_field_tag 'user[status]', User::STATUS_ACTIVE %> + <%= submit_tag l(:button_unlock), :class => "button-small" %> + <% elsif user.registered? %> + <%= hidden_field_tag 'user[status]', User::STATUS_ACTIVE %> + <%= submit_tag l(:button_activate), :class => "button-small" %> + <% else %> + <%= hidden_field_tag 'user[status]', User::STATUS_LOCKED %> + <%= submit_tag l(:button_lock), :class => "button-small" %> + <% end %> + <%= end_form_tag %> +
+ +

<%= pagination_links_full @user_pages %> +[ <%= @user_pages.current.first_item %> - <%= @user_pages.current.last_item %> / <%= @user_count %> ] +

\ No newline at end of file diff --git a/issue_relations/app/views/versions/_form.rhtml b/issue_relations/app/views/versions/_form.rhtml new file mode 100644 index 000000000..3d6c9c208 --- /dev/null +++ b/issue_relations/app/views/versions/_form.rhtml @@ -0,0 +1,16 @@ +<%= error_messages_for 'version' %> + +
+ +

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

+

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

+

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

+ +
+ +<% 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 %> \ No newline at end of file diff --git a/issue_relations/app/views/versions/edit.rhtml b/issue_relations/app/views/versions/edit.rhtml new file mode 100644 index 000000000..1556ebba1 --- /dev/null +++ b/issue_relations/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/issue_relations/app/views/welcome/index.rhtml b/issue_relations/app/views/welcome/index.rhtml new file mode 100644 index 000000000..d32771c0f --- /dev/null +++ b/issue_relations/app/views/welcome/index.rhtml @@ -0,0 +1,27 @@ +

<%= $RDM_WELCOME_TITLE || l(:label_home) %>

+ +
+ <% if $RDM_WELCOME_TEXT %>

<%= $RDM_WELCOME_TEXT %>


<% end %> +
+

<%=l(:label_news_latest)%>

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

<%=l(:label_project_latest)%>

+
    + <% for project in @projects %> +
  • + <%= link_to project.name, :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>)
    + <%=h project.description %> +
  • + <% end %> +
+
+
+ +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:rss, {:controller => 'feeds' , :action => 'news' }) %> +<% end %> \ No newline at end of file diff --git a/issue_relations/config/boot.rb b/issue_relations/config/boot.rb new file mode 100644 index 000000000..9fcd50fe3 --- /dev/null +++ b/issue_relations/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/issue_relations/config/config_custom.example.rb b/issue_relations/config/config_custom.example.rb new file mode 100644 index 000000000..b00e716b1 --- /dev/null +++ b/issue_relations/config/config_custom.example.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. + + +# To set your own configuration, rename this file to config_custom.rb +# and edit parameters below +# Don't forget to restart the application after any change. + + +# Application host name +# Used to provide absolute links in mail notifications +# $RDM_HOST_NAME = "somenet.foo" + +# File storage path +# Directory used to store uploaded files +# #{RAILS_ROOT} represents application's home directory +# $RDM_STORAGE_PATH = "#{RAILS_ROOT}/files" + +# Set $RDM_LOGIN_REQUIRED to true if you want to force users to login +# to access any page of the application +# $RDM_LOGIN_REQUIRED = false + +# Uncomment to disable user self-registration +# $RDM_SELF_REGISTRATION = false + +# Default langage ('en', 'es', 'de', 'fr' are available) +# $RDM_DEFAULT_LANG = 'en' + +# Email adress used to send mail notifications +# $RDM_MAIL_FROM = "redmine@somenet.foo" + +# Page title +# $RDM_HEADER_TITLE = "Title" + +# Page sub-title +# $RDM_HEADER_SUBTITLE = "Sub title" + +# Welcome page title +# $RDM_WELCOME_TITLE = "Welcome" + +# Welcome page text +# $RDM_WELCOME_TEXT = "" + +# Signature displayed in footer +# Email adresses will be automatically displayed as a mailto link +# $RDM_FOOTER_SIG = "admin@somenet.foo" + +# Textile formatting (only available if RedCloth is installed) +# Textile formatting is automativaly disabled if RedCloth is not available +# Set to true to manually disable. +# $RDM_TEXTILE_DISABLED = true + +# Maximum size for attachments (in bytes) +# Default to 5 MB +# $RDM_ATTACHMENT_MAX_SIZE = 5*1024*1024 diff --git a/issue_relations/config/database.yml b/issue_relations/config/database.yml new file mode 100644 index 000000000..cfb6f13df --- /dev/null +++ b/issue_relations/config/database.yml @@ -0,0 +1,69 @@ +# 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: + +development_pgsql: + adapter: postgresql + database: redmine + host: localhost + username: postgres + password: "postgres" + +development_oracle: + adapter: oci + host: 192.168.0.14 + username: rails + password: "rails" + +development_sqlserver: + adapter: sqlserver + host: localhost,1157 + database: redmine + +test: + adapter: mysql + database: redmine_test + host: localhost + username: root + password: + +test_pgsql: + adapter: postgresql + database: redmine + host: localhost + username: postgres + password: "postgres" + +test_oracle: + adapter: oci + host: 192.168.0.14 + username: rails_test + password: "rails" + +test_sqlserver: + adapter: sqlserver + host: localhost,1157 + database: redmine_test + +demo: + adapter: sqlite3 + dbfile: db/redmine_demo.db + diff --git a/issue_relations/config/environment.rb b/issue_relations/config/environment.rb new file mode 100644 index 000000000..201051b13 --- /dev/null +++ b/issue_relations/config/environment.rb @@ -0,0 +1,140 @@ +# 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' + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +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 your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + + # 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 + + # 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 + + # 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.server_settings = { + :address => "127.0.0.1", + :port => 25, + :domain => "somenet.foo", + :authentication => :login, + :user_name => "redmine", + :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 + +# Add new inflection rules using the following format +# (all these examples are active by default): +# Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +if File.exist? File.join(File.dirname(__FILE__), 'config_custom.rb') + begin + print "=> Loading config_custom.rb... " + require File.join(File.dirname(__FILE__), 'config_custom') + puts "done." + rescue Exception => detail + puts + puts detail + puts detail.backtrace.join("\n") + puts "=> Error in config_custom.rb. Check your configuration." + exit + end +end + +# IMPORTANT !!! DO NOT MODIFY PARAMETERS HERE +# Instead, rename config_custom.example.rb to config_custom.rb +# and set your own configuration in that file +# Parameters defined in config_custom.rb override those defined below + +# application host name +$RDM_HOST_NAME ||= "localhost:3000" +# file storage path +$RDM_STORAGE_PATH ||= "#{RAILS_ROOT}/files" +# if RDM_LOGIN_REQUIRED is set to true, login is required to access the application +$RDM_LOGIN_REQUIRED ||= false +# default langage +$RDM_DEFAULT_LANG ||= 'en' +# email sender adress +$RDM_MAIL_FROM ||= "redmine@somenet.foo" + +# page title +$RDM_HEADER_TITLE ||= "redMine" +# page sub-title +$RDM_HEADER_SUBTITLE ||= "Project management" +# footer signature +$RDM_FOOTER_SIG = "admin@somenet.foo" + +# textile formatting +# automaticaly disabled if 'textile' method is not defined (RedCloth unavailable) +$RDM_TEXTILE_DISABLED = true unless ActionView::Helpers::TextHelper.method_defined? "textilize" + +# application name +RDM_APP_NAME = "redMine" +# application version +RDM_APP_VERSION = "0.4.1" + +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}" } + +GLoc.set_config :default_language => $RDM_DEFAULT_LANG +GLoc.clear_strings +GLoc.set_kcode +GLoc.load_localized_strings +GLoc.set_config(:raise_string_not_found_errors => false) + + diff --git a/issue_relations/config/environments/demo.rb b/issue_relations/config/environments/demo.rb new file mode 100644 index 000000000..52aef32f1 --- /dev/null +++ b/issue_relations/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/issue_relations/config/environments/development.rb b/issue_relations/config/environments/development.rb new file mode 100644 index 000000000..04b779200 --- /dev/null +++ b/issue_relations/config/environments/development.rb @@ -0,0 +1,19 @@ +# 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 + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = 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/issue_relations/config/environments/development_oracle.rb b/issue_relations/config/environments/development_oracle.rb new file mode 100644 index 000000000..04b779200 --- /dev/null +++ b/issue_relations/config/environments/development_oracle.rb @@ -0,0 +1,19 @@ +# 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 + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = 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/issue_relations/config/environments/development_pgsql.rb b/issue_relations/config/environments/development_pgsql.rb new file mode 100644 index 000000000..04b779200 --- /dev/null +++ b/issue_relations/config/environments/development_pgsql.rb @@ -0,0 +1,19 @@ +# 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 + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = 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/issue_relations/config/environments/development_sqlserver.rb b/issue_relations/config/environments/development_sqlserver.rb new file mode 100644 index 000000000..04b779200 --- /dev/null +++ b/issue_relations/config/environments/development_sqlserver.rb @@ -0,0 +1,19 @@ +# 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 + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = 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/issue_relations/config/environments/production.rb b/issue_relations/config/environments/production.rb new file mode 100644 index 000000000..4cd4e086b --- /dev/null +++ b/issue_relations/config/environments/production.rb @@ -0,0 +1,20 @@ +# 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 + diff --git a/issue_relations/config/environments/test.rb b/issue_relations/config/environments/test.rb new file mode 100644 index 000000000..9ba9ae0f8 --- /dev/null +++ b/issue_relations/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/issue_relations/config/environments/test_oracle.rb b/issue_relations/config/environments/test_oracle.rb new file mode 100644 index 000000000..35bb19bee --- /dev/null +++ b/issue_relations/config/environments/test_oracle.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/issue_relations/config/environments/test_pgsql.rb b/issue_relations/config/environments/test_pgsql.rb new file mode 100644 index 000000000..35bb19bee --- /dev/null +++ b/issue_relations/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/issue_relations/config/environments/test_sqlserver.rb b/issue_relations/config/environments/test_sqlserver.rb new file mode 100644 index 000000000..35bb19bee --- /dev/null +++ b/issue_relations/config/environments/test_sqlserver.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/issue_relations/config/help.yml b/issue_relations/config/help.yml new file mode 100644 index 000000000..69c45db72 --- /dev/null +++ b/issue_relations/config/help.yml @@ -0,0 +1,74 @@ +# 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. + +# available languages for help pages +langs: + - en + - fr + +# mapping between controller/action and help pages +# if action is not defined here, 'index' page will be displayed +pages: + # administration + admin: + index: ch01.html + mail_options: ch01s09.html + info: ch01s11.html + users: + index: ch01s02.html + roles: + index: ch01s03.html + workflow: ch01s07.html + trackers: + index: ch01s04.html + issue_statuses: + index: ch01s06.html + # projects + projects: + index: ch02.html + add: ch01s01.html + show: ch02s01.html + gantt: ch02s02.html + calendar: ch02s02.html + add_document: ch02s07.html + list_documents: ch02s07.html + add_issue: ch02s03.html + list_issues: ch02s03.html + add_news: ch02s06.html + list_news: ch02s06.html + add_file: ch02s08.html + list_files: ch02s08.html + changelog: ch02s05.html + issues: + index: ch02s03.html + documents: + index: ch02s07.html + news: + index: ch02s06.html + versions: + index: ch02s09.html + reports: + index: ch02s04.html + # accounts + my: + index: ch03.html + account: ch03s01.html + page: ch03s02.html + account: + index: ch03.html + lost_password: ch03s03.html + register: ch03s04.html \ No newline at end of file diff --git a/issue_relations/config/routes.rb b/issue_relations/config/routes.rb new file mode 100644 index 000000000..2559159f1 --- /dev/null +++ b/issue_relations/config/routes.rb @@ -0,0 +1,24 @@ +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 + + # You can have the root of your site routed by hooking up '' + # -- just remember to delete public/index.html. + map.connect '', :controller => "welcome" + + 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' + + # 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/issue_relations/db/migrate/001_setup.rb b/issue_relations/db/migrate/001_setup.rb new file mode 100644 index 000000000..ee22c148b --- /dev/null +++ b/issue_relations/db/migrate/001_setup.rb @@ -0,0 +1,317 @@ +# 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 + 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, :default => "" + 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, :default => "", :null => false + 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, :default => "" + 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, :default => "" + 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, :default => "", :null => false + 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, :default => "", :null => false + 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, :null => false + 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/issue_relations/db/migrate/002_issue_move.rb b/issue_relations/db/migrate/002_issue_move.rb new file mode 100644 index 000000000..d1acf7ee2 --- /dev/null +++ b/issue_relations/db/migrate/002_issue_move.rb @@ -0,0 +1,9 @@ +class IssueMove < ActiveRecord::Migration + 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/issue_relations/db/migrate/003_issue_add_note.rb b/issue_relations/db/migrate/003_issue_add_note.rb new file mode 100644 index 000000000..9f20039b0 --- /dev/null +++ b/issue_relations/db/migrate/003_issue_add_note.rb @@ -0,0 +1,9 @@ +class IssueAddNote < ActiveRecord::Migration + 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/issue_relations/db/migrate/004_export_pdf.rb b/issue_relations/db/migrate/004_export_pdf.rb new file mode 100644 index 000000000..66045553f --- /dev/null +++ b/issue_relations/db/migrate/004_export_pdf.rb @@ -0,0 +1,11 @@ +class ExportPdf < ActiveRecord::Migration + 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/issue_relations/db/migrate/005_issue_start_date.rb b/issue_relations/db/migrate/005_issue_start_date.rb new file mode 100644 index 000000000..3d1693fc6 --- /dev/null +++ b/issue_relations/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/issue_relations/db/migrate/006_calendar_and_activity.rb b/issue_relations/db/migrate/006_calendar_and_activity.rb new file mode 100644 index 000000000..5d8474fc2 --- /dev/null +++ b/issue_relations/db/migrate/006_calendar_and_activity.rb @@ -0,0 +1,13 @@ +class CalendarAndActivity < ActiveRecord::Migration + 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/issue_relations/db/migrate/007_create_journals.rb b/issue_relations/db/migrate/007_create_journals.rb new file mode 100644 index 000000000..6170b5bd3 --- /dev/null +++ b/issue_relations/db/migrate/007_create_journals.rb @@ -0,0 +1,54 @@ +class CreateJournals < ActiveRecord::Migration + + # model removed, but needed for data migration + class IssueHistory < ActiveRecord::Base; belongs_to :issue; 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/issue_relations/db/migrate/008_create_user_preferences.rb b/issue_relations/db/migrate/008_create_user_preferences.rb new file mode 100644 index 000000000..80ae1cdf9 --- /dev/null +++ b/issue_relations/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/issue_relations/db/migrate/009_add_hide_mail_pref.rb b/issue_relations/db/migrate/009_add_hide_mail_pref.rb new file mode 100644 index 000000000..a22eafd93 --- /dev/null +++ b/issue_relations/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/issue_relations/db/migrate/010_create_comments.rb b/issue_relations/db/migrate/010_create_comments.rb new file mode 100644 index 000000000..322a019bc --- /dev/null +++ b/issue_relations/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 :comment, :text, :default => "", :null => false + 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/issue_relations/db/migrate/011_add_news_comments_count.rb b/issue_relations/db/migrate/011_add_news_comments_count.rb new file mode 100644 index 000000000..a24743999 --- /dev/null +++ b/issue_relations/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/issue_relations/db/migrate/012_add_comments_permissions.rb b/issue_relations/db/migrate/012_add_comments_permissions.rb new file mode 100644 index 000000000..37f075082 --- /dev/null +++ b/issue_relations/db/migrate/012_add_comments_permissions.rb @@ -0,0 +1,11 @@ +class AddCommentsPermissions < ActiveRecord::Migration + 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/issue_relations/db/migrate/013_create_queries.rb b/issue_relations/db/migrate/013_create_queries.rb new file mode 100644 index 000000000..e0e8c90c0 --- /dev/null +++ b/issue_relations/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/issue_relations/db/migrate/014_add_queries_permissions.rb b/issue_relations/db/migrate/014_add_queries_permissions.rb new file mode 100644 index 000000000..27d674650 --- /dev/null +++ b/issue_relations/db/migrate/014_add_queries_permissions.rb @@ -0,0 +1,9 @@ +class AddQueriesPermissions < ActiveRecord::Migration + 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/issue_relations/db/migrate/015_create_repositories.rb b/issue_relations/db/migrate/015_create_repositories.rb new file mode 100644 index 000000000..d8c0524b3 --- /dev/null +++ b/issue_relations/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/issue_relations/db/migrate/016_add_repositories_permissions.rb b/issue_relations/db/migrate/016_add_repositories_permissions.rb new file mode 100644 index 000000000..992f8dccd --- /dev/null +++ b/issue_relations/db/migrate/016_add_repositories_permissions.rb @@ -0,0 +1,19 @@ +class AddRepositoriesPermissions < ActiveRecord::Migration + 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/issue_relations/doc/CHANGELOG b/issue_relations/doc/CHANGELOG new file mode 100644 index 000000000..bbd824da1 --- /dev/null +++ b/issue_relations/doc/CHANGELOG @@ -0,0 +1,112 @@ +== redMine changelog + +redMine - project management software +Copyright (C) 2006-2007 Jean-Philippe Lang +http://redmine.rubyforge.org/ + + +== 03/02/2006 v0.4.1 + +* fixed: emails have no recipient when one of the project members has notifications disabled + + +== 01/02/2006 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 + + +== 10/08/2006 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 + + +== 07/12/2006 - v0.2.2 + +* Fixed: bug in "issues list" + + +== 07/09/2006 - 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) + + +== 06/25/2006 - 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) \ No newline at end of file diff --git a/issue_relations/doc/COPYING b/issue_relations/doc/COPYING new file mode 100644 index 000000000..82fa1daad --- /dev/null +++ b/issue_relations/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/issue_relations/doc/INSTALL b/issue_relations/doc/INSTALL new file mode 100644 index 000000000..f55b81a60 --- /dev/null +++ b/issue_relations/doc/INSTALL @@ -0,0 +1,71 @@ +== redMine installation + +redMine - project management software +Copyright (C) 2006-2007 Jean-Philippe Lang +http://redmine.rubyforge.org/ + + +== Requirements + +* Ruby on Rails 1.1 +* Iconv +* A database (see compatibility below) + +Optional: +* RedCloth (to enable textile formatting) + +Supported databases: +* MySQL (tested with MySQL 5) +* PostgreSQL (tested with PostgreSQL 8.1) +* Oracle (tested with Oracle 10g) +* SQL Server (tested with SQL Server 2005) +* SQLite (tested with SQLite 3) + + +== Installation + +1. Uncompress program archive: + tar zxvf + +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 migrate RAILS_ENV="production" + It will create tables and an administrator account. + +5. Insert default configuration data in database: + rake load_default_data RAILS_ENV="production" + It will load default roles, trackers, statuses, workflows and enumerations. + This step is optional (but recommended), as you can define your + own configuration from sratch. + +6. 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 + +7. Use default administrator account to log in: + login: admin + password: admin + +8. Setup Apache or Lighttpd with fastcgi for best performance. + + +== Configuration + +A sample configuration file is provided: "config/config_custom.example.rb" +Rename it to config_custom.rb and set your parameters. +Don't forget to restart the application after any change. + +In config/environment.rb, you can set parameters for your SMTP server: +config.action_mailer.server_settings: SMTP server configuration +config.action_mailer.perform_deliveries: set to false to disable mail delivering + + +== Upgrading + +See UPGRADING diff --git a/issue_relations/doc/README b/issue_relations/doc/README new file mode 100644 index 000000000..b839c85bc --- /dev/null +++ b/issue_relations/doc/README @@ -0,0 +1,61 @@ +== redMine + +redMine - project management software +Copyright (C) 2006-2007 Jean-Philippe Lang +http://redmine.rubyforge.org/ + +== License + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +== Main features + +redMine is a project management software written using Ruby on Rails. + +* Multiple users/multiple projects +* Fully customizable role based access control +* Issue tracking system +* Fully customizable workflow +* Documents/files repository +* News management +* SVN browser and diff viewer +* Email notifications +* Custom fields for projects, users and issues +* Multiple LDAP authentication support +* User self-registration support +* Multilanguage support (english, french, german, spanish) +* Multiple databases support: MySQL, PostgreSQL, Oracle, SQL Server, SQLite + + +== User documentation + +User documentation for redMine is written using DocBook XML format. +It's also avaible as HTML files in /public/manual (contextual help) + + +== Versioning + +redMine versioning scheme is major.minor.revision +Versions before 1.0.0 must be considered as beta versions and upgrading support +may not be provided for these versions. + + +== Credits + +* Jean-Francois Boutier (spanish translation) +* Andreas Viklund (open source XHTML layout, http://andreasviklund.com/) +* Karim Trott (german translation and english online help translation) + diff --git a/issue_relations/doc/UPGRADING b/issue_relations/doc/UPGRADING new file mode 100644 index 000000000..cc4008a8e --- /dev/null +++ b/issue_relations/doc/UPGRADING @@ -0,0 +1,22 @@ +== redMine upgrade procedure + +redMine - project management software +Copyright (C) 2006-2007 Jean-Philippe Lang +http://redmine.rubyforge.org/ + +== From 0.3.0 + +1. Uncompress program archive in a new directory: + tar zxvf + +3. Copy your database (database.yml) and configuration settings (config_custom.rb) + into the new config directory + +4. Migrate your database: + rake migrate RAILS_ENV="production" + + +== From 0.2.x and previous + +Due to major database changes since 0.2.x, there is no migration support +from 0.2.x and previous versions. diff --git a/issue_relations/doc/docbook/en/images/arrow_bw.png b/issue_relations/doc/docbook/en/images/arrow_bw.png new file mode 100644 index 000000000..c50fbce8e Binary files /dev/null and b/issue_relations/doc/docbook/en/images/arrow_bw.png differ diff --git a/issue_relations/doc/docbook/en/images/arrow_from.png b/issue_relations/doc/docbook/en/images/arrow_from.png new file mode 100644 index 000000000..f1e1b6c31 Binary files /dev/null and b/issue_relations/doc/docbook/en/images/arrow_from.png differ diff --git a/issue_relations/doc/docbook/en/images/arrow_to.png b/issue_relations/doc/docbook/en/images/arrow_to.png new file mode 100644 index 000000000..faef20c78 Binary files /dev/null and b/issue_relations/doc/docbook/en/images/arrow_to.png differ diff --git a/issue_relations/doc/docbook/en/images/gantt.png b/issue_relations/doc/docbook/en/images/gantt.png new file mode 100644 index 000000000..65c33d685 Binary files /dev/null and b/issue_relations/doc/docbook/en/images/gantt.png differ diff --git a/issue_relations/doc/docbook/en/images/issues_list.png b/issue_relations/doc/docbook/en/images/issues_list.png new file mode 100644 index 000000000..520de14bf Binary files /dev/null and b/issue_relations/doc/docbook/en/images/issues_list.png differ diff --git a/issue_relations/doc/docbook/en/images/locked.png b/issue_relations/doc/docbook/en/images/locked.png new file mode 100644 index 000000000..5199dfe22 Binary files /dev/null and b/issue_relations/doc/docbook/en/images/locked.png differ diff --git a/issue_relations/doc/docbook/en/images/roles_edit.png b/issue_relations/doc/docbook/en/images/roles_edit.png new file mode 100644 index 000000000..a1ed2755b Binary files /dev/null and b/issue_relations/doc/docbook/en/images/roles_edit.png differ diff --git a/issue_relations/doc/docbook/en/images/user_new.png b/issue_relations/doc/docbook/en/images/user_new.png new file mode 100644 index 000000000..0c220257e Binary files /dev/null and b/issue_relations/doc/docbook/en/images/user_new.png differ diff --git a/issue_relations/doc/docbook/en/images/users_list.png b/issue_relations/doc/docbook/en/images/users_list.png new file mode 100644 index 000000000..74d9c9a5a Binary files /dev/null and b/issue_relations/doc/docbook/en/images/users_list.png differ diff --git a/issue_relations/doc/docbook/en/images/workflow.png b/issue_relations/doc/docbook/en/images/workflow.png new file mode 100644 index 000000000..04e79d61e Binary files /dev/null and b/issue_relations/doc/docbook/en/images/workflow.png differ diff --git a/issue_relations/doc/docbook/en/redmine-userdoc-en.xml b/issue_relations/doc/docbook/en/redmine-userdoc-en.xml new file mode 100644 index 000000000..6b7a1c853 --- /dev/null +++ b/issue_relations/doc/docbook/en/redmine-userdoc-en.xml @@ -0,0 +1,716 @@ + + + + Documentation + + + Administration + +
+ Projects + + These screens allow you to manage projects. +
+ +
+ Users + + These screens allow you to manage the application users. + +
+ Users’ List + + + + + Users’ List + + + + + + + + + Accounts status: + + + + This icon + + + + means that the account is locked. A user + having a locked account cannot log in and access the + application. + + + + This icon + + + + means that the user hasn't yet actived his + account. + + + + The Lock/Unlock buttons allow you to lock/unlock the user + accounts. + + +
+ +
+ User Creation or Modification + + In modification mode, please leave the Password field blank in + order to keep the user’s password unchanged. + + A user designated as administrator has unrestricted access to + the application and to all projects. + + + + Administrator : + designate the user as the administrator of the application. + + + + E-mail notifications : + activate or de-activate automatic e-mail notifications for this + user + + + + Locked : de-activates + the user’s account + + + + +
+
+ +
+ Roles and Permissions + + Roles organize the permissions of various members of a project. + Each member of a project has a one Role in a project. A user can have + different roles in different projects. + + On the new or edit Role screen, check off the actions authorized + for the Role. +
+ +
+ Trackers + + Trackers allow the sorting of Issues and can define specific + workflows. +
+ +
+ Custom fields + + Custom fields allow you to add additional information in Projects, + Issues or Users. A custom field can be of one the following + types: + + + + Integer : positive or + negative number + + + + String : a string of + characters - one single line of input. + + + + Text : a string of + characters with multiple lines of input. Differs from String Format + by providing multiple lines of input instead of a single + line. + + + + Date : date + + + + Boolean : true or false + (check if necessary) + + + + List : value to select + from a predefined list (aka: scroll list or select box) + + + + Validation elements can be defined: + + + + Required : A required + field must have input in the forms + + + + For all the projects : + field automatically associated to all of the projects + + + + Min - max length : + minimum and maximum length for the input fields (0 means that there + is no restriction) + + + + Regular Expression : + regular expressions may provide validation of the input value + + Examples: + + ^\[A-Z]{4}\d+$ : 4 capital letters followed by + one or several digits + + ^[^0-9]*$ : characters only - no digits + + + + Possible values : + possible values for the fields of "List" type. Values are separated + by the character | + + + + + +
+ Fields for Projects + + + + + + Required : required + field + + + + +
+ +
+ Fields for Issues + + + + + + For all projects : + field automatically associated to all project Issues + + If this option is not activated, each project could choose + whether or not to use the field for its Issues (please see the + project configuration). + + + + +
+ +
+ Field for Users + + + + + + Required : required + field + + + + +
+
+ +
+ Issue status + + These screens allow you to define the different possible Issue + statuses. + + + + Closed : indicates Issue + is considered as closed + + + + Default : status applied + by default to new Issue requests (only one status can be Default + status) + + + + Color : HTML color code + (6 characters) representing the displayed status + + + + +
+ +
+ Workflow + + The workflow allows to define changes the various project members + are allowed to make on the Issues, according to their type. + + Select the role and the tracker for which you want to modify the + workflow, then click Edit. The screen allows you then to modify the + authorized change, for the chosen role and tracker. The Current Status + options indicate the initial request status. The "New Statuses allowed" + columns stand for the authorized status to apply. + + Note: In order for a particular Role to change an Issue + status, the authorization must be given to it explicitly, regardless of + the workflow configuration. + + + + + Example of a workflow configuration + + + + + + + + + In the above example, Bug type Issue requests with a New status + could be given an Assigned or Resolved status by the Developer role. + Those with an Assigned status could get a Resolved status. The status of + all the other Bug type requests cannot be modified by the + Developer. +
+ +
+ Enumerations + + The value lists used by the application can be customized (for + example, setting Issue priorities). This screen allows you to define the + possible values for each of the following lists: + + + + Issue Priorities + + + + Document Categories + + + + +
+ +
+ E-mail notifications + + This screen allows you to select the actions that will generate an + e-mail notification for project members. + + Note: E-mail sending must be activated in the application + configuration if you want to make any notifications. +
+ +
+ Authentication + + By default, redMine refers to its own database to authenticate + users, by a specific password. + + If you already have one or several external user references (like + LDAP), you can make them known in order to be used for authentication on + redMine. This allows users to access redMine with their usual user names + and passwords. + + For each known reference, you can specify if the accounts can be + created on the fly on redMine. If needed, the user accounts will be + created automatically during the user’s signing in (without any specific + rights on the projects), according to information available in the + reference. Otherwise, the administrator must have previously created the + user account on redMine. + + + +
+ LDAP statement + + + + + + Name : reference + display name + + + + Host : LDAP server host + name + + + + Port : connection port + to the LDAP server + + + + Account : DN of the + connection account to LDAP (please leave it blank if the directory + authorizes anonymous read access) + + + + Password : password of + the connection account + + + + Base DN : Basic DN used + for user search in the directory + + + + LDAP screen : User + search screen in the directory (optional) + + + + LDAP features : + + + + Identifier : LDAP + feature name used as user identifier (e.g.: uid) + + + + First name : LDAP + feature name including the user’s first name (ex: + givenName) + + + + Last name : LDAP + feature name including the user’s last name (ex: + familyName) + + + + E-mail : LDAP + feature name including the user’s e-mail address (ex: + mail) + + + + + + The features" First name ", + " Last name " and " E-mail " are not used except when the + accounts are created on the fly. +
+
+ +
+ Information + + Displays application and environment information. +
+
+ + + Projects + +
+ Project overview + + The overview presents the general project information, its main + members, the latest announcements, as well as an synthesis of Issue + requests open by tracker. + + +
+ +
+ Planning + + + +
+ Project calendar + + Project calendar shows the tasks that begin or end during the + selected month (current month by default). An issue will be displayed + as a task if its start date and its due date are specified. + + + + This symbol + + + + represents a task that begins + + + + This symbol + + + + represents a task that ends + + + + Ths symbol + + + + represents a task that begins and ends the + same day + + + + +
+ +
+ Gantt chart + + This diagramme shows tasks and their achievement rate. + + Achievement is represented in blue. Delay in red. + + + + + Gantt chart + + + + + + + +
+
+ +
+ Issue management + + + +
+ Issue list + + By default, the entire list of the project open Issues are + displayed. Various screens allow you to select the Issues to be + displayed. If the project has sub-projects, you have the possibility + to display the sub-project's Issues as well (not displayed by + default). + + Once applied, a screen is valid during the entire session. You + can re-define it or delete it by clicking Cancel. + + + + + Request list + + + + + + + + + +
+
+ +
+ Reports + + This screen presents the number of Issues and Issue status + synthesis according to various criteria (tracker, priority, category). + Direct links allow for access to the detailed Issue list for each + criterion. +
+ +
+ Change log + + This page presents the entire list of the resolved Issues for each + version of the project. Certain types of Issues can be excluded from + this display. +
+ +
+ News + + Allows you to inform users on project activity. +
+ +
+ Documents + + Documents are grouped by categories (see Value Lists). A document + can contain several files (for example: revisions or successive + versions). +
+ +
+ Files + + This module allows you to display various folders (sources, + binaires, ...) for each version of the application. +
+ +
+ Settings + + + +
+ Project features + + + + + + Public : if it’s a + public project, it can be viewed (request consultation, documents + consultation, ...) for all the users, including those who are not + project members. If it’s not a public project, only the project + members have access to it, according to their role. + + + + Customized fields : + Select the customized fields that you want to use. Only the + administrator can define new customized fields. + + + + +
+ +
+ Members + + This screen allows you to define the project members as well as + their corresponding roles. A user can have only one role in a given + project. The role of a member determines the permissions they have in + a project. +
+ +
+ Versions + + Versions allow you to follow the changes made during all the + project. For instance, at the close of an Issue, you can indicate + which version takes it into account. You can display the various + versions of the application (see Files). +
+ +
+ Request categories + + Issue categories allow you to organize Issues. Categories can + correspond to different project modules. +
+
+
+ + + User accounts + + + +
+ My account + + + +
+ Information + + This screen allows you to modify your account: lastname, + firstname, email address, language. + + If Mail notifications is unchecked, no + email will be sent to you. +
+ +
+ Password + + To change your password, type your old password and your new + password (twice). Password length must be between 4 and 12 + characters. + + If your account uses an external authentication (LDAP), you + can't change your password in redMine. +
+
+ +
+ My page + + This page allows you to display various information about your + projects. + + To personalize your page, click on Personalize this + page. Then you can choose which information to display and + where it is displayed. +
+ +
+ Password lost + + If you loose your forget, a procedure allows you to choose a new + one. + + On the login screen, click on Lost password. + Type your email address and submit the form. An email is then sent to + you. It contains a link that allows you to change your password. + + If your account uses an external authentication (LDAP), this + procedure isn't be available. +
+ +
+ Register + + By registering, you can get an account without the intervention of + the administrator. + + On the login screen, click on Register. Fill + the form and submit it. An email will be sent to you. To activate your + account, use the link that is contained in this mail. + + The possibility to register can be desactived in the application + configuration. +
+
+
\ No newline at end of file diff --git a/issue_relations/doc/docbook/fr/images/arrow_bw.png b/issue_relations/doc/docbook/fr/images/arrow_bw.png new file mode 100644 index 000000000..c50fbce8e Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/arrow_bw.png differ diff --git a/issue_relations/doc/docbook/fr/images/arrow_from.png b/issue_relations/doc/docbook/fr/images/arrow_from.png new file mode 100644 index 000000000..f1e1b6c31 Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/arrow_from.png differ diff --git a/issue_relations/doc/docbook/fr/images/arrow_to.png b/issue_relations/doc/docbook/fr/images/arrow_to.png new file mode 100644 index 000000000..faef20c78 Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/arrow_to.png differ diff --git a/issue_relations/doc/docbook/fr/images/gantt.png b/issue_relations/doc/docbook/fr/images/gantt.png new file mode 100644 index 000000000..65c33d685 Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/gantt.png differ diff --git a/issue_relations/doc/docbook/fr/images/issues_list.png b/issue_relations/doc/docbook/fr/images/issues_list.png new file mode 100644 index 000000000..520de14bf Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/issues_list.png differ diff --git a/issue_relations/doc/docbook/fr/images/locked.png b/issue_relations/doc/docbook/fr/images/locked.png new file mode 100644 index 000000000..5199dfe22 Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/locked.png differ diff --git a/issue_relations/doc/docbook/fr/images/roles_edit.png b/issue_relations/doc/docbook/fr/images/roles_edit.png new file mode 100644 index 000000000..a1ed2755b Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/roles_edit.png differ diff --git a/issue_relations/doc/docbook/fr/images/user_new.png b/issue_relations/doc/docbook/fr/images/user_new.png new file mode 100644 index 000000000..0c220257e Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/user_new.png differ diff --git a/issue_relations/doc/docbook/fr/images/users_list.png b/issue_relations/doc/docbook/fr/images/users_list.png new file mode 100644 index 000000000..74d9c9a5a Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/users_list.png differ diff --git a/issue_relations/doc/docbook/fr/images/workflow.png b/issue_relations/doc/docbook/fr/images/workflow.png new file mode 100644 index 000000000..04e79d61e Binary files /dev/null and b/issue_relations/doc/docbook/fr/images/workflow.png differ diff --git a/issue_relations/doc/docbook/fr/redmine-userdoc-fr.xml b/issue_relations/doc/docbook/fr/redmine-userdoc-fr.xml new file mode 100644 index 000000000..fa67a6a24 --- /dev/null +++ b/issue_relations/doc/docbook/fr/redmine-userdoc-fr.xml @@ -0,0 +1,749 @@ + + + + Documentation + + + Administration + +
+ Projets + + Ces écrans vous permettent de gérer les projets. +
+ +
+ Utilisateurs + + Ces écrans vous permettent de gérer les utilisateurs de + l'application. + +
+ Liste des utilisateurs + + + + + Liste des utilisateurs + + + + + + + + + Statut des comptes: + + + + L'icône + + + + signifie que le compte est vérouillé. Un + utilisateur dont le compte est vérouillé ne peut plus s'identifier + pour accéder à l'application. + + + + L'icône + + + + signifie que le compte n'a pas encore été + activé par l'utilisateur . + + + + Les boutons Vérouiller/Dévérouiller vous permettent de bloquer + ou débloquer les comptes utilisateurs. + + +
+ +
+ Création ou modification d'un utilisateur + + En mode modification, laissez le champ Mot de + passe vide pour laisser le mot de passe de l'utilisateur + inchangé. + + Un utilisateur déclaré comme administrateur dispose de toutes + les permissions sur l'application et sur tous les projets. + + + + Administrateur: déclare l'utilisateur + comme administrateur de l'application. + + + + Notifications par mail: permet + d'activer ou non l'envoi automatique de notifications par mail + pour cet utilisateur + + + + Vérouillé: désactive le compte de + l'utilisateur + + + + +
+
+ +
+ Rôles et permissions + + Les rôles permettent de définir les permissions des différents + membres d'un projet. Chaque membre d'un projet dispose d'un rôle unique + au sein d'un projet. Un utilisateur peut avoir différents rôles au sein + de différents projets. + + Sur l'écran d'édition du rôle, cochez les actions que vous + souhaitez autoriser pour le rôle. + + + + + Rôle et permissions + + + + + + + + + +
+ +
+ Trackers + + Les trackers permettent de typer les demandes et de définir des + workflows spécifiques pour chacun de ces types. +
+ +
+ Champs personnalisés + + Les champs personnalisés vous permettent d'ajouter des + informations supplémentaires sur les projets, les demandes ou les + utilisateurs. Un champ personnalisé peut être de l'un des types + suivants: + + + + Entier: entier positif ou négatif + + + + Texte: chaîne de caractères + + + + Texte long: chaîne de caractères, avec + champ à plusieurs lignes + + + + Date: date + + + + Booléen: booléen (case à cocher) + + + + Liste: valeur à sélectionnée parmi une + liste prédéfinie (liste déroulante) + + + + Des éléments de validation peuvent être définis: + + + + Obligatoire: champ dont la saisie est + obligatoire sur les demandes + + + + Pour tous les projects: champ + automatiquement associé à l'ensemble des projets + + + + Min - max length: longueurs minimales et + maximales pour les champs en saisie libre (0 signifie qu'il n'y a + pas de restriction) + + + + Expression régulière: expression + régulière permettant de valider la valeur saisie + + Exemples: + + ^\[A-Z]{4}\d+$ : 4 lettres majuscules suivies + d'un ou plusieurs chiffres + + ^[^0-9]*$ : chaîne ne comportant pas de + chiffres + + + + Valeurs possibles: valeurs possibles pour + les champs de type "Liste". Les valeurs sont séparées par le + caractère | + + + + + +
+ Champs pour les projets + + + + + + Obligatoire: champ dont la saisie est + obligatoire + + + + +
+ +
+ Champs pour les demandes + + + + + + Pour tous les projects: champ + automatiquement associé aux demandes de l'ensemble des + projets + + Si cette option n'est pas activée, chaque projet pourra + choisir d'utiliser ou non le champ pour ses demandes (voir + configuration du projet). + + + + +
+ +
+ Champs pour les utilisateurs + + + + + + Obligatoire: champ dont la saisie est + obligatoire + + + + +
+
+ +
+ Statut des demandes + + Ces écrans vous permettent de définir les différents statuts + possibles des demandes. + + + + Demande fermée: indique que le statut + correspond à une demande considérée comme fermée + + + + Statut par défaut: statut appliqué par + défaut aux nouvelles demandes (seul un statut peut être déclaré + comme statut par défaut) + + + + Couleur: code couleur HTML (6 caractères) + représentant le statut à l'affichage + + + + +
+ +
+ Workflow + + Le workflow permet de définir les changements que les différents + membres d'un projet sont autorisés à effectuer sur les demandes, en + fonction de leur type. + + Sélectionnez le rôle et le tracker pour lesquels vous souhaitez + modifier le workflow, puis cliquez sur Edit. L'écran vous permet alors + de modifier, pour le rôle et le tracker choisi, les changements + autorisés. Les lignes représentent les statuts initiaux des demandes. + Les colonnes représentent les statuts autorisés à être appliqués. + + Remarque: pour qu'un rôle puisse changer le statut des + demandes, la permission doit lui être explicitement donnée + indépendemment de la configuration du workflow. + + + + + Exemple de configuration d'un workflow + + + + + + + + + Dans l'exemple ci-dessus, les demandes de type Bug au statut + Nouveau pourront être passées au statut Assignée ou Résolue par le rôle + Développeur. Celles au statut Assignée pourront être passées au statut + Résolue. Le statut de toutes les autres demandes de type Bug ne pourra + pas être modifié par le Développeur. +
+ +
+ Listes de valeurs + + Les listes de valeurs utilisées par l'application (exemple: les + priorités des demandes) peuvent être personnalisées. Cet écran vous + permet de définir les valeurs possibles pour chacune des listes + suivantes: + + + + Priorités des demandes + + + + Catégories de documents + + + + +
+ +
+ Notifications par mail + + Cet écran vous permet de sélectionner les actions qui donneront + lieu à une notification par mail aux membres du projet. + + Remarque: l'envoi de mails doit être activé dans la configuration + de l'application si souhaitez effectuer des notifications. +
+ +
+ Authentification + + Par défaut, redMine s'appuie sur sa propre base de données pour + authentifier les utilisateurs, à l'aide d'un mot de passe + spécifique. + + Si vous disposez déjà d'un ou plusieurs référentiels externes + d'utilisateurs (annuaires LDAP), vous pouvez les déclarer afin qu'ils + soient utilisés pour l'authentification sur redMine. Cela permet aux + utilisateurs d'accéder à redMine avec leurs identifiants et mots de + passe habituels. + + Pour chaque référentiel déclaré, vous pouvez spécifier si les + comptes peuvent être créés à la volée dans redMine. Si c'est le cas, les + comptes utilisateurs sont automatiquement créés à la première connexion + de l'utilisateur (sans droits spécifiques sur les projets), à partir des + informations disponibles dans le référentiel. Sinon, l'administrateur + doit au préalable créer le compte de l'utilisateur dans redMine. + + + +
+ Annuaire LDAP + + + + + + Nom: nom d'affichage du + référentiel + + + + Hôte: nom d'hôte du serveur LDAP + + + + Port: port de connexion au serveur + LDAP + + + + Compte: DN du compte de connexion au + LDAP (laisser vide si l'annuaire autorise l'accès anonyme en + lecture) + + + + Mot de passe: mot de passe du compte de + connexion + + + + Base DN: DN de base utilisé pour la + recherche des utilisateur dans l'annuaire + + + + Filtre LDAP: Filtre de recherche des + utilisateurs dans l'annuaire (optionnel) + + + + Attributs LDAP: + + + + Identifiant: nom de l'attribut LDAP + utilisé comme identifiant de l'utilisateur (ex: uid) + + + + Prénom: nom de l'attribut LDAP + contenant le prénom de l'utilisateur (ex: givenName) + + + + Nom: nom de l'attribut LDAP + contenant le nom de l'utilisateur (ex: sn) + + + + Email: nom de l'attribut LDAP + contenant l'adresse mail de l'utilisateur (ex: mail) + + + + + + Les attributs "Prénom", + "Nom" et "Email" ne sont + utilisés que lorsque les comptes sont créés à la volée. +
+
+ +
+ Informations + + Affiche des informations relatives à l'application et à son + environnement. +
+
+ + + Projets + +
+ Aperçu du projet + + L'aperçu vous présente les informations générales relatives au + projet, les principaux membres, les dernières annonces, ainsi qu'une + synthèse du nombre de demandes ouvertes par tracker. + + +
+ +
+ Planning + + + +
+ Calendrier du projet + + Le calendrier présente les tâches qui commencent ou se terminent + au cours du mois sélectionné (mois en cours par défaut). Les tâches + correspondent aux demandes pour lesquelles la date de début et + d'échéance sont renseignées. + + + + Le symoble + + + + représente le début d'une tâche + + + + Le symbole + + + + représente la fin d'une tâche + + + + Le symbole + + + + représente une tâche qui débute et se + termine le jour même + + + + +
+ +
+ Diagramme de Gantt + + Le diagramme de Gantt représente pour la période choisie + l'ensemble des tâches et leurs taux d'avancement. + + L'avancement est représenté en bleu. Le retard en rouge. + + + Diagramme de Gantt + + + + + + + + + +
+
+ +
+ Gestion des demandes + + + +
+ Liste des demandes + + Par défaut, l'ensemble des demandes ouvertes du projet sont + affichées. Différents filtres vous permettent de sélectionner les + demandes à afficher. + + Une fois appliqué, un filtre reste valable durant toute votre + session. Vous pouvez le redéfinir, ou le supprimer en cliquant sur + Effacer. + + + + + Liste des demandes + + + + + + + + + +
+
+ +
+ Rapports + + Cet écran présente la synthèse du nombre de demandes par statut et + selon différents critères (tracker, priorité, catégorie). Des liens + directs permettent d'accéder à la liste détaillée des demandes pour + chaque critère. +
+ +
+ Historique + + Cette page présente l'ensemble des demandes résolues dans chacune + des versions du projet. Certains types de demande peuvent être exclus de + cet affichage. +
+ +
+ Annonces + + Les nouvelles vous permettent d'informer les utilisateurs sur + l'activité du projet. +
+ +
+ Documents + + Les documents sont groupés par catégories (voir Listes de + valeurs). Un document peut contenir plusieurs fichiers (exemple: + révisions ou versions successives). +
+ +
+ Fichiers + + Ce module vous permet de publier les différents fichiers (sources, + binaires, ...) pour chaque version de l'application. +
+ +
+ Configuration du projet + + + +
+ Propriétés du projet + + + + + + Public: si le projet est public, il + sera visible (consultation des demandes, des documents, ...) pour + l'ensemble des utilisateurs, y compris ceux qui ne sont pas + membres du projet. Si le projet n'est pas public, seuls les + membres du projet y ont accès, en fonction de leur rôle. + + + + Champs personnalisés: sélectionner les + champs personnalisésMo que vous souhaitez utiliser pour les + demandes du projet. Seul l'administrateur peut définir de nouveaux + champs personnalisés. + + + + +
+ +
+ Membres + + Cett section vous permet de définir les membres du projet ainsi + que leurs rôles respectifs. Un utilisateur ne peut avoir qu'un rôle au + sein d'un projet donné. Le rôle d'un membre détermine les permissions + dont il bénéficie sur le projet. +
+ +
+ Versions + + Les versions vous permettent de suivre les changements survenus + tout au long du projet. A la fermeture d'une demande, vous pouvez par + exemple indiquer quelle version la prend en compte. Vous pouvez par + ailleurs publier les différentes versions de l'application (voir + Fichiers). +
+ +
+ Catégories des demandes + + Les catégories de demande vous permettent de typer les demandes. + Les catégories peuvent par exemple correspondre aux différents modules + du projet. + + Une catégorie référencée sur des demandes ne peut pas être + supprimée. + + +
+
+
+ + + Comptes utilisateurs + + + +
+ Mon compte + + + +
+ Informations + + Cet écran vous permet de modifier les informations relatives à + votre compte: nom, prénom, adresse mail, langue (Anglais, Allemand, + Espagnol ou Français). + + Si la case Notifications par mail est + décochée, aucune notification par mail ne vous sera envoyée. +
+ +
+ Changement de mot de passe + + Pour changer votre mot de passe, saisissez votre mot de passe + actuel et le nouveau mot de passe souhaité (double saisie pour + confirmation). Le mot de passe doit avoir une longueur comprise entre + 4 et 12 caractères. + + Si votre compte utilise une authentification externe (un + annuaire LDAP), vous ne pouvez pas changer votre mot de passe dans + redMine. +
+
+ +
+ Ma page + + Cette page vous permet d'afficher de manière synthétique des + informations sur vos projets. + + Pour personnaliser votre page, cliquer sur le lien + Personnaliser cette page. Vous pouvez alors + sélectionner les informations à afficher et les positionner où vous le + souhaitez sur la page, par glisser-déposer. +
+ +
+ Mot de passe perdu + + En cas de perte de votre mot de passe, une procédure par mail vous + permet d'en choisir un nouveau. + + Sur l'écran d'authentification, cliquez sur Mot de passe + perdu. Saisissez ensuite votre adresse mail dans le champ + prévu et validez. Un mail vous est alors transmis. Il contient un lien + qui vous permettra d'accéder à la page de choix du nouveau mot de + passe. + + Si votre compte utilise une authentification externe (un annuaire + LDAP), cette procédure n'est pas disponible. +
+ +
+ S'enregistrer + + L'enregistrement vous permet d'obtenir un compte sans intervention + de l'administrateur. + + Sur l'écran d'authentification, cliquez sur + S'enregistrer. Complétez le formulaire + d'inscription et validez. Un mail vous est alors transmis. Pour activer + votre compte, utilisez le lien présent dans le mail qui vous a été + envoyé. + + La possibilité de s'enregistrer peut avoir été désactivée par + l'administrateur. +
+
+
\ No newline at end of file diff --git a/issue_relations/files/delete.me b/issue_relations/files/delete.me new file mode 100644 index 000000000..18beddaa8 --- /dev/null +++ b/issue_relations/files/delete.me @@ -0,0 +1 @@ +default directory for uploaded files \ No newline at end of file diff --git a/issue_relations/lang/de.yml b/issue_relations/lang/de.yml new file mode 100644 index 000000000..d6a759593 --- /dev/null +++ b/issue_relations/lang/de.yml @@ -0,0 +1,359 @@ +_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: Bitte auserwählt + +activerecord_error_inclusion: ist nicht in der Liste eingeschlossen +activerecord_error_exclusion: ist reserviert +activerecord_error_invalid: ist unzulässig +activerecord_error_confirmation: bringt nicht Bestätigung zusammen +activerecord_error_accepted: muß angenommen werden +activerecord_error_empty: kann nicht leer sein +activerecord_error_blank: kann nicht leer sein +activerecord_error_too_long: ist zu lang +activerecord_error_too_short: ist zu kurz +activerecord_error_wrong_length: ist die falsche Länge +activerecord_error_taken: ist bereits genommen worden +activerecord_error_not_a_number: ist nicht eine Zahl +activerecord_error_not_a_date: ist nicht ein gültiges Datum +activerecord_error_greater_than_start_date: muß als grösser sein beginnen Datum + +general_fmt_age: %d yr +general_fmt_age_plural: %d yrs +general_fmt_date: %%b %%d, %%Y (%%a) +general_fmt_datetime: %%b %%d, %%Y (%%a), %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'Nein' +general_text_Yes: 'Ja' +general_text_no: 'nein' +general_text_yes: 'ja' +general_lang_de: 'Deutsch' +general_csv_separator: ';' +general_day_names: Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag,Sonntag + +notice_account_updated: Konto wurde erfolgreich aktualisiert. +notice_account_invalid_creditentials: Unzulässiger Benutzer oder Passwort +notice_account_password_updated: Passwort wurde erfolgreich aktualisiert. +notice_account_wrong_password: Falsches Passwort +notice_account_register_done: Konto wurde erfolgreich verursacht. +notice_account_unknown_email: Unbekannter Benutzer. +notice_can_t_change_password: Dieses Konto verwendet eine externe Authentisierung Quelle. Unmöglich, das Kennwort zu ändern. +notice_account_lost_email_sent: Ein email mit Anweisungen, ein neues Kennwort zu wählen ist dir geschickt worden. +notice_account_activated: Dein Konto ist aktiviert worden. Du kannst jetzt einloggen. +notice_successful_create: Erfolgreiche Kreation. +notice_successful_update: Erfolgreiches Update. +notice_successful_delete: Erfolgreiche Auslassung. +notice_successful_connection: Erfolgreicher Anschluß. +notice_file_not_found: Erbetene Akte besteht nicht oder ist gelöscht worden. +notice_locking_conflict: Data have been updated by another user. +notice_scm_error: Eintragung und/oder Neuausgabe besteht nicht im Behälter. + +mail_subject_lost_password: Dein redMine Kennwort +mail_subject_register: redMine Kontoaktivierung + +gui_validation_error: 1 Störung +gui_validation_error_plural: %d Störungen + +field_name: Name +field_description: Beschreibung +field_summary: Zusammenfassung +field_is_required: Erforderlich +field_firstname: Vorname +field_lastname: Nachname +field_mail: Email +field_filename: Datei +field_filesize: Grootte +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: Títel +field_project: Projekt +field_issue: Antrag +field_status: Status +field_notes: Anmerkungen +field_is_closed: Problem erledigt +field_is_default: Rückstellung status +field_html_color: Farbe +field_tracker: Tracker +field_subject: Thema +field_due_date: Abgabedatum +field_assigned_to: Zugewiesen an +field_priority: Priorität +field_fixed_version: Erledigt in Version +field_user: Benutzer +field_role: Rolle +field_homepage: Startseite +field_is_public: Öffentlich +field_parent: Subprojekt von +field_is_in_chlog: Ansicht der Issues in der Historie +field_login: Mitgliedsname +field_mail_notification: Mailbenachrichtigung +field_admin: Administrator +field_locked: Gesperrt +field_last_login_on: Letzte Anmeldung +field_language: Sprache +field_effective_date: Datum +field_password: Passwort +field_new_password: Neues Passwort +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: Mitgliedsnameattribut +field_attr_firstname: Vornamensattribut +field_attr_lastname: Namenattribut +field_attr_mail: Emailattribut +field_onthefly: On-the-fly Benutzerkreation +field_start_date: Beginn +field_done_ratio: %% Getan +field_hide_mail: Mein email address verstecken +field_comment: Anmerkung +field_url: URL + +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_latest: Neueste Projekte +label_issue: Antrag +label_issue_new: Neue Antrag +label_issue_plural: Anträge +label_issue_view_all: Alle Anträge ansehen +label_document: Dokument +label_document_new: Neues Dokument +label_document_plural: Dokumente +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: Antrag Status +label_issue_status_plural: Antrag Stati +label_issue_status_new: Neuer Status +label_issue_category: Antrag Kategorie +label_issue_category_plural: Antrag 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: Enumerationen +label_enumeration_new: Neuer Wert +label_information: Information +label_information_plural: Informationen +label_please_login: Anmelden +label_register: Anmelden +label_password_lost: Passwort vergessen +label_home: Hauptseite +label_my_page: Meine Seite +label_my_account: Mein Konto +label_my_projects: Meine Projekte +label_administration: Administration +label_login: Einloggen +label_logout: Abmelden +label_help: Hilfe +label_reported_issues: Gemeldete Issues +label_assigned_to_me_issues: Mir zugewiesen +label_last_login: Letzte Anmeldung +label_last_updates: Letztes aktualisiertes +label_last_updates_plural: %d Letztes aktualisiertes +label_registered_on: Angemeldet am +label_activity: Aktivität +label_new: Neue +label_logged_as: Angemeldet als +label_environment: Environment +label_authentication: Authentisierung +label_auth_source: Authentisierung Modus +label_auth_source_new: Neuer Authentisierung Modus +label_auth_source_plural: Authentisierung Modi +label_subproject: Vorprojekt von +label_subproject_plural: Vorprojekte +label_min_max_length: Min - Max Länge +label_list: Liste +label_date: Date +label_integer: Zahl +label_boolean: Boolesch +label_string: Text +label_text: Langer Text +label_attribute: Attribut +label_attribute_plural: Attribute +label_download: %d Herunterlade +label_download_plural: %d Herunterlade +label_no_data: Nichts anzuzeigen +label_change_status: Statuswechsel +label_history: Historie +label_attachment: Datei +label_attachment_new: Neue Datei +label_attachment_delete: Löschungakten +label_attachment_plural: Dateien +label_report: Bericht +label_report_plural: Berichte +label_news: Neuigkeit +label_news_new: Neuigkeite addieren +label_news_plural: Neuigkeiten +label_news_latest: Letzte Neuigkeiten +label_news_view_all: Alle Neuigkeiten anzeigen +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: Export zu +label_read: Lesen... +label_public_projects: Öffentliche Projekte +label_open_issues: geöffnet +label_open_issues_plural: geöffnet +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 Status gewährten +label_all: alle +label_none: kein +label_next: Weiter +label_previous: Zurück +label_used_by: Benutzt von +label_details: Details... +label_add_note: Eine Anmerkung addieren +label_per_page: Pro Seite +label_calendar: Kalender +label_months_from: Monate von +label_gantt: Gantt +label_internal: Intern +label_last_changes: %d änderungen des Letzten +label_change_view_all: Alle änderungen ansehen +label_personalize_page: Diese Seite personifizieren +label_comment: Anmerkung +label_comment_plural: Anmerkungen +label_comment_add: Anmerkung addieren +label_comment_added: Anmerkung fügte hinzu +label_comment_delete: Anmerkungen löschen +label_query: Benutzerdefiniertes Frage +label_query_plural: Benutzerdefinierte Fragen +label_query_new: Neue Frage +label_filter_add: Filter addieren +label_filter_plural: Filter +label_equals: ist +label_not_equals: ist nicht +label_in_less_than: an weniger als +label_in_more_than: an mehr als +label_in: an +label_today: heute +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: SVN Behälter +label_browse: Grasen +label_modification: %d änderung +label_modification_plural: %d änderungen +label_revision: Neuausgabe +label_revision_plural: Neuausgaben +label_added: hinzugefügt +label_modified: geändert +label_deleted: gelöscht +label_latest_revision: Neueste Neuausgabe +label_view_revisions: Die Neuausgaben ansehen +label_max_size: Maximale Größe +label_on: auf + +button_login: Einloggen +button_submit: Einreichen +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: Verriegeln +button_unlock: Entriegeln +button_download: Fernzuladen +button_list: Aufzulisten +button_view: Siehe +button_move: Bewegen +button_back: Rückkehr +button_cancel: Annullieren +button_activate: Aktivieren + +text_select_mail_notifications: Aktionen für die Mailbenachrichtigung aktiviert werden soll. +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 heisst keine Beschränkung +text_possible_values_info: Werte trennten sich mit | +text_project_destroy_confirmation: Sind sie sicher, daß sie das Projekt löschen wollen ? +text_workflow_edit: Auswahl Workflow zum Bearbeiten +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 beendet +text_tip_task_begin_end_day: Aufgabe, die an diesem Tag beginnt und beendet + +default_role_manager: Manager +default_role_developper: Developer +default_role_reporter: Reporter +default_tracker_bug: Fehler +default_tracker_feature: Feature +default_tracker_support: Support +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 + +enumeration_issue_priorities: Issue-Prioritäten +enumeration_doc_categories: Dokumentenkategorien diff --git a/issue_relations/lang/en.yml b/issue_relations/lang/en.yml new file mode 100644 index 000000000..6ef3bfe5e --- /dev/null +++ b/issue_relations/lang/en.yml @@ -0,0 +1,359 @@ +_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 + +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_en: 'English' +general_csv_separator: ',' +general_day_names: Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday + +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. +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 have been updated by another user. +notice_scm_error: Entry and/or revision doesn't exist in the repository. + +mail_subject_lost_password: Your redMine password +mail_subject_register: redMine account activation + +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 status +field_html_color: Color +field_tracker: Tracker +field_subject: Subject +field_due_date: Due date +field_assigned_to: Assigned to +field_priority: Priority +field_fixed_version: Fixed 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_login: Login +field_mail_notification: Mail notifications +field_admin: Administrator +field_locked: Locked +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_hide_mail: Hide my email address +field_comment: Comment +field_url: URL + +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_latest: Latest projects +label_issue: Issue +label_issue_new: New issue +label_issue_plural: Issues +label_issue_view_all: View all issues +label_document: Document +label_document_new: New document +label_document_plural: Documents +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 login +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: Login +label_logout: Logout +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_new: New +label_logged_as: Logged 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: Subproject +label_subproject_plural: Subprojects +label_min_max_length: Min - Max length +label_list: List +label_date: Date +label_integer: Integer +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_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_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: Export to +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_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_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: SVN 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_view_revisions: View revisions +label_max_size: Maximum size +label_on: 'on' + +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 + +text_select_mail_notifications: Select actions for which mail notifications should be sent. +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 means no restriction +text_possible_values_info: values separated with | +text_project_destroy_confirmation: Are you sure you want to delete this project and all related data ? +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 + +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 + +enumeration_issue_priorities: Issue priorities +enumeration_doc_categories: Document categories diff --git a/issue_relations/lang/es.yml b/issue_relations/lang/es.yml new file mode 100644 index 000000000..596cefc07 --- /dev/null +++ b/issue_relations/lang/es.yml @@ -0,0 +1,359 @@ +_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 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: no es una fecha válida +activerecord_error_greater_than_start_date: debe ser la fecha mayor que del comienzo + +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_es: 'Español' +general_csv_separator: ';' +general_day_names: Lunes,Martes,Miércoles,Jueves,Viernes,Sábado,Domingo + +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. +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: La página que intentabas tener acceso no existe ni se ha quitado. +notice_locking_conflict: Data have been updated by another user. +notice_scm_error: La entrada y/o la revisión no existe en el depósito. + +mail_subject_lost_password: Tu contraseña del redMine +mail_subject_register: Activación de la cuenta del redMine + +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: Email +field_filename: Fichero +field_filesize: Tamaño +field_downloads: Telecargas +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: Estatuto +field_notes: Notas +field_is_closed: Petición resuelta +field_is_default: Estatuto por defecto +field_html_color: Color +field_tracker: Tracker +field_subject: Tema +field_due_date: Fecha debida +field_assigned_to: Asignado a +field_priority: Prioridad +field_fixed_version: Versión corregida +field_user: Usuario +field_role: Papel +field_homepage: Sitio web +field_is_public: Público +field_parent: Proyecto secundario de +field_is_in_chlog: Consultar las peticiones en el histórico +field_login: Identificador +field_mail_notification: Notificación por mail +field_admin: Administrador +field_locked: Cerrado +field_last_login_on: Última conexión +field_language: Lengua +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: Base DN +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 On-the-fly +field_start_date: Comienzo +field_done_ratio: %% Realizado +field_hide_mail: Ocultar mi email address +field_comment: Comentario +field_url: URL + +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_latest: Los proyectos más últimos +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: Papel +label_role_plural: Papeles +label_role_new: Nuevo papel +label_role_and_permissions: Papeles 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: Workflow +label_issue_status: Estatuto de petición +label_issue_status_plural: Estatutos de las peticiones +label_issue_status_new: Nuevo estatuto +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: Informacion +label_information_plural: Informaciones +label_please_login: Conexión +label_register: Registrar +label_password_lost: ¿Olvidaste la contraseña? +label_home: Acogida +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 +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: Environment +label_authentication: Autentificación +label_auth_source: Modo de la autentificación +label_auth_source_new: Nuevo modo de la autentificación +label_auth_source_plural: Modos de la autentificación +label_subproject: Proyecto secundario +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 Telecarga +label_download_plural: %d Telecargas +label_no_data: Ningunos datos a exhibir +label_change_status: Cambiar el estatuto +label_history: Histórico +label_attachment: Fichero +label_attachment_new: Nuevo fichero +label_attachment_delete: Suprimir 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: Versiónes +label_confirmation: Confirmación +label_export_to: Exportar a +label_read: Leer... +label_public_projects: Proyectos publicos +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 estatutos autorizados +label_all: todos +label_none: ninguno +label_next: Próximo +label_previous: Precedente +label_used_by: Utilizado por +label_details: Detalles... +label_add_note: Agregar 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: Agregar un comentario +label_comment_added: Comentario agregó +label_comment_delete: Suprimir comentarios +label_query: Pregunta personalizada +label_query_plural: Preguntas personalizadas +label_query_new: Nueva preguntas +label_filter_add: Agregar 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: Depósito SVN +label_browse: Hojear +label_modification: %d modificación +label_modification_plural: %d modificaciones +label_revision: Revisión +label_revision_plural: Revisiones +label_added: agregado +label_modified: modificado +label_deleted: suprimido +label_latest_revision: La revisión más última +label_view_revisions: Ver las revisiones +label_max_size: Tamaño máximo +label_on: en + +button_login: Conexión +button_submit: Someter +button_save: Validar +button_check_all: Seleccionar todo +button_uncheck_all: No seleccionar nada +button_delete: Suprimir +button_create: Crear +button_test: Testar +button_edit: Modificar +button_add: Añadir +button_change: Cambiar +button_apply: Aplicar +button_clear: Anular +button_lock: Bloquear +button_unlock: Desbloquear +button_download: Telecargar +button_list: Listar +button_view: Ver +button_move: Mover +button_back: Atrás +button_cancel: Cancelar +button_activate: Activar + +text_select_mail_notifications: Seleccionar las actividades que necesitan la activación de la notificación por mail. +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 para ninguna restricción +text_possible_values_info: Los valores se separaron con | +text_project_destroy_confirmation: ¿ Estás seguro de querer eliminar el proyecto ? +text_workflow_edit: Seleccionar un workflow 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 + +default_role_manager: Manager +default_role_developper: Desarrollador +default_role_reporter: Informador +default_tracker_bug: Anomalía +default_tracker_feature: Evolución +default_tracker_support: Asistencia +default_issue_status_new: Nuevo +default_issue_status_assigned: Asignada +default_issue_status_resolved: Resuelta +default_issue_status_feedback: Comentario +default_issue_status_closed: Cerrada +default_issue_status_rejected: Rechazada +default_doc_category_user: Documentación del usuario +default_doc_category_tech: Documentación tecnica +default_priority_low: Bajo +default_priority_normal: Normal +default_priority_high: Alto +default_priority_urgent: Urgente +default_priority_immediate: Ahora + +enumeration_issue_priorities: Prioridad de las peticiones +enumeration_doc_categories: Categorías del documento diff --git a/issue_relations/lang/fr.yml b/issue_relations/lang/fr.yml new file mode 100644 index 000000000..9f7bfa45e --- /dev/null +++ b/issue_relations/lang/fr.yml @@ -0,0 +1,364 @@ +_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: 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: 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 + +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_fr: 'Français' +general_csv_separator: ';' +general_day_names: Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche + +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_scm_error: L'entrée et/ou la révision demandée n'existe pas dans le dépôt. + +mail_subject_lost_password: Votre mot de passe redMine +mail_subject_register: Activation de votre compte redMine + +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: Statut par défaut +field_html_color: Couleur +field_tracker: Tracker +field_subject: Sujet +field_due_date: Date d'échéance +field_assigned_to: Assigné à +field_priority: Priorité +field_fixed_version: Version corrigée +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_login: Identifiant +field_mail_notification: Notifications par mail +field_admin: Administrateur +field_locked: Verrouillé +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_comment: Commentaire +field_url: URL + +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_latest: Derniers projets +label_issue: Demande +label_issue_new: Nouvelle demande +label_issue_plural: Demandes +label_issue_view_all: Voir toutes les demandes +label_document: Document +label_document_new: Nouveau document +label_document_plural: Documents +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: Sous-projet +label_subproject_plural: Sous-projets +label_min_max_length: Longueurs mini - maxi +label_list: Liste +label_date: Date +label_integer: Entier +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_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_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: Exporter en +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_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_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 SVN +label_browse: Parcourir +label_modification: %d modification +label_modification_plural: %d modifications +label_revision: Révision +label_revision_plural: Révisions +label_added: ajouté +label_modified: modifié +label_deleted: supprimé +label_latest_revision: Dernière révision +label_view_revisions: Voir les révisions +label_max_size: Taille maximale +label_on: sur +label_rel_end_to_start: Fin à début +label_rel_end_to_end: Fin à fin +label_rel_start_to_start: Début à début +label_rel_start_to_end: Début à fin + +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 + +text_select_mail_notifications: Sélectionner les actions pour lesquelles la notification par mail doit être activée. +text_regexp_info: ex. ^[A-Z0-9]+$ +text_min_max_length_info: 0 pour aucune restriction +text_possible_values_info: valeurs séparées par | +text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et tout ce qui lui est rattaché ? +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 + +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 + +enumeration_issue_priorities: Priorités des demandes +enumeration_doc_categories: Catégories des documents diff --git a/issue_relations/lib/tasks/extract_fixtures.rake b/issue_relations/lib/tasks/extract_fixtures.rake new file mode 100644 index 000000000..49834e5ab --- /dev/null +++ b/issue_relations/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/issue_relations/lib/tasks/load_default_data.rake b/issue_relations/lib/tasks/load_default_data.rake new file mode 100644 index 000000000..f5a160545 --- /dev/null +++ b/issue_relations/lib/tasks/load_default_data.rake @@ -0,0 +1,101 @@ +desc 'Load default configuration data' + +task :load_default_data => :environment do + include GLoc + set_language_if_valid($RDM_DEFAULT_LANG) + 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 + # check that no data already exists + if Role.find(:first) + raise "Some roles are already defined." + end + if Tracker.find(:first) + raise "Some trackers are already defined." + end + if IssueStatus.find(:first) + raise "Some statuses are already defined." + end + if Enumeration.find(:first) + raise "Some enumerations are already defined." + end + + puts "Loading default configuration data for language: #{current_language}" + + # roles + manager = Role.create :name => l(:default_role_manager) + manager.permissions = Permission.find(:all, :conditions => ["is_public=?", false]) + + developper = Role.create :name => l(:default_role_developper) + perms = [150, 320, 321, 322, 420, 421, 422, 1050, 1060, 1070, 1075, 1130, 1220, 1221, 1222, 1223, 1224, 1320, 1322, 1061, 1057] + developper.permissions = Permission.find(:all, :conditions => ["sort IN (#{perms.join(',')})"]) + + reporter = Role.create :name => l(:default_role_reporter) + perms = [1050, 1060, 1070, 1057, 1130] + reporter.permissions = Permission.find(:all, :conditions => ["sort IN (#{perms.join(',')})"]) + + # trackers + Tracker.create(:name => l(:default_tracker_bug), :is_in_chlog => true) + Tracker.create(:name => l(:default_tracker_feature), :is_in_chlog => true) + Tracker.create(:name => l(:default_tracker_support), :is_in_chlog => false) + + # issue statuses + new = IssueStatus.create(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :html_color => 'F98787') + assigned = IssueStatus.create(:name => l(:default_issue_status_assigned), :is_closed => false, :is_default => false, :html_color => 'C0C0FF') + resolved = IssueStatus.create(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :html_color => '88E0B3') + feedback = IssueStatus.create(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :html_color => 'F3A4F4') + closed = IssueStatus.create(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :html_color => 'DBDBDB') + rejected = IssueStatus.create(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :html_color => 'F5C28B') + + # 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)) + Enumeration.create(:opt => "DCAT", :name => l(:default_doc_category_tech)) + Enumeration.create(:opt => "IPRI", :name => l(:default_priority_low)) + Enumeration.create(:opt => "IPRI", :name => l(:default_priority_normal)) + Enumeration.create(:opt => "IPRI", :name => l(:default_priority_high)) + Enumeration.create(:opt => "IPRI", :name => l(:default_priority_urgent)) + Enumeration.create(:opt => "IPRI", :name => l(:default_priority_immediate)) + +rescue => error + puts "Error: " + error + puts "Default configuration data can't be loaded." +end +end \ No newline at end of file diff --git a/issue_relations/log/delete.me b/issue_relations/log/delete.me new file mode 100644 index 000000000..18beddaa8 --- /dev/null +++ b/issue_relations/log/delete.me @@ -0,0 +1 @@ +default directory for uploaded files \ No newline at end of file diff --git a/issue_relations/public/.htaccess b/issue_relations/public/.htaccess new file mode 100644 index 000000000..d3c998345 --- /dev/null +++ b/issue_relations/public/.htaccess @@ -0,0 +1,40 @@ +# General Apache options +AddHandler fastcgi-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.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/issue_relations/public/404.html b/issue_relations/public/404.html new file mode 100644 index 000000000..ddf424b09 --- /dev/null +++ b/issue_relations/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/issue_relations/public/500.html b/issue_relations/public/500.html new file mode 100644 index 000000000..93eb0f128 --- /dev/null +++ b/issue_relations/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/issue_relations/public/dispatch.cgi b/issue_relations/public/dispatch.cgi new file mode 100644 index 000000000..9730473f2 --- /dev/null +++ b/issue_relations/public/dispatch.cgi @@ -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/issue_relations/public/dispatch.fcgi b/issue_relations/public/dispatch.fcgi new file mode 100644 index 000000000..f934b3002 --- /dev/null +++ b/issue_relations/public/dispatch.fcgi @@ -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/issue_relations/public/dispatch.rb b/issue_relations/public/dispatch.rb new file mode 100644 index 000000000..9730473f2 --- /dev/null +++ b/issue_relations/public/dispatch.rb @@ -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/issue_relations/public/favicon.ico b/issue_relations/public/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/issue_relations/public/images/22x22/authent.png b/issue_relations/public/images/22x22/authent.png new file mode 100644 index 000000000..d2b29945f Binary files /dev/null and b/issue_relations/public/images/22x22/authent.png differ diff --git a/issue_relations/public/images/22x22/comment.png b/issue_relations/public/images/22x22/comment.png new file mode 100644 index 000000000..c5186abd9 Binary files /dev/null and b/issue_relations/public/images/22x22/comment.png differ diff --git a/issue_relations/public/images/22x22/file.png b/issue_relations/public/images/22x22/file.png new file mode 100644 index 000000000..96c56a2b5 Binary files /dev/null and b/issue_relations/public/images/22x22/file.png differ diff --git a/issue_relations/public/images/22x22/info.png b/issue_relations/public/images/22x22/info.png new file mode 100644 index 000000000..cf54e2c6a Binary files /dev/null and b/issue_relations/public/images/22x22/info.png differ diff --git a/issue_relations/public/images/22x22/notifications.png b/issue_relations/public/images/22x22/notifications.png new file mode 100644 index 000000000..972f4a24d Binary files /dev/null and b/issue_relations/public/images/22x22/notifications.png differ diff --git a/issue_relations/public/images/22x22/options.png b/issue_relations/public/images/22x22/options.png new file mode 100644 index 000000000..48da1516c Binary files /dev/null and b/issue_relations/public/images/22x22/options.png differ diff --git a/issue_relations/public/images/22x22/projects.png b/issue_relations/public/images/22x22/projects.png new file mode 100644 index 000000000..4f023bedb Binary files /dev/null and b/issue_relations/public/images/22x22/projects.png differ diff --git a/issue_relations/public/images/22x22/role.png b/issue_relations/public/images/22x22/role.png new file mode 100644 index 000000000..4de98edd4 Binary files /dev/null and b/issue_relations/public/images/22x22/role.png differ diff --git a/issue_relations/public/images/22x22/tracker.png b/issue_relations/public/images/22x22/tracker.png new file mode 100644 index 000000000..f51394186 Binary files /dev/null and b/issue_relations/public/images/22x22/tracker.png differ diff --git a/issue_relations/public/images/22x22/users.png b/issue_relations/public/images/22x22/users.png new file mode 100644 index 000000000..92f396207 Binary files /dev/null and b/issue_relations/public/images/22x22/users.png differ diff --git a/issue_relations/public/images/22x22/workflow.png b/issue_relations/public/images/22x22/workflow.png new file mode 100644 index 000000000..9d1b9d8b9 Binary files /dev/null and b/issue_relations/public/images/22x22/workflow.png differ diff --git a/issue_relations/public/images/32x32/file.png b/issue_relations/public/images/32x32/file.png new file mode 100644 index 000000000..1662b5302 Binary files /dev/null and b/issue_relations/public/images/32x32/file.png differ diff --git a/issue_relations/public/images/add.png b/issue_relations/public/images/add.png new file mode 100644 index 000000000..db59058e5 Binary files /dev/null and b/issue_relations/public/images/add.png differ diff --git a/issue_relations/public/images/admin.png b/issue_relations/public/images/admin.png new file mode 100644 index 000000000..c98330ca1 Binary files /dev/null and b/issue_relations/public/images/admin.png differ diff --git a/issue_relations/public/images/alert.png b/issue_relations/public/images/alert.png new file mode 100644 index 000000000..7cc6e4aee Binary files /dev/null and b/issue_relations/public/images/alert.png differ diff --git a/issue_relations/public/images/arrow_bw.png b/issue_relations/public/images/arrow_bw.png new file mode 100644 index 000000000..52dfc96f0 Binary files /dev/null and b/issue_relations/public/images/arrow_bw.png differ diff --git a/issue_relations/public/images/arrow_down.png b/issue_relations/public/images/arrow_down.png new file mode 100644 index 000000000..ea37f3a9e Binary files /dev/null and b/issue_relations/public/images/arrow_down.png differ diff --git a/issue_relations/public/images/arrow_from.png b/issue_relations/public/images/arrow_from.png new file mode 100644 index 000000000..4d5eeb9ea Binary files /dev/null and b/issue_relations/public/images/arrow_from.png differ diff --git a/issue_relations/public/images/arrow_to.png b/issue_relations/public/images/arrow_to.png new file mode 100644 index 000000000..4f969716f Binary files /dev/null and b/issue_relations/public/images/arrow_to.png differ diff --git a/issue_relations/public/images/attachment.png b/issue_relations/public/images/attachment.png new file mode 100644 index 000000000..eea26921b Binary files /dev/null and b/issue_relations/public/images/attachment.png differ diff --git a/issue_relations/public/images/calendar.png b/issue_relations/public/images/calendar.png new file mode 100644 index 000000000..619172a99 Binary files /dev/null and b/issue_relations/public/images/calendar.png differ diff --git a/issue_relations/public/images/close.png b/issue_relations/public/images/close.png new file mode 100644 index 000000000..3501ed4d5 Binary files /dev/null and b/issue_relations/public/images/close.png differ diff --git a/issue_relations/public/images/close_hl.png b/issue_relations/public/images/close_hl.png new file mode 100644 index 000000000..a433f7515 Binary files /dev/null and b/issue_relations/public/images/close_hl.png differ diff --git a/issue_relations/public/images/csv.png b/issue_relations/public/images/csv.png new file mode 100644 index 000000000..405863116 Binary files /dev/null and b/issue_relations/public/images/csv.png differ diff --git a/issue_relations/public/images/delete.png b/issue_relations/public/images/delete.png new file mode 100644 index 000000000..108a3fc97 Binary files /dev/null and b/issue_relations/public/images/delete.png differ diff --git a/issue_relations/public/images/edit.png b/issue_relations/public/images/edit.png new file mode 100644 index 000000000..0275d91e4 Binary files /dev/null and b/issue_relations/public/images/edit.png differ diff --git a/issue_relations/public/images/expand.png b/issue_relations/public/images/expand.png new file mode 100644 index 000000000..3e3aaa441 Binary files /dev/null and b/issue_relations/public/images/expand.png differ diff --git a/issue_relations/public/images/file.png b/issue_relations/public/images/file.png new file mode 100644 index 000000000..f387dd305 Binary files /dev/null and b/issue_relations/public/images/file.png differ diff --git a/issue_relations/public/images/folder.png b/issue_relations/public/images/folder.png new file mode 100644 index 000000000..d2ab69ad5 Binary files /dev/null and b/issue_relations/public/images/folder.png differ diff --git a/issue_relations/public/images/help.png b/issue_relations/public/images/help.png new file mode 100644 index 000000000..af4e6ff46 Binary files /dev/null and b/issue_relations/public/images/help.png differ diff --git a/issue_relations/public/images/home.png b/issue_relations/public/images/home.png new file mode 100644 index 000000000..21ee5470e Binary files /dev/null and b/issue_relations/public/images/home.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_br.png b/issue_relations/public/images/jstoolbar/bt_br.png new file mode 100644 index 000000000..f8211a997 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_br.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_code.png b/issue_relations/public/images/jstoolbar/bt_code.png new file mode 100644 index 000000000..52924abf7 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_code.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_del.png b/issue_relations/public/images/jstoolbar/bt_del.png new file mode 100644 index 000000000..c6f3a8b40 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_del.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_em.png b/issue_relations/public/images/jstoolbar/bt_em.png new file mode 100644 index 000000000..f08de4f30 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_em.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_ins.png b/issue_relations/public/images/jstoolbar/bt_ins.png new file mode 100644 index 000000000..f6697db51 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_ins.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_link.png b/issue_relations/public/images/jstoolbar/bt_link.png new file mode 100644 index 000000000..9b3acbae5 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_link.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_ol.png b/issue_relations/public/images/jstoolbar/bt_ol.png new file mode 100644 index 000000000..2dfaec7c7 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_ol.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_quote.png b/issue_relations/public/images/jstoolbar/bt_quote.png new file mode 100644 index 000000000..25b2b8abe Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_quote.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_strong.png b/issue_relations/public/images/jstoolbar/bt_strong.png new file mode 100644 index 000000000..7e200d3f6 Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_strong.png differ diff --git a/issue_relations/public/images/jstoolbar/bt_ul.png b/issue_relations/public/images/jstoolbar/bt_ul.png new file mode 100644 index 000000000..6e20851ec Binary files /dev/null and b/issue_relations/public/images/jstoolbar/bt_ul.png differ diff --git a/issue_relations/public/images/loading.gif b/issue_relations/public/images/loading.gif new file mode 100644 index 000000000..085ccaeca Binary files /dev/null and b/issue_relations/public/images/loading.gif differ diff --git a/issue_relations/public/images/locked.png b/issue_relations/public/images/locked.png new file mode 100644 index 000000000..c2789e35c Binary files /dev/null and b/issue_relations/public/images/locked.png differ diff --git a/issue_relations/public/images/move.png b/issue_relations/public/images/move.png new file mode 100644 index 000000000..32fdb846d Binary files /dev/null and b/issue_relations/public/images/move.png differ diff --git a/issue_relations/public/images/package.png b/issue_relations/public/images/package.png new file mode 100644 index 000000000..ff629d117 Binary files /dev/null and b/issue_relations/public/images/package.png differ diff --git a/issue_relations/public/images/pdf.png b/issue_relations/public/images/pdf.png new file mode 100644 index 000000000..68c9bada8 Binary files /dev/null and b/issue_relations/public/images/pdf.png differ diff --git a/issue_relations/public/images/projects.png b/issue_relations/public/images/projects.png new file mode 100644 index 000000000..244c896f0 Binary files /dev/null and b/issue_relations/public/images/projects.png differ diff --git a/issue_relations/public/images/save.png b/issue_relations/public/images/save.png new file mode 100644 index 000000000..7c499b932 Binary files /dev/null and b/issue_relations/public/images/save.png differ diff --git a/issue_relations/public/images/sort_asc.png b/issue_relations/public/images/sort_asc.png new file mode 100644 index 000000000..e9cb0f4f2 Binary files /dev/null and b/issue_relations/public/images/sort_asc.png differ diff --git a/issue_relations/public/images/sort_desc.png b/issue_relations/public/images/sort_desc.png new file mode 100644 index 000000000..fc80a5cc9 Binary files /dev/null and b/issue_relations/public/images/sort_desc.png differ diff --git a/issue_relations/public/images/task_done.png b/issue_relations/public/images/task_done.png new file mode 100644 index 000000000..2a4c81e9d Binary files /dev/null and b/issue_relations/public/images/task_done.png differ diff --git a/issue_relations/public/images/task_late.png b/issue_relations/public/images/task_late.png new file mode 100644 index 000000000..2e8a40d6e Binary files /dev/null and b/issue_relations/public/images/task_late.png differ diff --git a/issue_relations/public/images/task_todo.png b/issue_relations/public/images/task_todo.png new file mode 100644 index 000000000..43c1eb9b9 Binary files /dev/null and b/issue_relations/public/images/task_todo.png differ diff --git a/issue_relations/public/images/true.png b/issue_relations/public/images/true.png new file mode 100644 index 000000000..929c605ff Binary files /dev/null and b/issue_relations/public/images/true.png differ diff --git a/issue_relations/public/images/user.png b/issue_relations/public/images/user.png new file mode 100644 index 000000000..5f55e7e49 Binary files /dev/null and b/issue_relations/public/images/user.png differ diff --git a/issue_relations/public/images/user_new.png b/issue_relations/public/images/user_new.png new file mode 100644 index 000000000..aaa430dea Binary files /dev/null and b/issue_relations/public/images/user_new.png differ diff --git a/issue_relations/public/images/user_page.png b/issue_relations/public/images/user_page.png new file mode 100644 index 000000000..78144862c Binary files /dev/null and b/issue_relations/public/images/user_page.png differ diff --git a/issue_relations/public/images/users.png b/issue_relations/public/images/users.png new file mode 100644 index 000000000..f3a07c3f7 Binary files /dev/null and b/issue_relations/public/images/users.png differ diff --git a/issue_relations/public/images/zoom_in.png b/issue_relations/public/images/zoom_in.png new file mode 100644 index 000000000..d9abe7f52 Binary files /dev/null and b/issue_relations/public/images/zoom_in.png differ diff --git a/issue_relations/public/images/zoom_in_g.png b/issue_relations/public/images/zoom_in_g.png new file mode 100644 index 000000000..72b271c5e Binary files /dev/null and b/issue_relations/public/images/zoom_in_g.png differ diff --git a/issue_relations/public/images/zoom_out.png b/issue_relations/public/images/zoom_out.png new file mode 100644 index 000000000..906e4a4e5 Binary files /dev/null and b/issue_relations/public/images/zoom_out.png differ diff --git a/issue_relations/public/images/zoom_out_g.png b/issue_relations/public/images/zoom_out_g.png new file mode 100644 index 000000000..7f2416be2 Binary files /dev/null and b/issue_relations/public/images/zoom_out_g.png differ diff --git a/issue_relations/public/javascripts/application.js b/issue_relations/public/javascripts/application.js new file mode 100644 index 000000000..3625914ab --- /dev/null +++ b/issue_relations/public/javascripts/application.js @@ -0,0 +1,19 @@ +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; + } + } +} + +function addFileField() { + var f = document.createElement("input"); + f.type = "file"; + f.name = "attachments[]"; + f.size = 30; + + p = document.getElementById("attachments_p"); + p.appendChild(document.createElement("br")); + p.appendChild(f); +} \ No newline at end of file diff --git a/issue_relations/public/javascripts/calendar/calendar-setup.js b/issue_relations/public/javascripts/calendar/calendar-setup.js new file mode 100644 index 000000000..f2b485430 --- /dev/null +++ b/issue_relations/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/issue_relations/public/javascripts/calendar/calendar.js b/issue_relations/public/javascripts/calendar/calendar.js new file mode 100644 index 000000000..9088e0e89 --- /dev/null +++ b/issue_relations/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/issue_relations/public/javascripts/calendar/lang/calendar-de.js b/issue_relations/public/javascripts/calendar/lang/calendar-de.js new file mode 100644 index 000000000..59fb983bf --- /dev/null +++ b/issue_relations/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/issue_relations/public/javascripts/calendar/lang/calendar-en.js b/issue_relations/public/javascripts/calendar/lang/calendar-en.js new file mode 100644 index 000000000..0dbde793d --- /dev/null +++ b/issue_relations/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/issue_relations/public/javascripts/calendar/lang/calendar-es.js b/issue_relations/public/javascripts/calendar/lang/calendar-es.js new file mode 100644 index 000000000..11d0b53d5 --- /dev/null +++ b/issue_relations/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/issue_relations/public/javascripts/calendar/lang/calendar-fr.js b/issue_relations/public/javascripts/calendar/lang/calendar-fr.js new file mode 100644 index 000000000..fb0cb2380 --- /dev/null +++ b/issue_relations/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", + "Mar", + "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/issue_relations/public/javascripts/controls.js b/issue_relations/public/javascripts/controls.js new file mode 100644 index 000000000..9742b6918 --- /dev/null +++ b/issue_relations/public/javascripts/controls.js @@ -0,0 +1,750 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// See scriptaculous.js for full license. + +// 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. + +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); + 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) + return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + 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; + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + }, + + 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 = 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.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entryCount = + this.update.firstChild.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; + 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({ + okText: "ok", + 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, + ajaxOptions: {} + }, 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); + 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); + } + + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + this.form.appendChild(okButton); + + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + 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(); + } + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.type = "text"; + textField.name = "value"; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + 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(); + }, + 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(); + + 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); + } + } +}; + +// 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)); + } +}; \ No newline at end of file diff --git a/issue_relations/public/javascripts/dragdrop.js b/issue_relations/public/javascripts/dragdrop.js new file mode 100644 index 000000000..92d1f7316 --- /dev/null +++ b/issue_relations/public/javascripts/dragdrop.js @@ -0,0 +1,584 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// See scriptaculous.js for full license. + +/*--------------------------------------------------------------------------*/ + +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 + }, 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); + }, + + isContained: function(element, drop) { + var parentNode = element.parentNode; + return drop._containers.detect(function(c) { return parentNode == 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; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) { + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + if(drop.greedy) { + Droppables.activate(drop); + throw $break; + } + } + }); + }, + + 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) { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + }, + + deactivate: function(draggbale) { + 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.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + }, + + 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); + }); + }, + + _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.prototype = { + initialize: function(element) { + var options = Object.extend({ + handle: false, + starteffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); + }, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + element._revert = new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur}); + }, + endeffect: function(element) { + new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + }, + zindex: 1000, + revert: false, + snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } + }, arguments[1] || {}); + + this.element = $(element); + + if(options.handle && (typeof options.handle == 'string')) + this.handle = Element.childrenWithClassName(this.element, options.handle)[0]; + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + 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(this.element.style.left || '0'), + parseInt(this.element.style.top || '0')]); + }, + + initDrag: function(event) { + 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=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + if(this.element._revert) { + this.element._revert.cancel(); + this.element._revert = null; + } + + 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); + } + + 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); + + // 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.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + 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]); + } 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 + } +} + +/*--------------------------------------------------------------------------*/ + +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 = { + sortables: new Array(), + + options: function(element){ + element = $(element); + return this.sortables.detect(function(s) { return s.element == element }); + }, + + destroy: function(element){ + element = $(element); + this.sortables.findAll(function(s) { return s.element == element }).each(function(s){ + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + }); + this.sortables = this.sortables.reject(function(s) { return s.element == element }); + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, // fixme: unimplemented + 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, + hoverclass: null, + ghosting: false, + format: null, + 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, + 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, + hoverclass: options.hoverclass, + onHover: Sortable.onHover, + greedy: !options.dropOnEmpty + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // make it so + + // drop on empty handling + if(options.dropOnEmpty) { + Droppables.add(element, + {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false}); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.childrenWithClassName(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + options.droppables.push(e); + }); + + // keep reference + this.sortables.push(options); + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + if(!element.hasChildNodes()) return null; + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() && + (!options.only || (Element.hasClassName(e, options.only)))) + elements.push(e); + if(options.tree) { + var grandchildren = this.findElements(e, options); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : null); + }, + + onHover: function(element, dropon, overlap) { + 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) { + if(element.parentNode!=dropon) { + var oldParentNode = element.parentNode; + dropon.appendChild(element); + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon).onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + 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') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.addClassName(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.left = offsets[0] + 'px'; + Sortable._marker.style.top = offsets[1] + 'px'; + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + else + Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + + Element.show(Sortable._marker); + }, + + serialize: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format || /^[^_]*_(.*)$/ + }, arguments[1] || {}); + return $(this.findElements(element, options) || []).map( function(item) { + return (encodeURIComponent(options.name) + "[]=" + + encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : '')); + }).join("&"); + } +} \ No newline at end of file diff --git a/issue_relations/public/javascripts/effects.js b/issue_relations/public/javascripts/effects.js new file mode 100644 index 000000000..414398ce4 --- /dev/null +++ b/issue_relations/public/javascripts/effects.js @@ -0,0 +1,854 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// See scriptaculous.js for full license. + +/* ------------- element ext -------------- */ + +// 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.collectTextNodesIgnoreClass = function(element, ignoreclass) { + var children = $(element).childNodes; + var text = ''; + var classtest = new RegExp('^([^ ]+ )*' + ignoreclass+ '( [^ ]+)*$','i'); + + for (var i = 0; i < children.length; i++) { + if(children[i].nodeType==3) { + text+=children[i].nodeValue; + } else { + if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) + text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); + } + } + + return text; +} + +Element.setStyle = function(element, style) { + element = $(element); + for(k in style) element.style[k.camelize()] = style[k]; +} + +Element.setContentZoom = function(element, percent) { + Element.setStyle(element, {fontSize: (percent/100) + 'em'}); + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + var opacity; + if (opacity = Element.getStyle(element, 'opacity')) + return parseFloat(opacity); + if (opacity = (Element.getStyle(element, '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(element, { opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : null }); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + Element.setStyle(element, {opacity: value}); + if(/MSIE/.test(navigator.userAgent)) + Element.setStyle(element, + { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.childrenWithClassName = function(element, className) { + return $A($(element).getElementsByTagName('*')).select( + function(c) { return Element.hasClassName(c, className) }); +} + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent)) 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 })); + }); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = {} + +Effect.Transitions.linear = function(pos) { + return pos; +} +Effect.Transitions.sinoidal = function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; +} +Effect.Transitions.reverse = function(pos) { + return 1-pos; +} +Effect.Transitions.flicker = function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; +} +Effect.Transitions.wobble = function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; +} +Effect.Transitions.pulse = function(pos) { + return (Math.floor(pos*10) % 2 == 0 ? + (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); +} +Effect.Transitions.none = function(pos) { + return 0; +} +Effect.Transitions.full = function(pos) { + return 1; +} + +/* ------------- core effects ------------- */ + +Effect.Queue = { + effects: [], + _each: function(iterator) { + this.effects._each(iterator); + }, + interval: null, + add: function(effect) { + var timestamp = new Date().getTime(); + + switch(effect.options.queue) { + 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 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + 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); + } +} +Object.extend(Effect.Queue, Enumerable); + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + setOptions: function(options) { + this.options = Object.extend({ + 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' + }, options || {}); + }, + start: function(options) { + this.setOptions(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.Queue.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.Queue.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.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + Element.setStyle(this.element, {zoom: 1}); + var options = Object.extend({ + from: Element.getOpacity(this.element) || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + Element.setOpacity(this.element, position); + } +}); + +Effect.MoveBy = Class.create(); +Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { + initialize: function(element, toTop, toLeft) { + this.element = $(element); + this.toTop = toTop; + this.toLeft = toLeft; + this.start(arguments[3]); + }, + 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) + Element.makePositioned(this.element); + this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0'); + this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0'); + }, + update: function(position) { + Element.setStyle(this.element, { + top: this.toTop * position + this.originalTop + 'px', + left: this.toLeft * position + this.originalLeft + 'px' + }); + } +}); + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + 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 = Element.getStyle(this.element,'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 = Element.getStyle(this.element,'font-size') || '100%'; + ['em','px','%'].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) + Element.setStyle(this.element, {fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if (this.restoreAfterFinish) Element.setStyle(this.element, this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = width + 'px'; + if(this.options.scaleY) d.height = 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'; + } + } + Element.setStyle(this.element, d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(Element.getStyle(this.element, 'display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: Element.getStyle(this.element, 'background-image') }; + Element.setStyle(this.element, {backgroundImage: 'none'}); + if(!this.options.endcolor) + this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = Element.getStyle(this.element, '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) { + Element.setStyle(this.element,{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() { + Element.setStyle(this.element, 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) { + var oldOpacity = Element.getInlineOpacity(element); + var options = Object.extend({ + from: Element.getOpacity(element) || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { with(Element) { + if(effect.options.to!=0) return; + hide(effect.element); + setStyle(effect.element, {opacity: oldOpacity}); }} + }, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + var options = Object.extend({ + from: (Element.getStyle(element, 'display') == 'none' ? 0.0 : Element.getOpacity(element) || 0.0), + to: 1.0, + beforeSetup: function(effect) { with(Element) { + setOpacity(effect.element, effect.options.from); + show(effect.element); }} + }, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { opacity: Element.getInlineOpacity(element), position: Element.getStyle(element, 'position') }; + 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) { with(Element) { + setStyle(effect.effects[0].element, {position: 'absolute'}); }}, + afterFinishInternal: function(effect) { with(Element) { + hide(effect.effects[0].element); + setStyle(effect.effects[0].element, oldStyle); }} + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + Element.makeClipping(element); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { with(Element) { + [hide, undoClipping].call(effect.element); }} + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var oldHeight = Element.getStyle(element, 'height'); + var elementDimensions = Element.getDimensions(element); + 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) { with(Element) { + makeClipping(effect.element); + setStyle(effect.element, {height: '0px'}); + show(effect.element); + }}, + afterFinishInternal: function(effect) { with(Element) { + undoClipping(effect.element); + setStyle(effect.element, {height: oldHeight}); + }} + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Appear(element, { + 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) { with(Element) { + [makePositioned,makeClipping].call(effect.element); + }}, + afterFinishInternal: function(effect) { with(Element) { + [hide,undoClipping,undoPositioned].call(effect.element); + setStyle(effect.element, {opacity: oldOpacity}); + }} + }) + } + }); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: Element.getStyle(element, 'top'), + left: Element.getStyle(element, 'left'), + opacity: Element.getInlineOpacity(element) }; + return new Effect.Parallel( + [ new Effect.MoveBy(element, 100, 0, { sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { with(Element) { + makePositioned(effect.effects[0].element); }}, + afterFinishInternal: function(effect) { with(Element) { + [hide, undoPositioned].call(effect.effects[0].element); + setStyle(effect.effects[0].element, oldStyle); }} + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: Element.getStyle(element, 'top'), + left: Element.getStyle(element, 'left') }; + return new Effect.MoveBy(element, 0, 20, + { duration: 0.05, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinishInternal: function(effect) { + new Effect.MoveBy(effect.element, 0, -20, + { duration: 0.05, afterFinishInternal: function(effect) { with(Element) { + undoPositioned(effect.element); + setStyle(effect.element, oldStyle); + }}}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + Element.cleanWhitespace(element); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom'); + var elementDimensions = Element.getDimensions(element); + 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) { with(Element) { + makePositioned(effect.element); + makePositioned(effect.element.firstChild); + if(window.opera) setStyle(effect.element, {top: ''}); + makeClipping(effect.element); + setStyle(effect.element, {height: '0px'}); + show(element); }}, + afterUpdateInternal: function(effect) { with(Element) { + setStyle(effect.element.firstChild, {bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); }}, + afterFinishInternal: function(effect) { with(Element) { + undoClipping(effect.element); + undoPositioned(effect.element.firstChild); + undoPositioned(effect.element); + setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }} + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + Element.cleanWhitespace(element); + var oldInnerBottom = Element.getStyle(element.firstChild, 'bottom'); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { with(Element) { + makePositioned(effect.element); + makePositioned(effect.element.firstChild); + if(window.opera) setStyle(effect.element, {top: ''}); + makeClipping(effect.element); + show(element); }}, + afterUpdateInternal: function(effect) { with(Element) { + setStyle(effect.element.firstChild, {bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); }}, + afterFinishInternal: function(effect) { with(Element) { + [hide, undoClipping].call(effect.element); + undoPositioned(effect.element.firstChild); + undoPositioned(effect.element); + setStyle(effect.element.firstChild, {bottom: oldInnerBottom}); }} + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { with(Element) { + makeClipping(effect.element); }}, + afterFinishInternal: function(effect) { with(Element) { + hide(effect.element); + undoClipping(effect.element); }} + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransistion: 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(element) }; + + var dims = Element.getDimensions(element); + 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.MoveBy(element, initialMoveY, initialMoveX, { + duration: 0.01, + beforeSetup: function(effect) { with(Element) { + hide(effect.element); + makeClipping(effect.element); + makePositioned(effect.element); + }}, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.MoveBy(effect.element, moveY, moveX, { 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) { with(Element) { + setStyle(effect.effects[0].element, {height: '0px'}); + show(effect.effects[0].element); }}, + afterFinishInternal: function(effect) { with(Element) { + [undoClipping, undoPositioned].call(effect.effects[0].element); + setStyle(effect.effects[0].element, oldStyle); }} + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransistion: 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(element) }; + + var dims = Element.getDimensions(element); + 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.MoveBy(element, moveY, moveX, { sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { with(Element) { + [makePositioned, makeClipping].call(effect.effects[0].element) }}, + afterFinishInternal: function(effect) { with(Element) { + [hide, undoClipping, undoPositioned].call(effect.effects[0].element); + setStyle(effect.effects[0].element, oldStyle); }} + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = Element.getInlineOpacity(element); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { Element.setStyle(effect.element, {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(element); + 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) { with(Element) { + [hide, undoClipping].call(effect.element); + setStyle(effect.element, oldStyle); + }} }); + }}, arguments[1] || {})); +} diff --git a/issue_relations/public/javascripts/jstoolbar.js b/issue_relations/public/javascripts/jstoolbar.js new file mode 100644 index 000000000..cf9454619 --- /dev/null +++ b/issue_relations/public/javascripts/jstoolbar.js @@ -0,0 +1,440 @@ +/* ***** 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) { + this.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'); + 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: {}, + + getMode: function() { + return this.mode; + }, + + setMode: function(mode) { + this.mode = mode || 'wiki'; + }, + + switchMode: function(mode) { + mode = mode || 'wiki'; + this.draw(mode); + }, + + 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/**/ + + // 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); + }, + + 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") { + var range = document.selection.createRange().text = subst; + 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 emphasis', + fn: { + wiki: function() { this.singleTag('*') } + } +} + +// em +jsToolBar.prototype.elements.em = { + type: 'button', + title: 'Emphasis', + fn: { + wiki: function() { this.singleTag("_") } + } +} + +// ins +jsToolBar.prototype.elements.ins = { + type: 'button', + title: 'Inserted', + fn: { + wiki: function() { this.singleTag('+') } + } +} + +// del +jsToolBar.prototype.elements.del = { + type: 'button', + title: 'Deleted', + fn: { + wiki: function() { this.singleTag('-') } + } +} + +// quote +//jsToolBar.prototype.elements.quote = { +// type: 'button', +// title: 'Inline quote', +// 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'} + +// br +//jsToolBar.prototype.elements.br = { +// type: 'button', +// title: 'Line break', +// fn: { +// wiki: function() { this.encloseSelection("%%%\n",'') } +// } +//} + +// spacer +jsToolBar.prototype.elements.space2 = {type: 'space'} + +// ul +jsToolBar.prototype.elements.ul = { + type: 'button', + title: 'Unordered list', + fn: { + wiki: function() { + this.encloseSelection('','',function(str) { + str = str.replace(/\r/g,''); + return '* '+str.replace(/\n/g,"\n* "); + }); + } + } +} + +// ol +jsToolBar.prototype.elements.ol = { + type: 'button', + title: 'Ordered list', + fn: { + wiki: function() { + this.encloseSelection('','',function(str) { + str = str.replace(/\r/g,''); + return '# '+str.replace(/\n/g,"\n# "); + }); + } + } +} + +// spacer +jsToolBar.prototype.elements.space3 = {type: 'space'} + +// link +jsToolBar.prototype.elements.link = { + type: 'button', + title: 'Link', + fn: {}, + href_prompt: 'Please give page URL:', + hreflang_prompt: 'Language of this page:', + default_hreflang: '', + prompt: function(href,hreflang) { + href = href || ''; + hreflang = hreflang || this.elements.link.default_hreflang; + + href = window.prompt(this.elements.link.href_prompt,href); + if (!href) { return false; } + + hreflang = "" + + return { href: this.stripBaseURL(href), hreflang: hreflang }; + } +} + +jsToolBar.prototype.elements.link.fn.wiki = function() { + var link = this.elements.link.prompt.call(this); + if (link) { + var stag = '"'; + var etag = '":'+link.href; + this.encloseSelection(stag,etag); + } +}; diff --git a/issue_relations/public/javascripts/menu.js b/issue_relations/public/javascripts/menu.js new file mode 100644 index 000000000..bf5612dd5 --- /dev/null +++ b/issue_relations/public/javascripts/menu.js @@ -0,0 +1,556 @@ +//***************************************************************************** +// Do not remove this notice. +// +// Copyright 2000-2004 by Mike Hall. +// See http://www.brainjar.com for terms of use. +//***************************************************************************** + +//---------------------------------------------------------------------------- +// Emulation de la fonction push pour IE5.0 +//---------------------------------------------------------------------------- +if(!Array.prototype.push){Array.prototype.push=function(){for(var i=0;i-1 && ua.indexOf("Mac")>-1) { + this.isIE5mac = true; + this.version = ""; + return; + } + //-- fin ajout ci ---- + + s = "Opera"; + if ((i = ua.indexOf(s)) >= 0) { + this.isOP = true; + this.version = parseFloat(ua.substr(i + s.length)); + return; + } + + s = "Netscape6/"; + if ((i = ua.indexOf(s)) >= 0) { + this.isNS = true; + this.version = parseFloat(ua.substr(i + s.length)); + return; + } + + // Treat any other "Gecko" browser as Netscape 6.1. + + s = "Gecko"; + if ((i = ua.indexOf(s)) >= 0) { + this.isNS = true; + this.version = 6.1; + return; + } + + s = "MSIE"; + if ((i = ua.indexOf(s))) { + this.isIE = true; + this.version = parseFloat(ua.substr(i + s.length)); + return; + } +} + +var browser = new Browser(); + +//---------------------------------------------------------------------------- +// Code for handling the menu bar and active button. +//---------------------------------------------------------------------------- + +var activeButton = null; + + +function buttonClick(event, menuId) { + + var button; + + // Get the target button element. + + if (browser.isIE) + button = window.event.srcElement; + else + button = event.currentTarget; + + // Blur focus from the link to remove that annoying outline. + + button.blur(); + + // Associate the named menu to this button if not already done. + // Additionally, initialize menu display. + + if (button.menu == null) { + button.menu = document.getElementById(menuId); + if (button.menu.isInitialized == null) + menuInit(button.menu); + } + + // Set mouseout event handler for the button, if not already done. + + if (button.onmouseout == null) + button.onmouseout = buttonOrMenuMouseout; + + // Exit if this button is the currently active one. + + if (button == activeButton) + return false; + + // Reset the currently active button, if any. + + if (activeButton != null) + resetButton(activeButton); + + // Activate this button, unless it was the currently active one. + + if (button != activeButton) { + depressButton(button); + activeButton = button; + } + else + activeButton = null; + + return false; +} + +function buttonMouseover(event, menuId) { + + var button; +//-- debut ajout ci ---- + if (!browser.isIE5mac) { + //-- fin ajout ci ---- + +//-- debut ajout ci ---- + cicacheselect(); +//-- fin ajout ci ---- + + // Activates this button's menu if no other is currently active. + + if (activeButton == null) { + buttonClick(event, menuId); + return; + } + + // Find the target button element. + + if (browser.isIE) + button = window.event.srcElement; + else + button = event.currentTarget; + + // If any other button menu is active, make this one active instead. + + if (activeButton != null && activeButton != button) + buttonClick(event, menuId); + //-- debut ajout ci ---- + } + //-- fin ajout ci ---- + +} + +function depressButton(button) { + + var x, y; + + // Update the button's style class to make it look like it's + // depressed. + + button.className += " menuButtonActive"; + + // Set mouseout event handler for the button, if not already done. + + if (button.onmouseout == null) + button.onmouseout = buttonOrMenuMouseout; + if (button.menu.onmouseout == null) + button.menu.onmouseout = buttonOrMenuMouseout; + + // Position the associated drop down menu under the button and + // show it. + + x = getPageOffsetLeft(button); + y = getPageOffsetTop(button) + button.offsetHeight - 1; + + // For IE, adjust position. + + if (browser.isIE) { + x += button.offsetParent.clientLeft; + y += button.offsetParent.clientTop; + } + + button.menu.style.left = x + "px"; + button.menu.style.top = y + "px";0 + button.menu.style.visibility = "visible"; +} + +function resetButton(button) { + + // Restore the button's style class. + + removeClassName(button, "menuButtonActive"); + + // Hide the button's menu, first closing any sub menus. + + if (button.menu != null) { + closeSubMenu(button.menu); + button.menu.style.visibility = "hidden"; + } +} + +//---------------------------------------------------------------------------- +// Code to handle the menus and sub menus. +//---------------------------------------------------------------------------- + +function menuMouseover(event) { + + var menu; + //-- debut ajout ci ---- + if (!browser.isIE5mac) { + //-- fin ajout ci ---- +//-- debut ajout ci ---- + cicacheselect(); +//-- fin ajout ci ---- + + // Find the target menu element. + if (browser.isIE) + menu = getContainerWith(window.event.srcElement, "DIV", "menu"); + else + menu = event.currentTarget; + + // Close any active sub menu. + + if (menu.activeItem != null) + closeSubMenu(menu); + //-- debut ajout ci ---- + } + //-- fin ajout ci ---- +} + +function menuItemMouseover(event, menuId) { + + var item, menu, x, y; +//-- debut ajout ci ---- + cicacheselect(); +//-- fin ajout ci ---- + + // Find the target item element and its parent menu element. + + if (browser.isIE) + item = getContainerWith(window.event.srcElement, "A", "menuItem"); + else + item = event.currentTarget; + menu = getContainerWith(item, "DIV", "menu"); + + // Close any active sub menu and mark this one as active. + + if (menu.activeItem != null) + closeSubMenu(menu); + menu.activeItem = item; + + // Highlight the item element. + + item.className += " menuItemHighlight"; + + // Initialize the sub menu, if not already done. + + if (item.subMenu == null) { + item.subMenu = document.getElementById(menuId); + if (item.subMenu.isInitialized == null) + menuInit(item.subMenu); + } + + // Set mouseout event handler for the sub menu, if not already done. + + if (item.subMenu.onmouseout == null) + item.subMenu.onmouseout = buttonOrMenuMouseout; + + // Get position for submenu based on the menu item. + + x = getPageOffsetLeft(item) + item.offsetWidth; + y = getPageOffsetTop(item); + + // Adjust position to fit in view. + + var maxX, maxY; + + if (browser.isIE) { + maxX = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft) + + (document.documentElement.clientWidth != 0 ? document.documentElement.clientWidth : document.body.clientWidth); + maxY = Math.max(document.documentElement.scrollTop, document.body.scrollTop) + + (document.documentElement.clientHeight != 0 ? document.documentElement.clientHeight : document.body.clientHeight); + } + if (browser.isOP) { + maxX = document.documentElement.scrollLeft + window.innerWidth; + maxY = document.documentElement.scrollTop + window.innerHeight; + } + if (browser.isNS) { + maxX = window.scrollX + window.innerWidth; + maxY = window.scrollY + window.innerHeight; + } + maxX -= item.subMenu.offsetWidth; + maxY -= item.subMenu.offsetHeight; + + if (x > maxX) + x = Math.max(0, x - item.offsetWidth - item.subMenu.offsetWidth + + (menu.offsetWidth - item.offsetWidth)); + y = Math.max(0, Math.min(y, maxY)); + + // Position and show the sub menu. + + item.subMenu.style.left = x + "px"; + item.subMenu.style.top = y + "px"; + item.subMenu.style.visibility = "visible"; + + // Stop the event from bubbling. + + if (browser.isIE) + window.event.cancelBubble = true; + else + event.stopPropagation(); +} + +function closeSubMenu(menu) { + + if (menu == null || menu.activeItem == null) + return; + + // Recursively close any sub menus. + + if (menu.activeItem.subMenu != null) { + closeSubMenu(menu.activeItem.subMenu); + menu.activeItem.subMenu.style.visibility = "hidden"; + menu.activeItem.subMenu = null; + } + removeClassName(menu.activeItem, "menuItemHighlight"); + menu.activeItem = null; +} + + +function buttonOrMenuMouseout(event) { + + var el; + + // If there is no active button, exit. + + if (activeButton == null) + return; + + // Find the element the mouse is moving to. + + if (browser.isIE) + el = window.event.toElement; + else if (event.relatedTarget != null) + el = (event.relatedTarget.tagName ? event.relatedTarget : event.relatedTarget.parentNode); + + // If the element is not part of a menu, reset the active button. + + if (getContainerWith(el, "DIV", "menu") == null) { + resetButton(activeButton); + activeButton = null; +//-- debut ajout ci ---- + cimontreselect(); +//-- fin ajout ci ---- + } +} + + +//---------------------------------------------------------------------------- +// Code to initialize menus. +//---------------------------------------------------------------------------- + +function menuInit(menu) { + + var itemList, spanList; + var textEl, arrowEl; + var itemWidth; + var w, dw; + var i, j; + + // For IE, replace arrow characters. + + if (browser.isIE) { + menu.style.lineHeight = "2.5ex"; + spanList = menu.getElementsByTagName("SPAN"); + for (i = 0; i < spanList.length; i++) + if (hasClassName(spanList[i], "menuItemArrow")) { + spanList[i].style.fontFamily = "Webdings"; + spanList[i].firstChild.nodeValue = "4"; + } + } + + // Find the width of a menu item. + + itemList = menu.getElementsByTagName("A"); + if (itemList.length > 0) + itemWidth = itemList[0].offsetWidth; + else + return; + + // For items with arrows, add padding to item text to make the + // arrows flush right. + + for (i = 0; i < itemList.length; i++) { + spanList = itemList[i].getElementsByTagName("SPAN"); + textEl = null; + arrowEl = null; + for (j = 0; j < spanList.length; j++) { + if (hasClassName(spanList[j], "menuItemText")) + textEl = spanList[j]; + if (hasClassName(spanList[j], "menuItemArrow")) + arrowEl = spanList[j]; + } + if (textEl != null && arrowEl != null) { + textEl.style.paddingRight = (itemWidth + - (textEl.offsetWidth + arrowEl.offsetWidth)) + "px"; + // For Opera, remove the negative right margin to fix a display bug. + if (browser.isOP) + arrowEl.style.marginRight = "0px"; + } + } + + // Fix IE hover problem by setting an explicit width on first item of + // the menu. + + if (browser.isIE) { + w = itemList[0].offsetWidth; + itemList[0].style.width = w + "px"; + dw = itemList[0].offsetWidth - w; + w -= dw; + itemList[0].style.width = w + "px"; + } + + // Mark menu as initialized. + + menu.isInitialized = true; +} + +//---------------------------------------------------------------------------- +// General utility functions. +//---------------------------------------------------------------------------- + +function getContainerWith(node, tagName, className) { + + // Starting with the given node, find the nearest containing element + // with the specified tag name and style class. + + while (node != null) { + if (node.tagName != null && node.tagName == tagName && + hasClassName(node, className)) + return node; + node = node.parentNode; + } + + return node; +} + +function hasClassName(el, name) { + + var i, list; + + // Return true if the given element currently has the given class + // name. + + list = el.className.split(" "); + for (i = 0; i < list.length; i++) + if (list[i] == name) + return true; + + return false; +} + +function removeClassName(el, name) { + + var i, curList, newList; + + if (el.className == null) + return; + + // Remove the given class name from the element's className property. + + newList = new Array(); + curList = el.className.split(" "); + for (i = 0; i < curList.length; i++) + if (curList[i] != name) + newList.push(curList[i]); + el.className = newList.join(" "); +} + +function getPageOffsetLeft(el) { + + var x; + + // Return the x coordinate of an element relative to the page. + + x = el.offsetLeft; + if (el.offsetParent != null) + x += getPageOffsetLeft(el.offsetParent); + + return x; +} + +function getPageOffsetTop(el) { + + var y; + + // Return the x coordinate of an element relative to the page. + + y = el.offsetTop; + if (el.offsetParent != null) + y += getPageOffsetTop(el.offsetParent); + + return y; +} + +//-- debut ajout ci ---- +function cicacheselect(){ + if (browser.isIE) { + oSelects = document.getElementsByTagName('SELECT'); + if (oSelects.length > 0) { + for (i = 0; i < oSelects.length; i++) { + oSlt = oSelects[i]; + if (oSlt.style.visibility != 'hidden') {oSlt.style.visibility = 'hidden';} + } + } + oSelects = document.getElementsByName('masquable'); + if (oSelects.length > 0) { + for (i = 0; i < oSelects.length; i++) { + oSlt = oSelects[i]; + if (oSlt.style.visibility != 'hidden') {oSlt.style.visibility = 'hidden';} + } + } + } +} + +function cimontreselect(){ + if (browser.isIE) { + oSelects = document.getElementsByTagName('SELECT'); + if (oSelects.length > 0) { + for (i = 0; i < oSelects.length; i++) { + oSlt = oSelects[i]; + if (oSlt.style.visibility != 'visible') {oSlt.style.visibility = 'visible';} + } + } + oSelects = document.getElementsByName('masquable'); + if (oSelects.length > 0) { + for (i = 0; i < oSelects.length; i++) { + oSlt = oSelects[i]; + if (oSlt.style.visibility != 'visible') {oSlt.style.visibility = 'visible';} + } + } + } +} + +//-- fin ajout ci ---- diff --git a/issue_relations/public/javascripts/prototype.js b/issue_relations/public/javascripts/prototype.js new file mode 100644 index 000000000..e9ccd3c88 --- /dev/null +++ b/issue_relations/public/javascripts/prototype.js @@ -0,0 +1,1785 @@ +/* Prototype JavaScript framework, version 1.4.0 + * (c) 2005 Sam Stephenson + * + * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff + * against the source tree, available from the Prototype darcs repository. + * + * 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.4.0', + 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 (property in source) { + destination[property] = source[property]; + } + return destination; +} + +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; + } +} + +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; + return function(event) { + return __method.call(object, event || window.event); + } +} + +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; i < arguments.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() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} + +/*--------------------------------------------------------------------------*/ + +function $() { + var elements = new Array(); + + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + + if (arguments.length == 1) + return element; + + elements.push(element); + } + + return elements; +} +Object.extend(String.prototype, { + 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(eval); + }, + + 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[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +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; + } + }, + + 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 = true; + 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(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; + }, + + 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.collect(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 (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + 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.collect(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.collect(Prototype.K); + }, + + 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) { + iterator(value = collections.pluck(index)); + return value; + }); + }, + + 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; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.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 != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(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; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + shift: function() { + var result = this[0]; + for (var i = 0; i < this.length - 1; i++) + this[i] = this[i + 1]; + this.length--; + return result; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (key in this) { + var value = this[key]; + if (typeof value == 'function') 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($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +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; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + 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 ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')}, + function() {return new XMLHttpRequest()} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && 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, + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval(this.header('X-JSON')); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + 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.containers = { + success: container.success ? $(container.success) : $(container), + failure: 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, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + 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.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); + } +}); +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(child); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + 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; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + 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; + } + } + }, + + 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 = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +}); + +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) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.insertContent(this.contentFromAnonymousTable()); + } 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(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +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(); + if (this.lastValue != value) { + 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() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + 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, + + 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; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var 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) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +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) { + 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; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + 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 { + 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]; + } +} \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01.html b/issue_relations/public/manual/en/ch01.html new file mode 100644 index 000000000..67447735e --- /dev/null +++ b/issue_relations/public/manual/en/ch01.html @@ -0,0 +1,3 @@ + + + Chapter 1. Administrationredmine

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s01.html b/issue_relations/public/manual/en/ch01s01.html new file mode 100644 index 000000000..c30dfd3f0 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s01.html @@ -0,0 +1,3 @@ + + + 1. Projectsredmine

    1. Projects

    These screens allow you to manage projects.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s02.html b/issue_relations/public/manual/en/ch01s02.html new file mode 100644 index 000000000..71eb523e7 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s02.html @@ -0,0 +1,3 @@ + + + 2. Usersredmine

    2. Users

    These screens allow you to manage the application users.

    2.1. Users’ List

    Accounts status:

    • This icon means that the account is locked. A user having a locked account cannot log in and access the application.

    • This icon means that the user hasn't yet actived his account.

    The Lock/Unlock buttons allow you to lock/unlock the user accounts.

    2.2. User Creation or Modification

    In modification mode, please leave the Password field blank in order to keep the user’s password unchanged.

    A user designated as administrator has unrestricted access to the application and to all projects.

    • Administrator : designate the user as the administrator of the application.

    • E-mail notifications : activate or de-activate automatic e-mail notifications for this user

    • Locked : de-activates the user’s account

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s03.html b/issue_relations/public/manual/en/ch01s03.html new file mode 100644 index 000000000..aba795d0e --- /dev/null +++ b/issue_relations/public/manual/en/ch01s03.html @@ -0,0 +1,3 @@ + + + 3. Roles and Permissionsredmine

    3. Roles and Permissions

    Roles organize the permissions of various members of a project. Each member of a project has a one Role in a project. A user can have different roles in different projects.

    On the new or edit Role screen, check off the actions authorized for the Role.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s04.html b/issue_relations/public/manual/en/ch01s04.html new file mode 100644 index 000000000..7de2e211d --- /dev/null +++ b/issue_relations/public/manual/en/ch01s04.html @@ -0,0 +1,3 @@ + + + 4. Trackersredmine

    4. Trackers

    Trackers allow the sorting of Issues and can define specific workflows.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s05.html b/issue_relations/public/manual/en/ch01s05.html new file mode 100644 index 000000000..77939d302 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s05.html @@ -0,0 +1,3 @@ + + + 5. Custom fieldsredmine

    5. Custom fields

    Custom fields allow you to add additional information in Projects, Issues or Users. A custom field can be of one the following types:

    • Integer : positive or negative number

    • String : a string of characters - one single line of input.

    • Text : a string of characters with multiple lines of input. Differs from String Format by providing multiple lines of input instead of a single line.

    • Date : date

    • Boolean : true or false (check if necessary)

    • List : value to select from a predefined list (aka: scroll list or select box)

    Validation elements can be defined:

    • Required : A required field must have input in the forms

    • For all the projects : field automatically associated to all of the projects

    • Min - max length : minimum and maximum length for the input fields (0 means that there is no restriction)

    • Regular Expression : regular expressions may provide validation of the input value

      Examples:

      ^\[A-Z]{4}\d+$ : 4 capital letters followed by one or several digits

      ^[^0-9]*$ : characters only - no digits

    • Possible values : possible values for the fields of "List" type. Values are separated by the character |

    5.1. Fields for Projects

    • Required : required field

    5.2. Fields for Issues

    • For all projects : field automatically associated to all project Issues

      If this option is not activated, each project could choose whether or not to use the field for its Issues (please see the project configuration).

    5.3. Field for Users

    • Required : required field

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s06.html b/issue_relations/public/manual/en/ch01s06.html new file mode 100644 index 000000000..822634e43 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s06.html @@ -0,0 +1,3 @@ + + + 6. Issue statusredmine

    6. Issue status

    These screens allow you to define the different possible Issue statuses.

    • Closed : indicates Issue is considered as closed

    • Default : status applied by default to new Issue requests (only one status can be Default status)

    • Color : HTML color code (6 characters) representing the displayed status

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s07.html b/issue_relations/public/manual/en/ch01s07.html new file mode 100644 index 000000000..22bca99a3 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s07.html @@ -0,0 +1,3 @@ + + + 7. Workflowredmine

    7. Workflow

    The workflow allows to define changes the various project members are allowed to make on the Issues, according to their type.

    Select the role and the tracker for which you want to modify the workflow, then click Edit. The screen allows you then to modify the authorized change, for the chosen role and tracker. The Current Status options indicate the initial request status. The "New Statuses allowed" columns stand for the authorized status to apply.

    Note: In order for a particular Role to change an Issue status, the authorization must be given to it explicitly, regardless of the workflow configuration.

    In the above example, Bug type Issue requests with a New status could be given an Assigned or Resolved status by the Developer role. Those with an Assigned status could get a Resolved status. The status of all the other Bug type requests cannot be modified by the Developer.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s08.html b/issue_relations/public/manual/en/ch01s08.html new file mode 100644 index 000000000..579599b26 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s08.html @@ -0,0 +1,3 @@ + + + 8. Enumerationsredmine

    8. Enumerations

    The value lists used by the application can be customized (for example, setting Issue priorities). This screen allows you to define the possible values for each of the following lists:

    • Issue Priorities

    • Document Categories

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s09.html b/issue_relations/public/manual/en/ch01s09.html new file mode 100644 index 000000000..81ed5b538 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s09.html @@ -0,0 +1,3 @@ + + + 9. E-mail notificationsredmine

    9. E-mail notifications

    This screen allows you to select the actions that will generate an e-mail notification for project members.

    Note: E-mail sending must be activated in the application configuration if you want to make any notifications.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s10.html b/issue_relations/public/manual/en/ch01s10.html new file mode 100644 index 000000000..d81f1e415 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s10.html @@ -0,0 +1,3 @@ + + + 10. Authenticationredmine

    10. Authentication

    By default, redMine refers to its own database to authenticate users, by a specific password.

    If you already have one or several external user references (like LDAP), you can make them known in order to be used for authentication on redMine. This allows users to access redMine with their usual user names and passwords.

    For each known reference, you can specify if the accounts can be created on the fly on redMine. If needed, the user accounts will be created automatically during the user’s signing in (without any specific rights on the projects), according to information available in the reference. Otherwise, the administrator must have previously created the user account on redMine.

    10.1. LDAP statement

    • Name : reference display name

    • Host : LDAP server host name

    • Port : connection port to the LDAP server

    • Account : DN of the connection account to LDAP (please leave it blank if the directory authorizes anonymous read access)

    • Password : password of the connection account

    • Base DN : Basic DN used for user search in the directory

    • LDAP screen : User search screen in the directory (optional)

    • LDAP features :

      • Identifier : LDAP feature name used as user identifier (e.g.: uid)

      • First name : LDAP feature name including the user’s first name (ex: givenName)

      • Last name : LDAP feature name including the user’s last name (ex: familyName)

      • E-mail : LDAP feature name including the user’s e-mail address (ex: mail)

    The features" First name ", " Last name " and " E-mail " are not used except when the accounts are created on the fly.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch01s11.html b/issue_relations/public/manual/en/ch01s11.html new file mode 100644 index 000000000..8fc9ef8d0 --- /dev/null +++ b/issue_relations/public/manual/en/ch01s11.html @@ -0,0 +1,3 @@ + + + 11. Informationredmine

    11. Information

    Displays application and environment information.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02.html b/issue_relations/public/manual/en/ch02.html new file mode 100644 index 000000000..b0bb8dbdc --- /dev/null +++ b/issue_relations/public/manual/en/ch02.html @@ -0,0 +1,3 @@ + + + Chapter 2. Projectsredmine \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s01.html b/issue_relations/public/manual/en/ch02s01.html new file mode 100644 index 000000000..677f6fc47 --- /dev/null +++ b/issue_relations/public/manual/en/ch02s01.html @@ -0,0 +1,3 @@ + + + 1. Project overviewredmine

    1. Project overview

    The overview presents the general project information, its main members, the latest announcements, as well as an synthesis of Issue requests open by tracker.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s02.html b/issue_relations/public/manual/en/ch02s02.html new file mode 100644 index 000000000..ef7964d74 --- /dev/null +++ b/issue_relations/public/manual/en/ch02s02.html @@ -0,0 +1,3 @@ + + + 2. Planningredmine

    2. Planning

    2.1. Project calendar

    Project calendar shows the tasks that begin or end during the selected month (current month by default). An issue will be displayed as a task if its start date and its due date are specified.

    • This symbol represents a task that begins

    • This symbol represents a task that ends

    • Ths symbol represents a task that begins and ends the same day

    2.2. Gantt chart

    This diagramme shows tasks and their achievement rate.

    Achievement is represented in blue. Delay in red.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s03.html b/issue_relations/public/manual/en/ch02s03.html new file mode 100644 index 000000000..afb0c070f --- /dev/null +++ b/issue_relations/public/manual/en/ch02s03.html @@ -0,0 +1,3 @@ + + + 3. Issue managementredmine

    3. Issue management

    3.1. Issue list

    By default, the entire list of the project open Issues are displayed. Various screens allow you to select the Issues to be displayed. If the project has sub-projects, you have the possibility to display the sub-project's Issues as well (not displayed by default).

    Once applied, a screen is valid during the entire session. You can re-define it or delete it by clicking Cancel.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s04.html b/issue_relations/public/manual/en/ch02s04.html new file mode 100644 index 000000000..c8b8c73d3 --- /dev/null +++ b/issue_relations/public/manual/en/ch02s04.html @@ -0,0 +1,3 @@ + + + 4. Reportsredmine

    4. Reports

    This screen presents the number of Issues and Issue status synthesis according to various criteria (tracker, priority, category). Direct links allow for access to the detailed Issue list for each criterion.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s05.html b/issue_relations/public/manual/en/ch02s05.html new file mode 100644 index 000000000..444eaa623 --- /dev/null +++ b/issue_relations/public/manual/en/ch02s05.html @@ -0,0 +1,3 @@ + + + 5. Change logredmine

    5. Change log

    This page presents the entire list of the resolved Issues for each version of the project. Certain types of Issues can be excluded from this display.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s06.html b/issue_relations/public/manual/en/ch02s06.html new file mode 100644 index 000000000..743326d87 --- /dev/null +++ b/issue_relations/public/manual/en/ch02s06.html @@ -0,0 +1,3 @@ + + + 6. Newsredmine

    6. News

    Allows you to inform users on project activity.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s07.html b/issue_relations/public/manual/en/ch02s07.html new file mode 100644 index 000000000..6c23ad5ef --- /dev/null +++ b/issue_relations/public/manual/en/ch02s07.html @@ -0,0 +1,3 @@ + + + 7. Documentsredmine

    7. Documents

    Documents are grouped by categories (see Value Lists). A document can contain several files (for example: revisions or successive versions).

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s08.html b/issue_relations/public/manual/en/ch02s08.html new file mode 100644 index 000000000..aecc3355c --- /dev/null +++ b/issue_relations/public/manual/en/ch02s08.html @@ -0,0 +1,3 @@ + + + 8. Filesredmine

    8. Files

    This module allows you to display various folders (sources, binaires, ...) for each version of the application.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch02s09.html b/issue_relations/public/manual/en/ch02s09.html new file mode 100644 index 000000000..f245737c5 --- /dev/null +++ b/issue_relations/public/manual/en/ch02s09.html @@ -0,0 +1,3 @@ + + + 9. Settingsredmine

    9. Settings

    9.1. Project features

    • Public : if it’s a public project, it can be viewed (request consultation, documents consultation, ...) for all the users, including those who are not project members. If it’s not a public project, only the project members have access to it, according to their role.

    • Customized fields : Select the customized fields that you want to use. Only the administrator can define new customized fields.

    9.2. Members

    This screen allows you to define the project members as well as their corresponding roles. A user can have only one role in a given project. The role of a member determines the permissions they have in a project.

    9.3. Versions

    Versions allow you to follow the changes made during all the project. For instance, at the close of an Issue, you can indicate which version takes it into account. You can display the various versions of the application (see Files).

    9.4. Request categories

    Issue categories allow you to organize Issues. Categories can correspond to different project modules.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch03.html b/issue_relations/public/manual/en/ch03.html new file mode 100644 index 000000000..f439ffefb --- /dev/null +++ b/issue_relations/public/manual/en/ch03.html @@ -0,0 +1,3 @@ + + + Chapter 3. User accountsredmine \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch03s01.html b/issue_relations/public/manual/en/ch03s01.html new file mode 100644 index 000000000..28e6d07bc --- /dev/null +++ b/issue_relations/public/manual/en/ch03s01.html @@ -0,0 +1,3 @@ + + + 1. My accountredmine

    1. My account

    1.1. Information

    This screen allows you to modify your account: lastname, firstname, email address, language.

    If Mail notifications is unchecked, no email will be sent to you.

    1.2. Password

    To change your password, type your old password and your new password (twice). Password length must be between 4 and 12 characters.

    If your account uses an external authentication (LDAP), you can't change your password in redMine.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch03s02.html b/issue_relations/public/manual/en/ch03s02.html new file mode 100644 index 000000000..ccbf08488 --- /dev/null +++ b/issue_relations/public/manual/en/ch03s02.html @@ -0,0 +1,3 @@ + + + 2. My pageredmine

    2. My page

    This page allows you to display various information about your projects.

    To personalize your page, click on Personalize this page. Then you can choose which information to display and where it is displayed.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch03s03.html b/issue_relations/public/manual/en/ch03s03.html new file mode 100644 index 000000000..2283aceeb --- /dev/null +++ b/issue_relations/public/manual/en/ch03s03.html @@ -0,0 +1,3 @@ + + + 3. Password lostredmine

    3. Password lost

    If you loose your forget, a procedure allows you to choose a new one.

    On the login screen, click on Lost password. Type your email address and submit the form. An email is then sent to you. It contains a link that allows you to change your password.

    If your account uses an external authentication (LDAP), this procedure isn't be available.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/ch03s04.html b/issue_relations/public/manual/en/ch03s04.html new file mode 100644 index 000000000..a09010ec5 --- /dev/null +++ b/issue_relations/public/manual/en/ch03s04.html @@ -0,0 +1,3 @@ + + + 4. Registerredmine

    4. Register

    By registering, you can get an account without the intervention of the administrator.

    On the login screen, click on Register. Fill the form and submit it. An email will be sent to you. To activate your account, use the link that is contained in this mail.

    The possibility to register can be desactived in the application configuration.

    \ No newline at end of file diff --git a/issue_relations/public/manual/en/html.css b/issue_relations/public/manual/en/html.css new file mode 100644 index 000000000..c1b2a6fca --- /dev/null +++ b/issue_relations/public/manual/en/html.css @@ -0,0 +1,55 @@ +body { + background: #FFFFFF; + font: 0.8em Verdana,Tahoma,Arial,sans-serif; +} + +h1, h2, h3, h4, h5 { + color: #800000; + font-family: sans-serif; +} + +table { + font-size: 1em; +} + +a{ +color:#467aa7; +font-weight:bold; +text-decoration:none; +background-color:inherit; +} + +a:hover{ + color: #800000; + text-decoration:underline; + background-color:inherit; +} + +a img{border:none;} + +.screenshot { + text-align: center; +} + +.guilabel { + font-weight: bold; +} + +span.term { + font-weight: bold; +} + +div.sidebar { + background: #F0F0F0; + border: 1px solid gray; + padding: 5px; + margin: 20px; +} + +pre.programlisting { + background: #F0F0F0; + border: 1px solid gray; + padding: 2px; + font-size: 10pt; + white-space: pre; +} diff --git a/issue_relations/public/manual/en/index.html b/issue_relations/public/manual/en/index.html new file mode 100644 index 000000000..4ff6a0913 --- /dev/null +++ b/issue_relations/public/manual/en/index.html @@ -0,0 +1,3 @@ + + + Documentationredmine \ No newline at end of file diff --git a/issue_relations/public/manual/en/resources/arrow_bw.png b/issue_relations/public/manual/en/resources/arrow_bw.png new file mode 100644 index 000000000..c50fbce8e Binary files /dev/null and b/issue_relations/public/manual/en/resources/arrow_bw.png differ diff --git a/issue_relations/public/manual/en/resources/arrow_from.png b/issue_relations/public/manual/en/resources/arrow_from.png new file mode 100644 index 000000000..f1e1b6c31 Binary files /dev/null and b/issue_relations/public/manual/en/resources/arrow_from.png differ diff --git a/issue_relations/public/manual/en/resources/arrow_to.png b/issue_relations/public/manual/en/resources/arrow_to.png new file mode 100644 index 000000000..faef20c78 Binary files /dev/null and b/issue_relations/public/manual/en/resources/arrow_to.png differ diff --git a/issue_relations/public/manual/en/resources/gantt.png b/issue_relations/public/manual/en/resources/gantt.png new file mode 100644 index 000000000..65c33d685 Binary files /dev/null and b/issue_relations/public/manual/en/resources/gantt.png differ diff --git a/issue_relations/public/manual/en/resources/issues_list.png b/issue_relations/public/manual/en/resources/issues_list.png new file mode 100644 index 000000000..520de14bf Binary files /dev/null and b/issue_relations/public/manual/en/resources/issues_list.png differ diff --git a/issue_relations/public/manual/en/resources/locked.png b/issue_relations/public/manual/en/resources/locked.png new file mode 100644 index 000000000..5199dfe22 Binary files /dev/null and b/issue_relations/public/manual/en/resources/locked.png differ diff --git a/issue_relations/public/manual/en/resources/user_new.png b/issue_relations/public/manual/en/resources/user_new.png new file mode 100644 index 000000000..0c220257e Binary files /dev/null and b/issue_relations/public/manual/en/resources/user_new.png differ diff --git a/issue_relations/public/manual/en/resources/users_list.png b/issue_relations/public/manual/en/resources/users_list.png new file mode 100644 index 000000000..74d9c9a5a Binary files /dev/null and b/issue_relations/public/manual/en/resources/users_list.png differ diff --git a/issue_relations/public/manual/en/resources/workflow.png b/issue_relations/public/manual/en/resources/workflow.png new file mode 100644 index 000000000..04e79d61e Binary files /dev/null and b/issue_relations/public/manual/en/resources/workflow.png differ diff --git a/issue_relations/public/manual/fr/ch01.html b/issue_relations/public/manual/fr/ch01.html new file mode 100644 index 000000000..1be9d81de --- /dev/null +++ b/issue_relations/public/manual/fr/ch01.html @@ -0,0 +1,3 @@ + + + Chapter 1. Administrationredmine \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s01.html b/issue_relations/public/manual/fr/ch01s01.html new file mode 100644 index 000000000..2b1a7c8d9 --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s01.html @@ -0,0 +1,3 @@ + + + 1. Projetsredmine

    1. Projets

    Ces écrans vous permettent de gérer les projets.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s02.html b/issue_relations/public/manual/fr/ch01s02.html new file mode 100644 index 000000000..a819e6cfa --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s02.html @@ -0,0 +1,3 @@ + + + 2. Utilisateursredmine

    2. Utilisateurs

    Ces écrans vous permettent de gérer les utilisateurs de l'application.

    2.1. Liste des utilisateurs

    Statut des comptes:

    • L'icône signifie que le compte est vérouillé. Un utilisateur dont le compte est vérouillé ne peut plus s'identifier pour accéder à l'application.

    • L'icône signifie que le compte n'a pas encore été activé par l'utilisateur .

    Les boutons Vérouiller/Dévérouiller vous permettent de bloquer ou débloquer les comptes utilisateurs.

    2.2. Création ou modification d'un utilisateur

    En mode modification, laissez le champ Mot de passe vide pour laisser le mot de passe de l'utilisateur inchangé.

    Un utilisateur déclaré comme administrateur dispose de toutes les permissions sur l'application et sur tous les projets.

    • Administrateur: déclare l'utilisateur comme administrateur de l'application.

    • Notifications par mail: permet d'activer ou non l'envoi automatique de notifications par mail pour cet utilisateur

    • Vérouillé: désactive le compte de l'utilisateur

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s03.html b/issue_relations/public/manual/fr/ch01s03.html new file mode 100644 index 000000000..d42073864 --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s03.html @@ -0,0 +1,3 @@ + + + 3. Rôles et permissionsredmine

    3. Rôles et permissions

    Les rôles permettent de définir les permissions des différents membres d'un projet. Chaque membre d'un projet dispose d'un rôle unique au sein d'un projet. Un utilisateur peut avoir différents rôles au sein de différents projets.

    Sur l'écran d'édition du rôle, cochez les actions que vous souhaitez autoriser pour le rôle.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s04.html b/issue_relations/public/manual/fr/ch01s04.html new file mode 100644 index 000000000..9811cb6ef --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s04.html @@ -0,0 +1,3 @@ + + + 4. Trackersredmine

    4. Trackers

    Les trackers permettent de typer les demandes et de définir des workflows spécifiques pour chacun de ces types.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s05.html b/issue_relations/public/manual/fr/ch01s05.html new file mode 100644 index 000000000..3d5d3de32 --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s05.html @@ -0,0 +1,3 @@ + + + 5. Champs personnalisésredmine

    5. Champs personnalisés

    Les champs personnalisés vous permettent d'ajouter des informations supplémentaires sur les projets, les demandes ou les utilisateurs. Un champ personnalisé peut être de l'un des types suivants:

    • Entier: entier positif ou négatif

    • Texte: chaîne de caractères

    • Texte long: chaîne de caractères, avec champ à plusieurs lignes

    • Date: date

    • Booléen: booléen (case à cocher)

    • Liste: valeur à sélectionnée parmi une liste prédéfinie (liste déroulante)

    Des éléments de validation peuvent être définis:

    • Obligatoire: champ dont la saisie est obligatoire sur les demandes

    • Pour tous les projects: champ automatiquement associé à l'ensemble des projets

    • Min - max length: longueurs minimales et maximales pour les champs en saisie libre (0 signifie qu'il n'y a pas de restriction)

    • Expression régulière: expression régulière permettant de valider la valeur saisie

      Exemples:

      ^\[A-Z]{4}\d+$ : 4 lettres majuscules suivies d'un ou plusieurs chiffres

      ^[^0-9]*$ : chaîne ne comportant pas de chiffres

    • Valeurs possibles: valeurs possibles pour les champs de type "Liste". Les valeurs sont séparées par le caractère |

    5.1. Champs pour les projets

    • Obligatoire: champ dont la saisie est obligatoire

    5.2. Champs pour les demandes

    • Pour tous les projects: champ automatiquement associé aux demandes de l'ensemble des projets

      Si cette option n'est pas activée, chaque projet pourra choisir d'utiliser ou non le champ pour ses demandes (voir configuration du projet).

    5.3. Champs pour les utilisateurs

    • Obligatoire: champ dont la saisie est obligatoire

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s06.html b/issue_relations/public/manual/fr/ch01s06.html new file mode 100644 index 000000000..85a279bba --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s06.html @@ -0,0 +1,3 @@ + + + 6. Statut des demandesredmine

    6. Statut des demandes

    Ces écrans vous permettent de définir les différents statuts possibles des demandes.

    • Demande fermée: indique que le statut correspond à une demande considérée comme fermée

    • Statut par défaut: statut appliqué par défaut aux nouvelles demandes (seul un statut peut être déclaré comme statut par défaut)

    • Couleur: code couleur HTML (6 caractères) représentant le statut à l'affichage

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s07.html b/issue_relations/public/manual/fr/ch01s07.html new file mode 100644 index 000000000..b2e4cf08f --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s07.html @@ -0,0 +1,3 @@ + + + 7. Workflowredmine

    7. Workflow

    Le workflow permet de définir les changements que les différents membres d'un projet sont autorisés à effectuer sur les demandes, en fonction de leur type.

    Sélectionnez le rôle et le tracker pour lesquels vous souhaitez modifier le workflow, puis cliquez sur Edit. L'écran vous permet alors de modifier, pour le rôle et le tracker choisi, les changements autorisés. Les lignes représentent les statuts initiaux des demandes. Les colonnes représentent les statuts autorisés à être appliqués.

    Remarque: pour qu'un rôle puisse changer le statut des demandes, la permission doit lui être explicitement donnée indépendemment de la configuration du workflow.

    Dans l'exemple ci-dessus, les demandes de type Bug au statut Nouveau pourront être passées au statut Assignée ou Résolue par le rôle Développeur. Celles au statut Assignée pourront être passées au statut Résolue. Le statut de toutes les autres demandes de type Bug ne pourra pas être modifié par le Développeur.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s08.html b/issue_relations/public/manual/fr/ch01s08.html new file mode 100644 index 000000000..7dd021d0e --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s08.html @@ -0,0 +1,3 @@ + + + 8. Listes de valeursredmine

    8. Listes de valeurs

    Les listes de valeurs utilisées par l'application (exemple: les priorités des demandes) peuvent être personnalisées. Cet écran vous permet de définir les valeurs possibles pour chacune des listes suivantes:

    • Priorités des demandes

    • Catégories de documents

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s09.html b/issue_relations/public/manual/fr/ch01s09.html new file mode 100644 index 000000000..1cbecace3 --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s09.html @@ -0,0 +1,3 @@ + + + 9. Notifications par mailredmine

    9. Notifications par mail

    Cet écran vous permet de sélectionner les actions qui donneront lieu à une notification par mail aux membres du projet.

    Remarque: l'envoi de mails doit être activé dans la configuration de l'application si souhaitez effectuer des notifications.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s10.html b/issue_relations/public/manual/fr/ch01s10.html new file mode 100644 index 000000000..22a2f050d --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s10.html @@ -0,0 +1,3 @@ + + + 10. Authentificationredmine

    10. Authentification

    Par défaut, redMine s'appuie sur sa propre base de données pour authentifier les utilisateurs, à l'aide d'un mot de passe spécifique.

    Si vous disposez déjà d'un ou plusieurs référentiels externes d'utilisateurs (annuaires LDAP), vous pouvez les déclarer afin qu'ils soient utilisés pour l'authentification sur redMine. Cela permet aux utilisateurs d'accéder à redMine avec leurs identifiants et mots de passe habituels.

    Pour chaque référentiel déclaré, vous pouvez spécifier si les comptes peuvent être créés à la volée dans redMine. Si c'est le cas, les comptes utilisateurs sont automatiquement créés à la première connexion de l'utilisateur (sans droits spécifiques sur les projets), à partir des informations disponibles dans le référentiel. Sinon, l'administrateur doit au préalable créer le compte de l'utilisateur dans redMine.

    10.1. Annuaire LDAP

    • Nom: nom d'affichage du référentiel

    • Hôte: nom d'hôte du serveur LDAP

    • Port: port de connexion au serveur LDAP

    • Compte: DN du compte de connexion au LDAP (laisser vide si l'annuaire autorise l'accès anonyme en lecture)

    • Mot de passe: mot de passe du compte de connexion

    • Base DN: DN de base utilisé pour la recherche des utilisateur dans l'annuaire

    • Filtre LDAP: Filtre de recherche des utilisateurs dans l'annuaire (optionnel)

    • Attributs LDAP:

      • Identifiant: nom de l'attribut LDAP utilisé comme identifiant de l'utilisateur (ex: uid)

      • Prénom: nom de l'attribut LDAP contenant le prénom de l'utilisateur (ex: givenName)

      • Nom: nom de l'attribut LDAP contenant le nom de l'utilisateur (ex: sn)

      • Email: nom de l'attribut LDAP contenant l'adresse mail de l'utilisateur (ex: mail)

    Les attributs "Prénom", "Nom" et "Email" ne sont utilisés que lorsque les comptes sont créés à la volée.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch01s11.html b/issue_relations/public/manual/fr/ch01s11.html new file mode 100644 index 000000000..0deef3736 --- /dev/null +++ b/issue_relations/public/manual/fr/ch01s11.html @@ -0,0 +1,3 @@ + + + 11. Informationsredmine

    11. Informations

    Affiche des informations relatives à l'application et à son environnement.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02.html b/issue_relations/public/manual/fr/ch02.html new file mode 100644 index 000000000..0d04c02dc --- /dev/null +++ b/issue_relations/public/manual/fr/ch02.html @@ -0,0 +1,3 @@ + + + Chapter 2. Projetsredmine \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s01.html b/issue_relations/public/manual/fr/ch02s01.html new file mode 100644 index 000000000..3a3ce7078 --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s01.html @@ -0,0 +1,3 @@ + + + 1. Aperçu du projetredmine

    1. Aperçu du projet

    L'aperçu vous présente les informations générales relatives au projet, les principaux membres, les dernières annonces, ainsi qu'une synthèse du nombre de demandes ouvertes par tracker.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s02.html b/issue_relations/public/manual/fr/ch02s02.html new file mode 100644 index 000000000..ee9d1beff --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s02.html @@ -0,0 +1,3 @@ + + + 2. Planningredmine

    2. Planning

    2.1. Calendrier

    Le calendrier présente les tâches qui commencent ou se terminent au cours du mois sélectionné (mois en cours par défaut). Les tâches correspondent aux demandes pour lesquelles la date de début et d'échéance sont renseignées.

    • Le symoble représente le début d'une tâche

    • Le symbole représente la fin d'une tâche

    • Le symbole représente une tâche qui débute et se termine le jour même

    2.2. Diagramme de Gantt

    Le diagramme de Gantt représente pour la période choisie l'ensemble des tâches et leurs taux d'avancement.

    L'avancement est représenté en bleu. Le retard en rouge.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s03.html b/issue_relations/public/manual/fr/ch02s03.html new file mode 100644 index 000000000..b7f008381 --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s03.html @@ -0,0 +1,3 @@ + + + 3. Gestion des demandesredmine

    3. Gestion des demandes

    3.1. Liste des demandes

    Par défaut, l'ensemble des demandes ouvertes du projet sont affichées. Différents filtres vous permettent de sélectionner les demandes à afficher.

    Une fois appliqué, un filtre reste valable durant toute votre session. Vous pouvez le redéfinir, ou le supprimer en cliquant sur Effacer.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s04.html b/issue_relations/public/manual/fr/ch02s04.html new file mode 100644 index 000000000..fe7bcae92 --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s04.html @@ -0,0 +1,3 @@ + + + 4. Rapportsredmine

    4. Rapports

    Cet écran présente la synthèse du nombre de demandes par statut et selon différents critères (tracker, priorité, catégorie). Des liens directs permettent d'accéder à la liste détaillée des demandes pour chaque critère.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s05.html b/issue_relations/public/manual/fr/ch02s05.html new file mode 100644 index 000000000..f74e5e0df --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s05.html @@ -0,0 +1,3 @@ + + + 5. Historiqueredmine

    5. Historique

    Cette page présente l'ensemble des demandes résolues dans chacune des versions du projet. Certains types de demande peuvent être exclus de cet affichage.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s06.html b/issue_relations/public/manual/fr/ch02s06.html new file mode 100644 index 000000000..83590223d --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s06.html @@ -0,0 +1,3 @@ + + + 6. Annoncesredmine

    6. Annonces

    Les nouvelles vous permettent d'informer les utilisateurs sur l'activité du projet.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s07.html b/issue_relations/public/manual/fr/ch02s07.html new file mode 100644 index 000000000..81e530eb2 --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s07.html @@ -0,0 +1,3 @@ + + + 7. Documentsredmine

    7. Documents

    Les documents sont groupés par catégories (voir Listes de valeurs). Un document peut contenir plusieurs fichiers (exemple: révisions ou versions successives).

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s08.html b/issue_relations/public/manual/fr/ch02s08.html new file mode 100644 index 000000000..ab69a77e0 --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s08.html @@ -0,0 +1,3 @@ + + + 8. Fichiersredmine

    8. Fichiers

    Ce module vous permet de publier les différents fichiers (sources, binaires, ...) pour chaque version de l'application.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch02s09.html b/issue_relations/public/manual/fr/ch02s09.html new file mode 100644 index 000000000..6ca84500f --- /dev/null +++ b/issue_relations/public/manual/fr/ch02s09.html @@ -0,0 +1,3 @@ + + + 9. Configuration du projetredmine

    9. Configuration du projet

    9.1. Propriétés du projet

    • Public: si le projet est public, il sera visible (consultation des demandes, des documents, ...) pour l'ensemble des utilisateurs, y compris ceux qui ne sont pas membres du projet. Si le projet n'est pas public, seuls les membres du projet y ont accès, en fonction de leur rôle.

    • Champs personnalisés: sélectionner les champs personnalisésMo que vous souhaitez utiliser pour les demandes du projet. Seul l'administrateur peut définir de nouveaux champs personnalisés.

    9.2. Membres

    Cett section vous permet de définir les membres du projet ainsi que leurs rôles respectifs. Un utilisateur ne peut avoir qu'un rôle au sein d'un projet donné. Le rôle d'un membre détermine les permissions dont il bénéficie sur le projet.

    9.3. Versions

    Les versions vous permettent de suivre les changements survenus tout au long du projet. A la fermeture d'une demande, vous pouvez par exemple indiquer quelle version la prend en compte. Vous pouvez par ailleurs publier les différentes versions de l'application (voir Fichiers).

    9.4. Catégories des demandes

    Les catégories de demande vous permettent de typer les demandes. Les catégories peuvent par exemple correspondre aux différents modules du projet.

    Une catégorie référencée sur des demandes ne peut pas être supprimée.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch03.html b/issue_relations/public/manual/fr/ch03.html new file mode 100644 index 000000000..b5b607a6b --- /dev/null +++ b/issue_relations/public/manual/fr/ch03.html @@ -0,0 +1,3 @@ + + + Chapter 3. Comptes utilisateursredmine \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch03s01.html b/issue_relations/public/manual/fr/ch03s01.html new file mode 100644 index 000000000..c44305f71 --- /dev/null +++ b/issue_relations/public/manual/fr/ch03s01.html @@ -0,0 +1,3 @@ + + + 1. Mon compteredmine

    1. Mon compte

    1.1. Informations

    Cet écran vous permet de modifier les informations relatives à votre compte: nom, prénom, adresse mail, langue (Anglais, Allemand, Espagnol ou Français).

    Si la case Notifications par mail est décochée, aucune notification par mail ne vous sera envoyée.

    1.2. Changement de mot de passe

    Pour changer votre mot de passe, saisissez votre mot de passe actuel et le nouveau mot de passe souhaité (double saisie pour confirmation). Le mot de passe doit avoir une longueur comprise entre 4 et 12 caractères.

    Si votre compte utilise une authentification externe (un annuaire LDAP), vous ne pouvez pas changer votre mot de passe dans redMine.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch03s02.html b/issue_relations/public/manual/fr/ch03s02.html new file mode 100644 index 000000000..66131e25a --- /dev/null +++ b/issue_relations/public/manual/fr/ch03s02.html @@ -0,0 +1,3 @@ + + + 2. Ma pageredmine

    2. Ma page

    Cette page vous permet d'afficher de manière synthétique des informations sur vos projets.

    Pour personnaliser votre page, cliquer sur le lien Personnaliser cette page. Vous pouvez alors sélectionner les informations à afficher et les positionner où vous le souhaitez sur la page, par glisser-déposer.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch03s03.html b/issue_relations/public/manual/fr/ch03s03.html new file mode 100644 index 000000000..c0efff03a --- /dev/null +++ b/issue_relations/public/manual/fr/ch03s03.html @@ -0,0 +1,3 @@ + + + 3. Mot de passe perduredmine

    3. Mot de passe perdu

    En cas de perte de votre mot de passe, une procédure par mail vous permet d'en choisir un nouveau.

    Sur l'écran d'authentification, cliquez sur Mot de passe perdu. Saisissez ensuite votre adresse mail dans le champ prévu et validez. Un mail vous est alors transmis. Il contient un lien qui vous permettra d'accéder à la page de choix du nouveau mot de passe.

    Si votre compte utilise une authentification externe (ex: un annuaire LDAP), cette procédure n'est pas disponible.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/ch03s04.html b/issue_relations/public/manual/fr/ch03s04.html new file mode 100644 index 000000000..ba0a15606 --- /dev/null +++ b/issue_relations/public/manual/fr/ch03s04.html @@ -0,0 +1,3 @@ + + + 4. S'enregistrerredmine

    4. S'enregistrer

    L'enregistrement vous permet d'obtenir un compte sans intervention de l'administrateur.

    Sur l'écran d'authentification, cliquez sur S'enregistrer. Complétez le formulaire d'inscription et validez. Un mail vous est alors transmis. Pour activer votre compte, utilisez le lien présent dans le mail qui vous a été envoyé.

    La possibilité de s'enregistrer peut avoir été désactivée par l'administrateur.

    \ No newline at end of file diff --git a/issue_relations/public/manual/fr/html.css b/issue_relations/public/manual/fr/html.css new file mode 100644 index 000000000..c1b2a6fca --- /dev/null +++ b/issue_relations/public/manual/fr/html.css @@ -0,0 +1,55 @@ +body { + background: #FFFFFF; + font: 0.8em Verdana,Tahoma,Arial,sans-serif; +} + +h1, h2, h3, h4, h5 { + color: #800000; + font-family: sans-serif; +} + +table { + font-size: 1em; +} + +a{ +color:#467aa7; +font-weight:bold; +text-decoration:none; +background-color:inherit; +} + +a:hover{ + color: #800000; + text-decoration:underline; + background-color:inherit; +} + +a img{border:none;} + +.screenshot { + text-align: center; +} + +.guilabel { + font-weight: bold; +} + +span.term { + font-weight: bold; +} + +div.sidebar { + background: #F0F0F0; + border: 1px solid gray; + padding: 5px; + margin: 20px; +} + +pre.programlisting { + background: #F0F0F0; + border: 1px solid gray; + padding: 2px; + font-size: 10pt; + white-space: pre; +} diff --git a/issue_relations/public/manual/fr/index.html b/issue_relations/public/manual/fr/index.html new file mode 100644 index 000000000..3f55c39b9 --- /dev/null +++ b/issue_relations/public/manual/fr/index.html @@ -0,0 +1,3 @@ + + + Documentationredmine \ No newline at end of file diff --git a/issue_relations/public/manual/fr/resources/arrow_bw.png b/issue_relations/public/manual/fr/resources/arrow_bw.png new file mode 100644 index 000000000..c50fbce8e Binary files /dev/null and b/issue_relations/public/manual/fr/resources/arrow_bw.png differ diff --git a/issue_relations/public/manual/fr/resources/arrow_from.png b/issue_relations/public/manual/fr/resources/arrow_from.png new file mode 100644 index 000000000..f1e1b6c31 Binary files /dev/null and b/issue_relations/public/manual/fr/resources/arrow_from.png differ diff --git a/issue_relations/public/manual/fr/resources/arrow_to.png b/issue_relations/public/manual/fr/resources/arrow_to.png new file mode 100644 index 000000000..faef20c78 Binary files /dev/null and b/issue_relations/public/manual/fr/resources/arrow_to.png differ diff --git a/issue_relations/public/manual/fr/resources/gantt.png b/issue_relations/public/manual/fr/resources/gantt.png new file mode 100644 index 000000000..65c33d685 Binary files /dev/null and b/issue_relations/public/manual/fr/resources/gantt.png differ diff --git a/issue_relations/public/manual/fr/resources/issues_list.png b/issue_relations/public/manual/fr/resources/issues_list.png new file mode 100644 index 000000000..520de14bf Binary files /dev/null and b/issue_relations/public/manual/fr/resources/issues_list.png differ diff --git a/issue_relations/public/manual/fr/resources/locked.png b/issue_relations/public/manual/fr/resources/locked.png new file mode 100644 index 000000000..5199dfe22 Binary files /dev/null and b/issue_relations/public/manual/fr/resources/locked.png differ diff --git a/issue_relations/public/manual/fr/resources/roles_edit.png b/issue_relations/public/manual/fr/resources/roles_edit.png new file mode 100644 index 000000000..a1ed2755b Binary files /dev/null and b/issue_relations/public/manual/fr/resources/roles_edit.png differ diff --git a/issue_relations/public/manual/fr/resources/user_new.png b/issue_relations/public/manual/fr/resources/user_new.png new file mode 100644 index 000000000..0c220257e Binary files /dev/null and b/issue_relations/public/manual/fr/resources/user_new.png differ diff --git a/issue_relations/public/manual/fr/resources/users_list.png b/issue_relations/public/manual/fr/resources/users_list.png new file mode 100644 index 000000000..74d9c9a5a Binary files /dev/null and b/issue_relations/public/manual/fr/resources/users_list.png differ diff --git a/issue_relations/public/manual/fr/resources/workflow.png b/issue_relations/public/manual/fr/resources/workflow.png new file mode 100644 index 000000000..04e79d61e Binary files /dev/null and b/issue_relations/public/manual/fr/resources/workflow.png differ diff --git a/issue_relations/public/manual/redmine.png b/issue_relations/public/manual/redmine.png new file mode 100644 index 000000000..b62f9732e Binary files /dev/null and b/issue_relations/public/manual/redmine.png differ diff --git a/issue_relations/public/robots.txt b/issue_relations/public/robots.txt new file mode 100644 index 000000000..4ab9e89fe --- /dev/null +++ b/issue_relations/public/robots.txt @@ -0,0 +1 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file \ No newline at end of file diff --git a/issue_relations/public/stylesheets/application.css b/issue_relations/public/stylesheets/application.css new file mode 100644 index 000000000..1e78b3321 --- /dev/null +++ b/issue_relations/public/stylesheets/application.css @@ -0,0 +1,604 @@ +/* andreas08 - an open source xhtml/css website layout by Andreas Viklund - http://andreasviklund.com . Free to use in any way and for any purpose as long as the proper credits are given to the original designer. Version: 1.0, November 28, 2005 */ +/* Edited by Jean-Philippe Lang *> +/**************** Body and tag styles ****************/ + + +#header * {margin:0; padding:0;} +p, ul, ol, li {margin:0; padding:0;} + + +body{ +font:76% Verdana,Tahoma,Arial,sans-serif; +line-height:1.4em; +text-align:center; +color:#303030; +background:#e8eaec; +margin:0; +} + + +a{ +color:#467aa7; +font-weight:bold; +text-decoration:none; +background-color:inherit; +} + +a:hover{color:#2a5a8a; text-decoration:none; background-color:inherit;} +a img{border:none;} + +p{margin:0 0 1em 0;} +p form{margin-top:0; margin-bottom:20px;} + +img.left,img.center,img.right{padding:4px; border:1px solid #a0a0a0;} +img.left{float:left; margin:0 12px 5px 0;} +img.center{display:block; margin:0 auto 5px auto;} +img.right{float:right; margin:0 0 5px 12px;} + +/**************** Header and navigation styles ****************/ + +#container{ +width:100%; +min-width: 800px; +margin:0; +padding:0; +text-align:left; +background:#ffffff; +color:#303030; +} + +#header{ +height:4.5em; +margin:0; +background:#467aa7; +color:#ffffff; +margin-bottom:1px; +} + +#header h1{ +padding:10px 0 0 20px; +font-size:2em; +background-color:inherit; +color:#fff; +letter-spacing:-1px; +font-weight:bold; +font-family: Trebuchet MS,Georgia,"Times New Roman",serif; +} + +#header h2{ +margin:3px 0 0 40px; +font-size:1.5em; +background-color:inherit; +color:#f0f2f4; +letter-spacing:-1px; +font-weight:normal; +font-family: Trebuchet MS,Georgia,"Times New Roman",serif; +} + +#navigation{ +height:2.2em; +line-height:2.2em; +margin:0; +background:#578bb8; +color:#ffffff; +} + +#navigation li{ +float:left; +list-style-type:none; +border-right:1px solid #ffffff; +white-space:nowrap; +} + +#navigation li.right { + float:right; +list-style-type:none; +border-right:0; +border-left:1px solid #ffffff; +white-space:nowrap; +} + +#navigation li a{ +display:block; +padding:0px 10px 0px 22px; +font-size:0.8em; +font-weight:normal; +text-decoration:none; +background-color:inherit; +color: #ffffff; +} + +#navigation li.submenu { +background:url(../images/arrow_down.png) 96% 80% no-repeat; +} + +#navigation li.submenu a { +padding:0px 16px 0px 22px; +} + +* html #navigation a {width:1%;} + +#navigation .selected,#navigation a:hover{ +color:#ffffff; +text-decoration:none; +background-color: #80b0da; +} + +/**************** Icons *******************/ +.icon { +background-position: 0% 40%; +background-repeat: no-repeat; +padding-left: 20px; +padding-top: 2px; +padding-bottom: 3px; +vertical-align: middle; +} + +#navigation .icon { +background-position: 4px 50%; +} + +.icon22 { +background-position: 0% 40%; +background-repeat: no-repeat; +padding-left: 24px; +line-height: 22px; +vertical-align: middle; +} + +.icon-add { background-image: url(../images/add.png); } +.icon-edit { background-image: url(../images/edit.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-pdf { background-image: url(../images/pdf.png); } +.icon-csv { background-image: url(../images/csv.png); } +.icon-file { background-image: url(../images/file.png); } +.icon-folder { background-image: url(../images/folder.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); } + +.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); } + +/**************** Content styles ****************/ + +html>body #content { +height: auto; +min-height: 500px; +} + +#content{ +width: auto; +height:500px; +font-size:0.9em; +padding:20px 10px 10px 20px; +margin-left: 120px; +border-left: 1px dashed #c0c0c0; + +} + +#content h2{ +display:block; +margin:0 0 16px 0; +font-size:1.7em; +font-weight:normal; +letter-spacing:-1px; +color:#606060; +background-color:inherit; +font-family: Trebuchet MS,Georgia,"Times New Roman",serif; +} + +#content h2 a{font-weight:normal;} +#content h3{margin:0 0 12px 0; font-size:1.4em;color:#707070;font-family: Trebuchet MS,Georgia,"Times New Roman",serif;} +#content h4{font-size: 1em; margin-bottom: 12px; margin-top: 20px; font-weight: normal; border-bottom: dotted 1px #c0c0c0;} +#content a:hover,#subcontent a:hover{text-decoration:underline;} +#content ul,#content ol{margin:0 5px 16px 35px;} +#content dl{margin:0 5px 10px 25px;} +#content dt{font-weight:bold; margin-bottom:5px;} +#content dd{margin:0 0 10px 15px;} + + +/***********************************************/ + +form { + display: inline; +} + +blockquote { + padding-left: 6px; + border-left: 2px solid #ccc; +} + +input, select { + vertical-align: middle; + margin-bottom: 4px; +} + +input.button-small +{ + font-size: 0.8em; +} + +.select-small +{ + font-size: 0.8em; +} + +label { + font-weight: bold; + font-size: 1em; + color: #505050; +} + +fieldset { + border:1px solid #c0c0c0; + padding: 6px; +} + +legend { + color: #505050; + +} + +.required { + color: #bb0000; +} + +.odd { + background-color:#f6f7f8; +} +.even { + background-color: #fff; +} + +hr { border:none; border-bottom: dotted 1px #c0c0c0; } + +div.square { + border: 1px solid #999; + float: left; + margin: .4em .5em 0 0; + overflow: hidden; + width: .6em; height: .6em; +} + +table p { + margin:0; + padding:0; +} + +ul.documents { +list-style-type: none; +padding: 0; +margin: 0; +} + +ul.documents li { +background-image: url(../images/32x32/file.png); +background-repeat: no-repeat; +background-position: 0 1px; +padding-left: 36px; +margin-bottom: 10px; +margin-left: -37px; +} + +/********** Table used to display lists of things ***********/ + +table.list { + width:100%; + border-collapse: collapse; + border: 1px dotted #d0d0d0; + margin-bottom: 6px; +} + +table.with-cells td { + border: 1px solid #d7d7d7; +} + +table.list thead th { + text-align: center; + background: #eee; + border: 1px solid #d7d7d7; + color: #777; +} + +table.list tbody th { + font-weight: normal; + background: #eed; + border: 1px solid #d7d7d7; +} + +/********** Validation error messages *************/ +#errorExplanation { + width: 400px; + border: 0; + padding: 7px; + padding-bottom: 3px; + margin-bottom: 0px; +} + +#errorExplanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 10px 26px; + font-size: 1em; + margin: -7px; + background: url(../images/alert.png) no-repeat 6px 6px; +} + +#errorExplanation p { + color: #333; + margin-bottom: 0; + padding: 5px; +} + +#errorExplanation ul li { + font-size: 1em; + list-style: none; + margin-left: -16px; +} + +/*========== Drop down menu ==============*/ +div.menu { + background-color: #FFFFFF; + border-style: solid; + border-width: 1px; + border-color: #7F9DB9; + position: absolute; + top: 0px; + left: 0px; + padding: 0; + visibility: hidden; + z-index: 101; +} + +div.menu a.menuItem { + font-size: 10px; + font-weight: normal; + line-height: 2em; + color: #000000; + background-color: #FFFFFF; + cursor: default; + display: block; + padding: 0 1em; + margin: 0; + border: 0; + text-decoration: none; + white-space: nowrap; +} + +div.menu a.menuItem:hover, div.menu a.menuItemHighlight { + background-color: #80b0da; + color: #ffffff; +} + +div.menu a.menuItem span.menuItemText {} + +div.menu a.menuItem span.menuItemArrow { + margin-right: -.75em; +} + +/**************** Sidebar styles ****************/ + +#subcontent{ +position: absolute; +left: 0px; +width:110px; +padding:20px 20px 10px 5px; +} + +#subcontent h2{ +display:block; +margin:0 0 5px 0; +font-size:1.0em; +font-weight:bold; +text-align:left; +color:#606060; +background-color:inherit; +font-family: Trebuchet MS,Georgia,"Times New Roman",serif; +} + +#subcontent p{margin:0 0 16px 0; font-size:0.9em;} + +/**************** Menublock styles ****************/ + +.menublock{margin:0 0 20px 8px; font-size:0.8em;} +.menublock li{list-style:none; display:block; padding:1px; margin-bottom:0px;} +.menublock li a{font-weight:bold; text-decoration:none;} +.menublock li a:hover{text-decoration:none;} +.menublock li ul{margin:0; font-size:1em; font-weight:normal;} +.menublock li ul li{margin-bottom:0;} +.menublock li ul a{font-weight:normal;} + +/**************** Footer styles ****************/ + +#footer{ +clear:both; +padding:5px 0; +margin:0; +font-size:0.9em; +color:#f0f0f0; +background:#467aa7; +} + +#footer p{padding:0; margin:0; text-align:center;} +#footer a{color:#f0f0f0; background-color:inherit; font-weight:bold;} +#footer a:hover{color:#ffffff; background-color:inherit; text-decoration: underline;} + +/**************** Misc classes and styles ****************/ + +.splitcontentleft{float:left; width:49%;} +.splitcontentright{float:right; width:49%;} +.clear{clear:both;} +.small{font-size:0.8em;line-height:1.4em;padding:0 0 0 0;} +.hide{display:none;} +.textcenter{text-align:center;} +.textright{text-align:right;} +.important{color:#f02025; background-color:inherit; font-weight:bold;} + +.box{ +margin:0 0 20px 0; +padding:10px; +border:1px solid #c0c0c0; +background-color:#fafbfc; +color:#505050; +line-height:1.5em; +} + +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'); +} + +.rightbox{ +background: #fafbfc; +border: 1px solid #c0c0c0; +float: right; +padding: 8px; +position: relative; +margin: 0 5px 5px; +} + +.layout-active { +background: #ECF3E1; +} + +.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; +} + +.login { +width: 50%; +text-align: left; +} + +img.calendar-trigger { + cursor: pointer; + vertical-align: middle; + margin-left: 4px; +} + +#history p { + margin-left: 34px; +} + +/***** Contextual links div *****/ +.contextual { +float: right; +font-size: 0.8em; +line-height: 16px; +padding: 2px; +} + +.contextual select, .contextual input { +font-size: 1em; +} + +/***** 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; } + +/***** Tooltips ******/ +.tooltip{position:relative;z-index:24;} +.tooltip:hover{z-index:25;color:#000;} +.tooltip span.tip{display: none} + +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; +} + +/***** CSS FORM ******/ +.tabular p{ +margin: 0; +padding: 5px 0 8px 0; +padding-left: 180px; /*width of left column containing the label elements*/ +height: 1%; +} + +.tabular label{ +font-weight: bold; +float: left; +margin-left: -180px; /*width of left column*/ +width: 175px; /*width of labels. Should be smaller than left column to create some right +margin*/ +} + +.error { +color: #cc0000; +} + + +/*.threepxfix class below: +Targets IE6- ONLY. Adds 3 pixel indent for multi-line form contents. +to account for 3 pixel bug: http://www.positioniseverything.net/explorer/threepxtest.html +*/ + +* html .threepxfix{ +margin-left: 3px; +} \ No newline at end of file diff --git a/issue_relations/public/stylesheets/calendar.css b/issue_relations/public/stylesheets/calendar.css new file mode 100644 index 000000000..7f0b908e5 --- /dev/null +++ b/issue_relations/public/stylesheets/calendar.css @@ -0,0 +1,231 @@ +/* The main calendar widget. DIV containing a table. */ + +div.calendar { position: relative; } + +.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/issue_relations/public/stylesheets/csshover.htc b/issue_relations/public/stylesheets/csshover.htc new file mode 100644 index 000000000..3ba936ac3 --- /dev/null +++ b/issue_relations/public/stylesheets/csshover.htc @@ -0,0 +1,120 @@ + + \ No newline at end of file diff --git a/issue_relations/public/stylesheets/jstoolbar.css b/issue_relations/public/stylesheets/jstoolbar.css new file mode 100644 index 000000000..8094b47bb --- /dev/null +++ b/issue_relations/public/stylesheets/jstoolbar.css @@ -0,0 +1,78 @@ +.jstEditor { + padding-left: 0px; +} +.jstEditor textarea, .jstEditor iframe { + margin: 0; +} + +.jstHandle { + height: 16px; + 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; +} + +/* 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_ul { + background-image: url(../images/jstoolbar/bt_ul.png); +} +.jstb_ol { + background-image: url(../images/jstoolbar/bt_ol.png); +} +.jstb_link { + background-image: url(../images/jstoolbar/bt_link.png); +} diff --git a/issue_relations/public/stylesheets/print.css b/issue_relations/public/stylesheets/print.css new file mode 100644 index 000000000..76e0eae0b --- /dev/null +++ b/issue_relations/public/stylesheets/print.css @@ -0,0 +1,3 @@ +#header, #navigation, #subcontent, #footer { display:none; } +.menu { display:none; } +.contextual { display:none; } \ No newline at end of file diff --git a/issue_relations/public/stylesheets/scm.css b/issue_relations/public/stylesheets/scm.css new file mode 100644 index 000000000..c2b477edf --- /dev/null +++ b/issue_relations/public/stylesheets/scm.css @@ -0,0 +1,22 @@ + +div.action_M { background: #fd8 } +div.action_D { background: #f88 } +div.action_A { background: #bfb } + + +tr.spacing { + border: 1px solid #d7d7d7; +} + +.line-num { + border: 1px solid #d7d7d7; + font-size: 0.8em; + text-align: right; + width: 3em; + padding-right: 3px; +} + +.line-code { + font-family: "Courier New", monospace; + font-size: 1em; +} \ No newline at end of file diff --git a/issue_relations/script/about b/issue_relations/script/about new file mode 100644 index 000000000..7b07d46a3 --- /dev/null +++ b/issue_relations/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/issue_relations/script/breakpointer b/issue_relations/script/breakpointer new file mode 100644 index 000000000..64af76edd --- /dev/null +++ b/issue_relations/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/issue_relations/script/console b/issue_relations/script/console new file mode 100644 index 000000000..42f28f7d6 --- /dev/null +++ b/issue_relations/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/issue_relations/script/destroy b/issue_relations/script/destroy new file mode 100644 index 000000000..fa0e6fcd0 --- /dev/null +++ b/issue_relations/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/issue_relations/script/generate b/issue_relations/script/generate new file mode 100644 index 000000000..ef976e09f --- /dev/null +++ b/issue_relations/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/issue_relations/script/performance/benchmarker b/issue_relations/script/performance/benchmarker new file mode 100644 index 000000000..c842d35d3 --- /dev/null +++ b/issue_relations/script/performance/benchmarker @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/benchmarker' diff --git a/issue_relations/script/performance/profiler b/issue_relations/script/performance/profiler new file mode 100644 index 000000000..d855ac8b1 --- /dev/null +++ b/issue_relations/script/performance/profiler @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/profiler' diff --git a/issue_relations/script/plugin b/issue_relations/script/plugin new file mode 100644 index 000000000..26ca64c06 --- /dev/null +++ b/issue_relations/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/issue_relations/script/process/reaper b/issue_relations/script/process/reaper new file mode 100644 index 000000000..c77f04535 --- /dev/null +++ b/issue_relations/script/process/reaper @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/reaper' diff --git a/issue_relations/script/process/spawner b/issue_relations/script/process/spawner new file mode 100644 index 000000000..7118f3983 --- /dev/null +++ b/issue_relations/script/process/spawner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/spawner' diff --git a/issue_relations/script/process/spinner b/issue_relations/script/process/spinner new file mode 100644 index 000000000..6816b32ef --- /dev/null +++ b/issue_relations/script/process/spinner @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/spinner' diff --git a/issue_relations/script/runner b/issue_relations/script/runner new file mode 100644 index 000000000..ccc30f9d2 --- /dev/null +++ b/issue_relations/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/issue_relations/script/server b/issue_relations/script/server new file mode 100644 index 000000000..dfabcb881 --- /dev/null +++ b/issue_relations/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/issue_relations/test/fixtures/attachments.yml b/issue_relations/test/fixtures/attachments.yml new file mode 100644 index 000000000..6c352e1e3 --- /dev/null +++ b/issue_relations/test/fixtures/attachments.yml @@ -0,0 +1,13 @@ +--- +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 diff --git a/issue_relations/test/fixtures/auth_sources.yml b/issue_relations/test/fixtures/auth_sources.yml new file mode 100644 index 000000000..086c00f62 --- /dev/null +++ b/issue_relations/test/fixtures/auth_sources.yml @@ -0,0 +1,2 @@ +--- {} + diff --git a/issue_relations/test/fixtures/comments.yml b/issue_relations/test/fixtures/comments.yml new file mode 100644 index 000000000..24a4546aa --- /dev/null +++ b/issue_relations/test/fixtures/comments.yml @@ -0,0 +1,10 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +comments_001: + commented_type: News + commented_id: 1 + id: 1 + author_id: 1 + comment: my first comment + created_on: 2006-12-10 18:10:10 +01:00 + updated_on: 2006-12-10 18:10:10 +01:00 + \ No newline at end of file diff --git a/issue_relations/test/fixtures/custom_fields.yml b/issue_relations/test/fixtures/custom_fields.yml new file mode 100644 index 000000000..fcf52c17a --- /dev/null +++ b/issue_relations/test/fixtures/custom_fields.yml @@ -0,0 +1,45 @@ +--- +custom_fields_001: + name: Database + min_length: 0 + regexp: "" + is_for_all: false + type: IssueCustomField + max_length: 0 + possible_values: MySQL|PostgreSQL|Oracle + id: 1 + is_required: false + field_format: list +custom_fields_002: + name: Build + min_length: 1 + regexp: "" + is_for_all: true + type: IssueCustomField + max_length: 10 + possible_values: "" + id: 2 + is_required: false + field_format: 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 +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 diff --git a/issue_relations/test/fixtures/custom_fields_projects.yml b/issue_relations/test/fixtures/custom_fields_projects.yml new file mode 100644 index 000000000..086c00f62 --- /dev/null +++ b/issue_relations/test/fixtures/custom_fields_projects.yml @@ -0,0 +1,2 @@ +--- {} + diff --git a/issue_relations/test/fixtures/custom_fields_trackers.yml b/issue_relations/test/fixtures/custom_fields_trackers.yml new file mode 100644 index 000000000..cb06d2fcf --- /dev/null +++ b/issue_relations/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/issue_relations/test/fixtures/custom_values.yml b/issue_relations/test/fixtures/custom_values.yml new file mode 100644 index 000000000..4a65619c4 --- /dev/null +++ b/issue_relations/test/fixtures/custom_values.yml @@ -0,0 +1,43 @@ +--- +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: "101" +custom_values_005: + customized_type: Issue + custom_field_id: 2 + customized_id: 2 + id: 8 + value: "" diff --git a/issue_relations/test/fixtures/documents.yml b/issue_relations/test/fixtures/documents.yml new file mode 100644 index 000000000..086c00f62 --- /dev/null +++ b/issue_relations/test/fixtures/documents.yml @@ -0,0 +1,2 @@ +--- {} + diff --git a/issue_relations/test/fixtures/enumerations.yml b/issue_relations/test/fixtures/enumerations.yml new file mode 100644 index 000000000..eeef99b5b --- /dev/null +++ b/issue_relations/test/fixtures/enumerations.yml @@ -0,0 +1,33 @@ +--- +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 diff --git a/issue_relations/test/fixtures/issue_categories.yml b/issue_relations/test/fixtures/issue_categories.yml new file mode 100644 index 000000000..a994560d4 --- /dev/null +++ b/issue_relations/test/fixtures/issue_categories.yml @@ -0,0 +1,9 @@ +--- +issue_categories_001: + name: Printing + project_id: 1 + id: 1 +issue_categories_002: + name: Recipes + project_id: 1 + id: 2 diff --git a/issue_relations/test/fixtures/issue_statuses.yml b/issue_relations/test/fixtures/issue_statuses.yml new file mode 100644 index 000000000..b5a509f39 --- /dev/null +++ b/issue_relations/test/fixtures/issue_statuses.yml @@ -0,0 +1,37 @@ +--- +issue_statuses_006: + name: Rejected + is_default: false + html_color: F5C28B + is_closed: true + id: 6 +issue_statuses_001: + name: New + is_default: true + html_color: F98787 + is_closed: false + id: 1 +issue_statuses_002: + name: Assigned + is_default: false + html_color: C0C0FF + is_closed: false + id: 2 +issue_statuses_003: + name: Resolved + is_default: false + html_color: 88E0B3 + is_closed: false + id: 3 +issue_statuses_004: + name: Feedback + is_default: false + html_color: F3A4F4 + is_closed: false + id: 4 +issue_statuses_005: + name: Closed + is_default: false + html_color: DBDBDB + is_closed: true + id: 5 diff --git a/issue_relations/test/fixtures/issues.yml b/issue_relations/test/fixtures/issues.yml new file mode 100644 index 000000000..5719a9bc9 --- /dev/null +++ b/issue_relations/test/fixtures/issues.yml @@ -0,0 +1,43 @@ +--- +issues_001: + created_on: 2006-07-19 21:02:17 +02:00 + project_id: 1 + updated_on: 2006-07-19 21:04:30 +02:00 + 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 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 diff --git a/issue_relations/test/fixtures/members.yml b/issue_relations/test/fixtures/members.yml new file mode 100644 index 000000000..0626bdb18 --- /dev/null +++ b/issue_relations/test/fixtures/members.yml @@ -0,0 +1,13 @@ +--- +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 diff --git a/issue_relations/test/fixtures/news.yml b/issue_relations/test/fixtures/news.yml new file mode 100644 index 000000000..2c2e2c134 --- /dev/null +++ b/issue_relations/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/issue_relations/test/fixtures/permissions.yml b/issue_relations/test/fixtures/permissions.yml new file mode 100644 index 000000000..81350e1af --- /dev/null +++ b/issue_relations/test/fixtures/permissions.yml @@ -0,0 +1,379 @@ +--- +permissions_041: + action: add_file + id: 41 + description: button_add + controller: projects + mail_enabled: false + mail_option: false + sort: 1320 + is_public: false +permissions_030: + action: destroy + id: 30 + description: button_delete + controller: news + mail_enabled: false + mail_option: false + sort: 1122 + is_public: false +permissions_019: + action: download + id: 19 + description: button_download + controller: issues + mail_enabled: false + mail_option: false + sort: 1010 + is_public: true +permissions_008: + action: edit + id: 8 + description: button_edit + controller: members + mail_enabled: false + mail_option: false + sort: 221 + is_public: false +permissions_042: + action: destroy_file + id: 42 + description: button_delete + controller: versions + mail_enabled: false + mail_option: false + sort: 1322 + is_public: false +permissions_031: + action: list_documents + id: 31 + description: button_list + controller: projects + mail_enabled: false + mail_option: false + sort: 1200 + is_public: true +permissions_020: + action: add_issue + id: 20 + description: button_add + controller: projects + mail_enabled: true + mail_option: true + sort: 1050 + is_public: false +permissions_009: + action: destroy + id: 9 + description: button_delete + controller: members + mail_enabled: false + mail_option: false + sort: 222 + is_public: false +permissions_032: + action: show + id: 32 + description: button_view + controller: documents + mail_enabled: false + mail_option: false + sort: 1201 + is_public: true +permissions_021: + action: edit + id: 21 + description: button_edit + controller: issues + mail_enabled: false + mail_option: false + sort: 1055 + is_public: false +permissions_010: + action: add_version + id: 10 + description: button_add + controller: projects + mail_enabled: false + mail_option: false + sort: 320 + is_public: false +permissions_033: + action: download + id: 33 + description: button_download + controller: documents + mail_enabled: false + mail_option: false + sort: 1202 + is_public: true +permissions_022: + action: change_status + id: 22 + description: label_change_status + controller: issues + mail_enabled: true + mail_option: true + sort: 1060 + is_public: false +permissions_011: + action: edit + id: 11 + description: button_edit + controller: versions + mail_enabled: false + mail_option: false + sort: 321 + is_public: false +permissions_034: + action: add_document + id: 34 + description: button_add + controller: projects + mail_enabled: false + mail_option: false + sort: 1220 + is_public: false +permissions_023: + action: destroy + id: 23 + description: button_delete + controller: issues + mail_enabled: false + mail_option: false + sort: 1065 + is_public: false +permissions_012: + action: destroy + id: 12 + description: button_delete + controller: versions + mail_enabled: false + mail_option: false + sort: 322 + is_public: false +permissions_001: + action: show + id: 1 + description: label_overview + controller: projects + mail_enabled: false + mail_option: false + sort: 100 + is_public: true +permissions_035: + action: edit + id: 35 + description: button_edit + controller: documents + mail_enabled: false + mail_option: false + sort: 1221 + is_public: false +permissions_024: + action: add_attachment + id: 24 + description: label_attachment_new + controller: issues + mail_enabled: false + mail_option: false + sort: 1070 + is_public: false +permissions_013: + action: add_issue_category + id: 13 + description: button_add + controller: projects + mail_enabled: false + mail_option: false + sort: 420 + is_public: false +permissions_002: + action: changelog + id: 2 + description: label_change_log + controller: projects + mail_enabled: false + mail_option: false + sort: 105 + is_public: true +permissions_036: + action: destroy + id: 36 + description: button_delete + controller: documents + mail_enabled: false + mail_option: false + sort: 1222 + is_public: false +permissions_025: + action: destroy_attachment + id: 25 + description: label_attachment_delete + controller: issues + mail_enabled: false + mail_option: false + sort: 1075 + is_public: false +permissions_014: + action: edit + id: 14 + description: button_edit + controller: issue_categories + mail_enabled: false + mail_option: false + sort: 421 + is_public: false +permissions_003: + action: issue_report + id: 3 + description: label_report_plural + controller: reports + mail_enabled: false + mail_option: false + sort: 110 + is_public: true +permissions_037: + action: add_attachment + id: 37 + description: label_attachment_new + controller: documents + mail_enabled: false + mail_option: false + sort: 1223 + is_public: false +permissions_026: + action: list_news + id: 26 + description: button_list + controller: projects + mail_enabled: false + mail_option: false + sort: 1100 + is_public: true +permissions_015: + action: destroy + id: 15 + description: button_delete + controller: issue_categories + mail_enabled: false + mail_option: false + sort: 422 + is_public: false +permissions_004: + action: settings + id: 4 + description: label_settings + controller: projects + mail_enabled: false + mail_option: false + sort: 150 + is_public: false +permissions_038: + action: destroy_attachment + id: 38 + description: label_attachment_delete + controller: documents + mail_enabled: false + mail_option: false + sort: 1224 + is_public: false +permissions_027: + action: show + id: 27 + description: button_view + controller: news + mail_enabled: false + mail_option: false + sort: 1101 + is_public: true +permissions_016: + action: list_issues + id: 16 + description: button_list + controller: projects + mail_enabled: false + mail_option: false + sort: 1000 + is_public: true +permissions_005: + action: edit + id: 5 + description: button_edit + controller: projects + mail_enabled: false + mail_option: false + sort: 151 + is_public: false +permissions_039: + action: list_files + id: 39 + description: button_list + controller: projects + mail_enabled: false + mail_option: false + sort: 1300 + is_public: true +permissions_028: + action: add_news + id: 28 + description: button_add + controller: projects + mail_enabled: false + mail_option: false + sort: 1120 + is_public: false +permissions_017: + action: export_issues_csv + id: 17 + description: label_export_csv + controller: projects + mail_enabled: false + mail_option: false + sort: 1001 + is_public: true +permissions_006: + action: list_members + id: 6 + description: button_list + controller: projects + mail_enabled: false + mail_option: false + sort: 200 + is_public: true +permissions_040: + action: download + id: 40 + description: button_download + controller: versions + mail_enabled: false + mail_option: false + sort: 1301 + is_public: true +permissions_029: + action: edit + id: 29 + description: button_edit + controller: news + mail_enabled: false + mail_option: false + sort: 1121 + is_public: false +permissions_018: + action: show + id: 18 + description: button_view + controller: issues + mail_enabled: false + mail_option: false + sort: 1005 + is_public: true +permissions_007: + action: add_member + id: 7 + description: button_add + controller: projects + mail_enabled: false + mail_option: false + sort: 220 + is_public: false diff --git a/issue_relations/test/fixtures/permissions_roles.yml b/issue_relations/test/fixtures/permissions_roles.yml new file mode 100644 index 000000000..d4a054ecc --- /dev/null +++ b/issue_relations/test/fixtures/permissions_roles.yml @@ -0,0 +1,379 @@ +--- +permissions_roles_075: + role_id: 3 + permission_id: 34 +permissions_roles_047: + role_id: 1 + permission_id: 15 +permissions_roles_102: + role_id: 2 + permission_id: 4 +permissions_roles_019: + role_id: 3 + permission_id: 30 +permissions_roles_048: + role_id: 2 + permission_id: 24 +permissions_roles_103: + role_id: 2 + permission_id: 27 +permissions_roles_076: + role_id: 2 + permission_id: 41 +permissions_roles_049: + role_id: 1 + permission_id: 3 +permissions_roles_104: + role_id: 2 + permission_id: 36 +permissions_roles_077: + role_id: 2 + permission_id: 7 +permissions_roles_105: + role_id: 2 + permission_id: 32 +permissions_roles_078: + role_id: 3 + permission_id: 38 +permissions_roles_106: + role_id: 2 + permission_id: 14 +permissions_roles_020: + role_id: 2 + permission_id: 9 +permissions_roles_079: + role_id: 2 + permission_id: 18 +permissions_roles_107: + role_id: 3 + permission_id: 40 +permissions_roles_021: + role_id: 1 + permission_id: 13 +permissions_roles_108: + role_id: 1 + permission_id: 29 +permissions_roles_050: + role_id: 2 + permission_id: 29 +permissions_roles_022: + role_id: 3 + permission_id: 4 +permissions_roles_109: + role_id: 3 + permission_id: 22 +permissions_roles_051: + role_id: 3 + permission_id: 37 +permissions_roles_023: + role_id: 1 + permission_id: 23 +permissions_roles_052: + role_id: 2 + permission_id: 33 +permissions_roles_024: + role_id: 1 + permission_id: 1 +permissions_roles_080: + role_id: 2 + permission_id: 13 +permissions_roles_053: + role_id: 2 + permission_id: 1 +permissions_roles_025: + role_id: 2 + permission_id: 10 +permissions_roles_081: + role_id: 3 + permission_id: 20 +permissions_roles_054: + role_id: 2 + permission_id: 12 +permissions_roles_026: + role_id: 1 + permission_id: 36 +permissions_roles_082: + role_id: 1 + permission_id: 39 +permissions_roles_110: + role_id: 3 + permission_id: 6 +permissions_roles_027: + role_id: 3 + permission_id: 31 +permissions_roles_083: + role_id: 1 + permission_id: 33 +permissions_roles_055: + role_id: 1 + permission_id: 38 +permissions_roles_111: + role_id: 3 + permission_id: 1 +permissions_roles_028: + role_id: 1 + permission_id: 24 +permissions_roles_084: + role_id: 3 + permission_id: 16 +permissions_roles_056: + role_id: 2 + permission_id: 5 +permissions_roles_029: + role_id: 1 + permission_id: 9 +permissions_roles_085: + role_id: 3 + permission_id: 27 +permissions_roles_057: + role_id: 1 + permission_id: 16 +permissions_roles_112: + role_id: 1 + permission_id: 20 +permissions_roles_086: + role_id: 3 + permission_id: 12 +permissions_roles_058: + role_id: 1 + permission_id: 26 +permissions_roles_113: + role_id: 2 + permission_id: 37 +permissions_roles_087: + role_id: 1 + permission_id: 5 +permissions_roles_059: + role_id: 3 + permission_id: 18 +permissions_roles_114: + role_id: 2 + permission_id: 20 +permissions_roles_115: + role_id: 2 + permission_id: 15 +permissions_roles_088: + role_id: 2 + permission_id: 3 +permissions_roles_001: + role_id: 2 + permission_id: 21 +permissions_roles_116: + role_id: 3 + permission_id: 23 +permissions_roles_030: + role_id: 1 + permission_id: 30 +permissions_roles_089: + role_id: 1 + permission_id: 28 +permissions_roles_002: + role_id: 3 + permission_id: 29 +permissions_roles_117: + role_id: 3 + permission_id: 28 +permissions_roles_031: + role_id: 2 + permission_id: 38 +permissions_roles_003: + role_id: 3 + permission_id: 41 +permissions_roles_118: + role_id: 1 + permission_id: 34 +permissions_roles_032: + role_id: 3 + permission_id: 9 +permissions_roles_004: + role_id: 2 + permission_id: 8 +permissions_roles_060: + role_id: 2 + permission_id: 2 +permissions_roles_119: + role_id: 1 + permission_id: 21 +permissions_roles_033: + role_id: 2 + permission_id: 28 +permissions_roles_005: + role_id: 3 + permission_id: 3 +permissions_roles_061: + role_id: 2 + permission_id: 40 +permissions_roles_006: + role_id: 3 + permission_id: 14 +permissions_roles_090: + role_id: 2 + permission_id: 26 +permissions_roles_062: + role_id: 1 + permission_id: 19 +permissions_roles_034: + role_id: 2 + permission_id: 11 +permissions_roles_007: + role_id: 1 + permission_id: 35 +permissions_roles_091: + role_id: 3 + permission_id: 35 +permissions_roles_063: + role_id: 2 + permission_id: 30 +permissions_roles_035: + role_id: 2 + permission_id: 23 +permissions_roles_008: + role_id: 2 + permission_id: 17 +permissions_roles_092: + role_id: 2 + permission_id: 31 +permissions_roles_064: + role_id: 3 + permission_id: 33 +permissions_roles_036: + role_id: 3 + permission_id: 5 +permissions_roles_120: + role_id: 3 + permission_id: 13 +permissions_roles_009: + role_id: 1 + permission_id: 12 +permissions_roles_093: + role_id: 2 + permission_id: 42 +permissions_roles_065: + role_id: 3 + permission_id: 26 +permissions_roles_037: + role_id: 1 + permission_id: 42 +permissions_roles_121: + role_id: 3 + permission_id: 2 +permissions_roles_094: + role_id: 3 + permission_id: 39 +permissions_roles_066: + role_id: 2 + permission_id: 6 +permissions_roles_038: + role_id: 1 + permission_id: 25 +permissions_roles_122: + role_id: 1 + permission_id: 7 +permissions_roles_095: + role_id: 2 + permission_id: 19 +permissions_roles_067: + role_id: 1 + permission_id: 17 +permissions_roles_039: + role_id: 3 + permission_id: 36 +permissions_roles_123: + role_id: 3 + permission_id: 24 +permissions_roles_096: + role_id: 1 + permission_id: 18 +permissions_roles_068: + role_id: 1 + permission_id: 32 +permissions_roles_124: + role_id: 1 + permission_id: 11 +permissions_roles_010: + role_id: 1 + permission_id: 8 +permissions_roles_069: + role_id: 3 + permission_id: 19 +permissions_roles_097: + role_id: 2 + permission_id: 35 +permissions_roles_125: + role_id: 2 + permission_id: 16 +permissions_roles_011: + role_id: 3 + permission_id: 42 +permissions_roles_098: + role_id: 1 + permission_id: 6 +permissions_roles_126: + role_id: 3 + permission_id: 7 +permissions_roles_012: + role_id: 3 + permission_id: 8 +permissions_roles_040: + role_id: 1 + permission_id: 2 +permissions_roles_099: + role_id: 3 + permission_id: 17 +permissions_roles_041: + role_id: 2 + permission_id: 39 +permissions_roles_013: + role_id: 1 + permission_id: 40 +permissions_roles_070: + role_id: 3 + permission_id: 11 +permissions_roles_042: + role_id: 1 + permission_id: 37 +permissions_roles_014: + role_id: 1 + permission_id: 22 +permissions_roles_071: + role_id: 1 + permission_id: 4 +permissions_roles_043: + role_id: 3 + permission_id: 32 +permissions_roles_015: + role_id: 2 + permission_id: 22 +permissions_roles_072: + role_id: 1 + permission_id: 27 +permissions_roles_044: + role_id: 1 + permission_id: 14 +permissions_roles_016: + role_id: 3 + permission_id: 15 +permissions_roles_073: + role_id: 2 + permission_id: 34 +permissions_roles_045: + role_id: 3 + permission_id: 10 +permissions_roles_100: + role_id: 1 + permission_id: 10 +permissions_roles_017: + role_id: 3 + permission_id: 25 +permissions_roles_074: + role_id: 2 + permission_id: 25 +permissions_roles_046: + role_id: 1 + permission_id: 31 +permissions_roles_101: + role_id: 3 + permission_id: 21 +permissions_roles_018: + role_id: 1 + permission_id: 41 diff --git a/issue_relations/test/fixtures/projects.yml b/issue_relations/test/fixtures/projects.yml new file mode 100644 index 000000000..9aa2f9abe --- /dev/null +++ b/issue_relations/test/fixtures/projects.yml @@ -0,0 +1,41 @@ +--- +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 + 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 + 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 + 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 + parent_id: 1 diff --git a/issue_relations/test/fixtures/roles.yml b/issue_relations/test/fixtures/roles.yml new file mode 100644 index 000000000..4fc9881b4 --- /dev/null +++ b/issue_relations/test/fixtures/roles.yml @@ -0,0 +1,10 @@ +--- +roles_001: + name: Manager + id: 1 +roles_002: + name: Developer + id: 2 +roles_003: + name: Reporter + id: 3 diff --git a/issue_relations/test/fixtures/tokens.yml b/issue_relations/test/fixtures/tokens.yml new file mode 100644 index 000000000..977bafe6e --- /dev/null +++ b/issue_relations/test/fixtures/tokens.yml @@ -0,0 +1 @@ +--- diff --git a/issue_relations/test/fixtures/trackers.yml b/issue_relations/test/fixtures/trackers.yml new file mode 100644 index 000000000..d4ea34ac8 --- /dev/null +++ b/issue_relations/test/fixtures/trackers.yml @@ -0,0 +1,13 @@ +--- +trackers_001: + name: Bug + id: 1 + is_in_chlog: true +trackers_002: + name: Feature request + id: 2 + is_in_chlog: true +trackers_003: + name: Support request + id: 3 + is_in_chlog: false diff --git a/issue_relations/test/fixtures/user_preferences.yml b/issue_relations/test/fixtures/user_preferences.yml new file mode 100644 index 000000000..8794d28ae --- /dev/null +++ b/issue_relations/test/fixtures/user_preferences.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +first: + id: 1 +another: + id: 2 diff --git a/issue_relations/test/fixtures/users.yml b/issue_relations/test/fixtures/users.yml new file mode 100644 index 000000000..ffa2fe42e --- /dev/null +++ b/issue_relations/test/fixtures/users.yml @@ -0,0 +1,61 @@ +--- +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 +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 +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 +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 diff --git a/issue_relations/test/fixtures/versions.yml b/issue_relations/test/fixtures/versions.yml new file mode 100644 index 000000000..89a738abd --- /dev/null +++ b/issue_relations/test/fixtures/versions.yml @@ -0,0 +1,17 @@ +--- +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 diff --git a/issue_relations/test/fixtures/workflows.yml b/issue_relations/test/fixtures/workflows.yml new file mode 100644 index 000000000..47e95e6e3 --- /dev/null +++ b/issue_relations/test/fixtures/workflows.yml @@ -0,0 +1,1621 @@ +--- +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_037: + new_status_id: 3 + role_id: 2 + old_status_id: 2 + id: 37 + tracker_id: 1 +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/issue_relations/test/functional/my_controller_test.rb b/issue_relations/test/functional/my_controller_test.rb new file mode 100644 index 000000000..525c71b45 --- /dev/null +++ b/issue_relations/test/functional/my_controller_test.rb @@ -0,0 +1,18 @@ +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 + def setup + @controller = MyController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/issue_relations/test/functional/projects_controller_test.rb b/issue_relations/test/functional/projects_controller_test.rb new file mode 100644 index 000000000..f20f8ad0f --- /dev/null +++ b/issue_relations/test/functional/projects_controller_test.rb @@ -0,0 +1,114 @@ +# 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' +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, :permissions + + 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(:projects) + end + + def test_show + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + end + + def test_list_members + get :list_members, :id => 1 + assert_response :success + assert_template 'list_members' + assert_not_nil assigns(:members) + end + + def test_list_documents + get :list_documents, :id => 1 + assert_response :success + assert_template 'list_documents' + assert_not_nil assigns(:documents) + end + + def test_list_issues + get :list_issues, :id => 1 + assert_response :success + assert_template 'list_issues' + assert_not_nil assigns(:issues) + end + + def test_list_issues_with_filter + get :list_issues, :id => 1, :set_filter => 1 + assert_response :success + assert_template 'list_issues' + assert_not_nil assigns(:issues) + end + + def test_list_issues_reset_filter + post :list_issues, :id => 1 + assert_response :success + assert_template 'list_issues' + assert_not_nil assigns(:issues) + end + + def test_export_issues_csv + get :export_issues_csv, :id => 1 + assert_response :success + assert_not_nil assigns(:issues) + end + + def test_list_news + get :list_news, :id => 1 + assert_response :success + assert_template 'list_news' + assert_not_nil assigns(:news) + 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(:fixed_issues) + end +end diff --git a/issue_relations/test/integration/account_test.rb b/issue_relations/test/integration/account_test.rb new file mode 100644 index 000000000..0d6f75d70 --- /dev/null +++ b/issue_relations/test/integration/account_test.rb @@ -0,0 +1,100 @@ +# 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 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_change_password + log_user('jsmith', 'jsmith') + get "my/account" + assert_response :success + assert_template "my/account" + + post "my/change_password", :password => 'jsmith', :new_password => "hello", :new_password_confirmation => "hello2" + assert_response :success + assert_template "my/account" + assert_tag :tag => "div", :attributes => { :class => "errorExplanation" } + + post "my/change_password", :password => 'jsmithZZ', :new_password => "hello", :new_password_confirmation => "hello" + assert_redirected_to "my/account" + assert_equal 'Wrong password', flash[:notice] + + post "my/change_password", :password => 'jsmith', :new_password => "hello", :new_password_confirmation => "hello" + assert_redirected_to "my/account" + log_user('jsmith', 'hello') + end + + def test_my_account + log_user('jsmith', 'jsmith') + get "my/account" + assert_response :success + assert_template "my/account" + + post "my/account", :user => {:firstname => "Joe", :login => "root", :admin => 1} + assert_response :success + assert_template "my/account" + user = User.find(2) + assert_equal "Joe", user.firstname + assert_equal "jsmith", user.login + assert_equal false, user.admin? + end + + def test_my_page + log_user('jsmith', 'jsmith') + get "my/page" + assert_response :success + assert_template "my/page" + end + + def test_lost_password + 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 +end diff --git a/issue_relations/test/integration/admin_test.rb b/issue_relations/test/integration/admin_test.rb new file mode 100644 index 000000000..0241ae8da --- /dev/null +++ b/issue_relations/test/integration/admin_test.rb @@ -0,0 +1,61 @@ +# 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", :is_public => 1} + 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/issue_relations/test/integration/issues_test.rb b/issue_relations/test/integration/issues_test.rb new file mode 100644 index 000000000..0836b45cf --- /dev/null +++ b/issue_relations/test/integration/issues_test.rb @@ -0,0 +1,78 @@ +require "#{File.dirname(__FILE__)}/../test_helper" + +class IssuesTest < ActionController::IntegrationTest + fixtures :projects, :users, :trackers, :issue_statuses, :issues, :permissions, :permissions_roles, :enumerations + + # create an issue + def test_add_issue + log_user('jsmith', 'jsmith') + get "projects/add_issue/1", :tracker_id => "1" + assert_response :success + assert_template "projects/add_issue" + + post "projects/add_issue/1", :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 => "" } + # find created issue + issue = Issue.find_by_subject("new test issue") + assert_kind_of Issue, issue + + # check redirection + assert_redirected_to "projects/list_issues/1" + follow_redirect! + assert assigns(:issues).include?(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') + + file_data_1 = "some text...." + file_name_1 = "sometext.txt" + file_data_2 = "more text..." + file_name_2 = "moretext.txt" + + boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor" + headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" } + + data = [ + "--" + boundary, + "Content-Disposition: form-data; name=\"attachments[]\"; filename=\"#{file_name_1}\"", + "Content-Type: text/plain", + "", file_data_1, + "--" + boundary, + "Content-Disposition: form-data; name=\"attachments[]\"; filename=\"#{file_name_2}\"", + "Content-Type: text/plain", + "", file_data_2, + "--" + boundary, "" + ].join("\x0D\x0A") + + post "issues/add_attachment/1", data, headers + assert_redirected_to "issues/show/1" + + # make sure attachment was saved + attachment = Issue.find(1).attachments.find_by_filename(file_name_1) + assert_kind_of Attachment, attachment + assert_equal Issue.find(1), attachment.container + # 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/issue_relations/test/test_helper.rb b/issue_relations/test/test_helper.rb new file mode 100644 index 000000000..2e4f7dcd0 --- /dev/null +++ b/issue_relations/test/test_helper.rb @@ -0,0 +1,55 @@ +# 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' + +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", :login => login, :password => password + assert_redirected_to "my/page" + assert_equal login, User.find(session[:user_id]).login + end +end diff --git a/issue_relations/test/unit/comment_test.rb b/issue_relations/test/unit/comment_test.rb new file mode 100644 index 000000000..4435f0a7f --- /dev/null +++ b/issue_relations/test/unit/comment_test.rb @@ -0,0 +1,30 @@ +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, :comment => "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/issue_relations/test/unit/member_test.rb b/issue_relations/test/unit/member_test.rb new file mode 100644 index 000000000..079782306 --- /dev/null +++ b/issue_relations/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/issue_relations/test/unit/project_test.rb b/issue_relations/test/unit/project_test.rb new file mode 100644 index 000000000..9c8f0c97e --- /dev/null +++ b/issue_relations/test/unit/project_test.rb @@ -0,0 +1,79 @@ +# 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 ProjectTest < Test::Unit::TestCase + fixtures :projects + + 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 l(: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_destroy + @ecookbook.destroy + assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) } + 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.projects_count + 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 +end diff --git a/issue_relations/test/unit/token_test.rb b/issue_relations/test/unit/token_test.rb new file mode 100644 index 000000000..1c3820e99 --- /dev/null +++ b/issue_relations/test/unit/token_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TokenTest < Test::Unit::TestCase + fixtures :tokens + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/issue_relations/test/unit/user_preference_test.rb b/issue_relations/test/unit/user_preference_test.rb new file mode 100644 index 000000000..4675a2652 --- /dev/null +++ b/issue_relations/test/unit/user_preference_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class UserPreferenceTest < Test::Unit::TestCase + fixtures :user_preferences + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/issue_relations/test/unit/user_test.rb b/issue_relations/test/unit/user_test.rb new file mode 100644 index 000000000..211e6554c --- /dev/null +++ b/issue_relations/test/unit/user_test.rb @@ -0,0 +1,88 @@ +# 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 + + def setup + @admin = User.find(1) + @jsmith = User.find(2) + 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 2, @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_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 +end diff --git a/issue_relations/vendor/plugins/gloc-1.1.0/CHANGELOG b/issue_relations/vendor/plugins/gloc-1.1.0/CHANGELOG new file mode 100644 index 000000000..6392d7cbe --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/MIT-LICENSE b/issue_relations/vendor/plugins/gloc-1.1.0/MIT-LICENSE new file mode 100644 index 000000000..081774a65 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/README b/issue_relations/vendor/plugins/gloc-1.1.0/README new file mode 100644 index 000000000..66f8e5e9f --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/Rakefile b/issue_relations/vendor/plugins/gloc-1.1.0/Rakefile new file mode 100644 index 000000000..a5b8fe762 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionController/Filters/ClassMethods.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionController/Filters/ClassMethods.html new file mode 100644 index 000000000..fba33b5b5 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionMailer/Base.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionMailer/Base.html new file mode 100644 index 000000000..056b23d85 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Base.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Base.html new file mode 100644 index 000000000..00767055d --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/DateHelper.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/DateHelper.html new file mode 100644 index 000000000..84ca8fae3 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/InstanceTag.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActionView/Helpers/InstanceTag.html new file mode 100644 index 000000000..a236e0e5d --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Errors.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Errors.html new file mode 100644 index 000000000..9a16f608b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Validations/ClassMethods.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/ActiveRecord/Validations/ClassMethods.html new file mode 100644 index 000000000..145a74c2b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc.html new file mode 100644 index 000000000..8a25c7de8 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/ClassMethods.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/ClassMethods.html new file mode 100644 index 000000000..ba1a28ad0 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/Helpers.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/Helpers.html new file mode 100644 index 000000000..f3fdf63e1 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/InstanceMethods.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/classes/GLoc/InstanceMethods.html new file mode 100644 index 000000000..4e15c9383 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/created.rid b/issue_relations/vendor/plugins/gloc-1.1.0/doc/created.rid new file mode 100644 index 000000000..eba9efa29 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/CHANGELOG.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/CHANGELOG.html new file mode 100644 index 000000000..aec36c5bf --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/README.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/README.html new file mode 100644 index 000000000..d078659d2 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-helpers_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-helpers_rb.html new file mode 100644 index 000000000..394b79d70 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-internal_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-internal_rb.html new file mode 100644 index 000000000..6d09fec7b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails-text_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails-text_rb.html new file mode 100644 index 000000000..52a387218 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-rails_rb.html new file mode 100644 index 000000000..3ae73b87b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-ruby_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-ruby_rb.html new file mode 100644 index 000000000..4b29e9d94 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-version_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc-version_rb.html new file mode 100644 index 000000000..17f93aa43 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc_rb.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/files/lib/gloc_rb.html new file mode 100644 index 000000000..9e68a89cd --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/fr_class_index.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/fr_class_index.html new file mode 100644 index 000000000..08e0418f3 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/fr_file_index.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/fr_file_index.html new file mode 100644 index 000000000..839e378d3 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/fr_method_index.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/fr_method_index.html new file mode 100644 index 000000000..325ed3589 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/index.html b/issue_relations/vendor/plugins/gloc-1.1.0/doc/index.html new file mode 100644 index 000000000..f29103142 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/doc/rdoc-style.css b/issue_relations/vendor/plugins/gloc-1.1.0/doc/rdoc-style.css new file mode 100644 index 000000000..fbf7326af --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/init.rb b/issue_relations/vendor/plugins/gloc-1.1.0/init.rb new file mode 100644 index 000000000..9d99acd61 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-config.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-config.rb new file mode 100644 index 000000000..e85b041f5 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-dev.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-dev.rb new file mode 100644 index 000000000..cb12b4cb3 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-helpers.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-helpers.rb new file mode 100644 index 000000000..f2ceb8e3d --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-internal.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-internal.rb new file mode 100644 index 000000000..f16e90555 --- /dev/null +++ b/issue_relations/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,'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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb new file mode 100644 index 000000000..abbb7a190 --- /dev/null +++ b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb @@ -0,0 +1,137 @@ +# 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 + + 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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb new file mode 100644 index 000000000..0a35b90ab --- /dev/null +++ b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb @@ -0,0 +1,230 @@ +# 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) + render_message_without_gloc("#{method_name}_#{current_language}", 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 + + 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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-ruby.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-ruby.rb new file mode 100644 index 000000000..f96ab6cf9 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-version.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc-version.rb new file mode 100644 index 000000000..91afcf482 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc.rb b/issue_relations/vendor/plugins/gloc-1.1.0/lib/gloc.rb new file mode 100644 index 000000000..bcad0ed9b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/tasks/gloc.rake b/issue_relations/vendor/plugins/gloc-1.1.0/tasks/gloc.rake new file mode 100644 index 000000000..8da73779e --- /dev/null +++ b/issue_relations/vendor/plugins/gloc-1.1.0/tasks/gloc.rake @@ -0,0 +1,30 @@ +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 +end \ No newline at end of file diff --git a/issue_relations/vendor/plugins/gloc-1.1.0/test/gloc_rails_test.rb b/issue_relations/vendor/plugins/gloc-1.1.0/test/gloc_rails_test.rb new file mode 100644 index 000000000..4cb232904 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/gloc_test.rb b/issue_relations/vendor/plugins/gloc-1.1.0/test/gloc_test.rb new file mode 100644 index 000000000..a39d5c41c --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lang/en.yaml b/issue_relations/vendor/plugins/gloc-1.1.0/test/lang/en.yaml new file mode 100644 index 000000000..325dc599e --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lang/en_AU.yaml b/issue_relations/vendor/plugins/gloc-1.1.0/test/lang/en_AU.yaml new file mode 100644 index 000000000..307cc7859 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lang/ja.yml b/issue_relations/vendor/plugins/gloc-1.1.0/test/lang/ja.yml new file mode 100644 index 000000000..64df03376 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lang2/en.yml b/issue_relations/vendor/plugins/gloc-1.1.0/test/lang2/en.yml new file mode 100644 index 000000000..e6467e7a0 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lang2/ja.yaml b/issue_relations/vendor/plugins/gloc-1.1.0/test/lang2/ja.yaml new file mode 100644 index 000000000..864b287d0 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lib/rails-string_ext.rb b/issue_relations/vendor/plugins/gloc-1.1.0/test/lib/rails-string_ext.rb new file mode 100644 index 000000000..418d28db2 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/gloc-1.1.0/test/lib/rails-time_ext.rb b/issue_relations/vendor/plugins/gloc-1.1.0/test/lib/rails-time_ext.rb new file mode 100644 index 000000000..d8771e4e6 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/CHANGELOG b/issue_relations/vendor/plugins/rfpdf/CHANGELOG new file mode 100644 index 000000000..6822b8364 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/MIT-LICENSE b/issue_relations/vendor/plugins/rfpdf/MIT-LICENSE new file mode 100644 index 000000000..f39a79dc0 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/README b/issue_relations/vendor/plugins/rfpdf/README new file mode 100644 index 000000000..9db19075b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/init.rb b/issue_relations/vendor/plugins/rfpdf/init.rb new file mode 100644 index 000000000..7e51d9eba --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf.rb new file mode 100644 index 000000000..9fc0683ef --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/bookmark.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/bookmark.rb new file mode 100644 index 000000000..a04ccd18d --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb new file mode 100644 index 000000000..731d582a2 --- /dev/null +++ b/issue_relations/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.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']=='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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/errors.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/errors.rb new file mode 100644 index 000000000..2be2dae16 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/fpdf.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/fpdf.rb new file mode 100644 index 000000000..ad52e9e62 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/fpdf_eps.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/fpdf_eps.rb new file mode 100644 index 000000000..c6a224310 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/japanese.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/japanese.rb new file mode 100644 index 000000000..7340936bb --- /dev/null +++ b/issue_relations/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.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(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.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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/korean.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/korean.rb new file mode 100644 index 000000000..64131405e --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/makefont.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/makefont.rb new file mode 100644 index 000000000..ffc98b48f --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/rfpdf.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/rfpdf.rb new file mode 100644 index 000000000..5ad882903 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/view.rb b/issue_relations/vendor/plugins/rfpdf/lib/rfpdf/view.rb new file mode 100644 index 000000000..185811202 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/rfpdf/test/test_helper.rb b/issue_relations/vendor/plugins/rfpdf/test/test_helper.rb new file mode 100644 index 000000000..2e2ea3bc5 --- /dev/null +++ b/issue_relations/vendor/plugins/rfpdf/test/test_helper.rb @@ -0,0 +1 @@ +#!/usr/bin/env ruby \ No newline at end of file diff --git a/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/COPYING b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/COPYING new file mode 100644 index 000000000..2ff629a20 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/ChangeLog b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/ChangeLog new file mode 100644 index 000000000..bd9b70e7d --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/LICENCE b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/LICENCE new file mode 100644 index 000000000..953ea0bb9 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/README b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/README new file mode 100644 index 000000000..f61a7ff15 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ber.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ber.rb new file mode 100644 index 000000000..e76100656 --- /dev/null +++ b/issue_relations/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 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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap.rb new file mode 100644 index 000000000..d741e722b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/dataset.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/dataset.rb new file mode 100644 index 000000000..1480a8f84 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/entry.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/entry.rb new file mode 100644 index 000000000..8978545ee --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/filter.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/filter.rb new file mode 100644 index 000000000..38944a710 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/pdu.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/pdu.rb new file mode 100644 index 000000000..dbc0d6f10 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/psw.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldap/psw.rb new file mode 100644 index 000000000..89d1ffdf2 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldif.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/lib/net/ldif.rb new file mode 100644 index 000000000..1641bda4b --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testber.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testber.rb new file mode 100644 index 000000000..4fe2e3071 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testdata.ldif b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testdata.ldif new file mode 100644 index 000000000..eb5610d5f --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testem.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testem.rb new file mode 100644 index 000000000..46b4909cb --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testfilter.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testfilter.rb new file mode 100644 index 000000000..b8fb40996 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldap.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldap.rb new file mode 100644 index 000000000..bb70a0b20 --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldif.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testldif.rb new file mode 100644 index 000000000..73eca746f --- /dev/null +++ b/issue_relations/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/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testpsw.rb b/issue_relations/vendor/plugins/ruby-net-ldap-0.0.4/tests/testpsw.rb new file mode 100644 index 000000000..6b1aa08be --- /dev/null +++ b/issue_relations/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 + +