diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2008-08-30 14:41:07 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2008-08-30 14:41:07 +0000 |
commit | a377069fb2f208213c07e949aa20a36d4cd8cdbf (patch) | |
tree | 21a642295e9fb5b08cf59c91d1e0814dea0dbaf2 | |
parent | ab83ed5d8eb9c07f53a5ba3a285d552f8eb34c42 (diff) | |
download | redmine-a377069fb2f208213c07e949aa20a36d4cd8cdbf.tar.gz redmine-a377069fb2f208213c07e949aa20a36d4cd8cdbf.zip |
Merged trunk r1773.
git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1774 e93f8b46-1217-0410-a6f0-8f06a7374b81
398 files changed, 14338 insertions, 5285 deletions
diff --git a/groups/app/controllers/account_controller.rb b/groups/app/controllers/account_controller.rb index b9224c158..4b2ec8317 100644 --- a/groups/app/controllers/account_controller.rb +++ b/groups/app/controllers/account_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AccountController < ApplicationController - layout 'base' helper :custom_fields include CustomFieldsHelper @@ -26,7 +25,7 @@ class AccountController < ApplicationController # Show user's account def show @user = User.find_active(params[:id]) - @custom_values = @user.custom_values.find(:all, :include => :custom_field) + @custom_values = @user.custom_values # show only public projects and private projects that the logged in user is also a member of @memberships = @user.memberships.select do |membership| @@ -44,7 +43,16 @@ class AccountController < ApplicationController else # Authenticate user user = User.try_to_login(params[:username], params[:password]) - if user + if user.nil? + # Invalid credentials + flash.now[:error] = l(:notice_account_invalid_creditentials) + elsif user.new_record? + # Onthefly creation failed, display the registration form to fill/fix attributes + @user = user + session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id } + render :action => 'register' + else + # Valid user self.logged_user = user # generate a key and set cookie if autologin if params[:autologin] && Setting.autologin? @@ -52,12 +60,8 @@ class AccountController < ApplicationController cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } end redirect_back_or_default :controller => 'my', :action => 'page' - else - flash.now[:error] = l(:notice_account_invalid_creditentials) end end - rescue User::OnTheFlyCreationFailure - flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.' end # Log out current user and redirect to welcome page @@ -107,43 +111,52 @@ class AccountController < ApplicationController # User self-registration def register - redirect_to(home_url) && return unless Setting.self_registration? + redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration] if request.get? + session[:auth_source_registration] = nil @user = User.new(:language => Setting.default_language) - @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } else @user = User.new(params[:user]) @user.admin = false - @user.login = params[:user][:login] @user.status = User::STATUS_REGISTERED - @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] - @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, - :customized => @user, - :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) } - @user.custom_values = @custom_values - case Setting.self_registration - when '1' - # Email activation - token = Token.new(:user => @user, :action => "register") - if @user.save and token.save - Mailer.deliver_register(token) - flash[:notice] = l(:notice_account_register_done) - redirect_to :action => 'login' - end - when '3' - # Automatic activation + if session[:auth_source_registration] @user.status = User::STATUS_ACTIVE + @user.login = session[:auth_source_registration][:login] + @user.auth_source_id = session[:auth_source_registration][:auth_source_id] if @user.save + session[:auth_source_registration] = nil + self.logged_user = @user flash[:notice] = l(:notice_account_activated) - redirect_to :action => 'login' + redirect_to :controller => 'my', :action => 'account' end else - # Manual activation by the administrator - if @user.save - # Sends an email to the administrators - Mailer.deliver_account_activation_request(@user) - flash[:notice] = l(:notice_account_pending) - redirect_to :action => 'login' + @user.login = params[:user][:login] + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + case Setting.self_registration + when '1' + # Email activation + token = Token.new(:user => @user, :action => "register") + if @user.save and token.save + Mailer.deliver_register(token) + flash[:notice] = l(:notice_account_register_done) + redirect_to :action => 'login' + end + when '3' + # Automatic activation + @user.status = User::STATUS_ACTIVE + if @user.save + self.logged_user = @user + flash[:notice] = l(:notice_account_activated) + redirect_to :controller => 'my', :action => 'account' + end + else + # Manual activation by the administrator + if @user.save + # Sends an email to the administrators + Mailer.deliver_account_activation_request(@user) + flash[:notice] = l(:notice_account_pending) + redirect_to :action => 'login' + end end end end diff --git a/groups/app/controllers/admin_controller.rb b/groups/app/controllers/admin_controller.rb index e002f3a27..a6df49dcd 100644 --- a/groups/app/controllers/admin_controller.rb +++ b/groups/app/controllers/admin_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AdminController < ApplicationController - layout 'base' before_filter :require_admin helper :sort diff --git a/groups/app/controllers/application.rb b/groups/app/controllers/application.rb index abf621641..7a56e61f0 100644 --- a/groups/app/controllers/application.rb +++ b/groups/app/controllers/application.rb @@ -15,7 +15,11 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'uri' + class ApplicationController < ActionController::Base + layout 'base' + before_filter :user_setup, :check_if_login_required, :set_localization filter_parameter_logging :password @@ -61,11 +65,11 @@ class ApplicationController < ActionController::Base def set_localization User.current.language = nil unless User.current.logged? lang = begin - if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym + if !User.current.language.blank? && GLoc.valid_language?(User.current.language) User.current.language elsif request.env['HTTP_ACCEPT_LANGUAGE'] - accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first - if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym + accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase + if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first)) User.current.language = accept_lang end end @@ -77,8 +81,7 @@ class ApplicationController < ActionController::Base def require_login if !User.current.logged? - store_location - redirect_to :controller => "account", :action => "login" + redirect_to :controller => "account", :action => "login", :back_url => request.request_uri return false end true @@ -115,20 +118,16 @@ class ApplicationController < ActionController::Base end end - # store current uri in session. - # return to this location by calling redirect_back_or_default - def store_location - session[:return_to_params] = params - end - - # move to the last store_location call or to the passed default one def redirect_back_or_default(default) - if session[:return_to_params].nil? - redirect_to default - else - redirect_to session[:return_to_params] - session[:return_to_params] = nil + back_url = params[:back_url] + if !back_url.blank? + uri = URI.parse(back_url) + # do not redirect user to another host + if uri.relative? || (uri.host == request.host) + redirect_to(back_url) and return + end end + redirect_to default end def render_403 diff --git a/groups/app/controllers/attachments_controller.rb b/groups/app/controllers/attachments_controller.rb index 4e87e5442..788bab94d 100644 --- a/groups/app/controllers/attachments_controller.rb +++ b/groups/app/controllers/attachments_controller.rb @@ -16,24 +16,40 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AttachmentsController < ApplicationController - layout 'base' - before_filter :find_project, :check_project_privacy + before_filter :find_project + def show + if @attachment.is_diff? + @diff = File.new(@attachment.diskfile, "rb").read + render :action => 'diff' + elsif @attachment.is_text? + @content = File.new(@attachment.diskfile, "rb").read + render :action => 'file' + elsif + download + end + end + def download + @attachment.increment_download if @attachment.container.is_a?(Version) + # images are sent inline send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), :type => @attachment.content_type, :disposition => (@attachment.image? ? 'inline' : 'attachment') - rescue - # in case the disk file was deleted - render_404 end private def find_project @attachment = Attachment.find(params[:id]) + # Show 404 if the filename in the url is wrong + raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename + @project = @attachment.project - rescue + permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym + allowed = User.current.allowed_to?(permission, @project) + allowed ? true : (User.current.logged? ? render_403 : require_login) + rescue ActiveRecord::RecordNotFound render_404 end end diff --git a/groups/app/controllers/auth_sources_controller.rb b/groups/app/controllers/auth_sources_controller.rb index b830f1970..981f29f03 100644 --- a/groups/app/controllers/auth_sources_controller.rb +++ b/groups/app/controllers/auth_sources_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AuthSourcesController < ApplicationController - layout 'base' before_filter :require_admin def index diff --git a/groups/app/controllers/boards_controller.rb b/groups/app/controllers/boards_controller.rb index 5bf4499bd..4532a88fe 100644 --- a/groups/app/controllers/boards_controller.rb +++ b/groups/app/controllers/boards_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class BoardsController < ApplicationController - layout 'base' before_filter :find_project, :authorize helper :messages diff --git a/groups/app/controllers/custom_fields_controller.rb b/groups/app/controllers/custom_fields_controller.rb index 57f700dc6..4ef4e4a90 100644 --- a/groups/app/controllers/custom_fields_controller.rb +++ b/groups/app/controllers/custom_fields_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class CustomFieldsController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -32,18 +31,20 @@ class CustomFieldsController < ApplicationController 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]) - when "GroupCustomField" - @custom_field = GroupCustomField.new(params[:custom_field]) - else - render_404 - return + 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]) + when "TimeEntryCustomField" + @custom_field = TimeEntryCustomField.new(params[:custom_field]) + when "GroupCustomField" + @custom_field = GroupCustomField.new(params[:custom_field]) + else + redirect_to :action => 'list' + return end if request.post? and @custom_field.save flash[:notice] = l(:notice_successful_create) diff --git a/groups/app/controllers/documents_controller.rb b/groups/app/controllers/documents_controller.rb index 7e732b9b6..dbf9cd8e5 100644 --- a/groups/app/controllers/documents_controller.rb +++ b/groups/app/controllers/documents_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class DocumentsController < ApplicationController - layout 'base' before_filter :find_project, :only => [:index, :new] before_filter :find_document, :except => [:index, :new] before_filter :authorize @@ -65,15 +64,6 @@ class DocumentsController < ApplicationController @document.destroy redirect_to :controller => 'documents', :action => 'index', :project_id => @project end - - def download - @attachment = @document.attachments.find(params[:attachment_id]) - @attachment.increment_download - send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), - :type => @attachment.content_type - rescue - render_404 - end def add_attachment attachments = attach_files(@document, params[:attachments]) diff --git a/groups/app/controllers/enumerations_controller.rb b/groups/app/controllers/enumerations_controller.rb index 7a7f1685a..50521bab8 100644 --- a/groups/app/controllers/enumerations_controller.rb +++ b/groups/app/controllers/enumerations_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class EnumerationsController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -75,11 +74,20 @@ class EnumerationsController < ApplicationController end def destroy - Enumeration.find(params[:id]).destroy - flash[:notice] = l(:notice_successful_delete) - redirect_to :action => 'list' - rescue - flash[:error] = "Unable to delete enumeration" - redirect_to :action => 'list' + @enumeration = Enumeration.find(params[:id]) + if !@enumeration.in_use? + # No associated objects + @enumeration.destroy + redirect_to :action => 'index' + elsif params[:reassign_to_id] + if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id]) + @enumeration.destroy(reassign_to) + redirect_to :action => 'index' + end + end + @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration] + #rescue + # flash[:error] = 'Unable to delete enumeration' + # redirect_to :action => 'index' end end diff --git a/groups/app/controllers/issue_categories_controller.rb b/groups/app/controllers/issue_categories_controller.rb index a73935b4f..8315d6eb8 100644 --- a/groups/app/controllers/issue_categories_controller.rb +++ b/groups/app/controllers/issue_categories_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueCategoriesController < ApplicationController - layout 'base' menu_item :settings before_filter :find_project, :authorize diff --git a/groups/app/controllers/issue_relations_controller.rb b/groups/app/controllers/issue_relations_controller.rb index cb0ad552a..2ca3f0d68 100644 --- a/groups/app/controllers/issue_relations_controller.rb +++ b/groups/app/controllers/issue_relations_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueRelationsController < ApplicationController - layout 'base' before_filter :find_project, :authorize def new diff --git a/groups/app/controllers/issue_statuses_controller.rb b/groups/app/controllers/issue_statuses_controller.rb index d0712e7c3..69d9db965 100644 --- a/groups/app/controllers/issue_statuses_controller.rb +++ b/groups/app/controllers/issue_statuses_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueStatusesController < ApplicationController - layout 'base' before_filter :require_admin verify :method => :post, :only => [ :destroy, :create, :update, :move ], diff --git a/groups/app/controllers/issues_controller.rb b/groups/app/controllers/issues_controller.rb index 322958b8c..76b851111 100644 --- a/groups/app/controllers/issues_controller.rb +++ b/groups/app/controllers/issues_controller.rb @@ -16,10 +16,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssuesController < ApplicationController - layout 'base' menu_item :new_issue, :only => :new - before_filter :find_issue, :only => [:show, :edit, :destroy_attachment] + before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment] before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] before_filter :find_project, :only => [:new, :update_form, :preview] before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu] @@ -43,6 +42,7 @@ class IssuesController < ApplicationController helper :sort include SortHelper include IssuesHelper + helper :timelog def index sort_init "#{Issue.table_name}.id", "desc" @@ -65,7 +65,7 @@ class IssuesController < ApplicationController :offset => @issue_pages.current.offset respond_to do |format| format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } - format.atom { render_feed(@issues, :title => l(:label_issue_plural)) } + format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') } end @@ -94,14 +94,13 @@ class IssuesController < ApplicationController end def show - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") @journals.each_with_index {|j,i| j.indice = i+1} @journals.reverse! if User.current.wants_comments_in_reverse_order? @allowed_statuses = @issue.new_statuses_allowed_to(User.current) @edit_allowed = User.current.allowed_to?(:edit_issues, @project) - @activities = Enumeration::get_values('ACTI') @priorities = Enumeration::get_values('IPRI') + @time_entry = TimeEntry.new respond_to do |format| format.html { render :template => 'issues/show.rhtml' } format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } @@ -112,15 +111,18 @@ class IssuesController < ApplicationController # Add a new issue # The new issue will be created from an existing one if copy_from parameter is given def new - @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue]) + @issue = Issue.new + @issue.copy_from(params[:copy_from]) if params[:copy_from] @issue.project = @project - @issue.author = User.current - @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first) + # Tracker must be set before custom field values + @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first) if @issue.tracker.nil? flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.' render :nothing => true, :layout => true return end + @issue.attributes = params[:issue] + @issue.author = User.current default_status = IssueStatus.default unless default_status @@ -133,22 +135,15 @@ class IssuesController < ApplicationController if request.get? || request.xhr? @issue.start_date ||= Date.today - @custom_values = @issue.custom_values.empty? ? - @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } : - @issue.custom_values else requested_status = (params[:issue] && params[:issue][:status_id] ? IssueStatus.find_by_id(params[:issue][:status_id]) : default_status) # Check that the user is allowed to apply the requested status @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, - :customized => @issue, - :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) } - @issue.custom_values = @custom_values if @issue.save attach_files(@issue, params[:attachments]) flash[:notice] = l(:notice_successful_create) Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') - redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project + redirect_to :controller => 'issues', :action => 'show', :id => @issue return end end @@ -162,10 +157,9 @@ class IssuesController < ApplicationController def edit @allowed_statuses = @issue.new_statuses_allowed_to(User.current) - @activities = Enumeration::get_values('ACTI') @priorities = Enumeration::get_values('IPRI') - @custom_values = [] @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + @time_entry = TimeEntry.new @notes = params[:notes] journal = @issue.init_journal(User.current, @notes) @@ -177,21 +171,14 @@ class IssuesController < ApplicationController @issue.attributes = attrs end - if request.get? - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } - else - # Update custom fields if user has :edit permission - if @edit_allowed && params[:custom_fields] - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } - @issue.custom_values = @custom_values - end + if request.post? + @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) + @time_entry.attributes = params[:time_entry] attachments = attach_files(@issue, params[:attachments]) attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} - if @issue.save + if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save # Log spend time if current_role.allowed_to?(:log_time) - @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) - @time_entry.attributes = params[:time_entry] @time_entry.save end if !journal.new_record? @@ -207,6 +194,26 @@ class IssuesController < ApplicationController flash.now[:error] = l(:notice_locking_conflict) end + def reply + journal = Journal.find(params[:journal_id]) if params[:journal_id] + if journal + user = journal.user + text = journal.notes + else + user = @issue.author + text = @issue.description + end + content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " + content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" + render(:update) { |page| + page.<< "$('notes').value = \"#{content}\";" + page.show 'update' + page << "Form.Element.focus('notes');" + page << "Element.scrollTo('update');" + page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;" + } + end + # Bulk edit a set of issues def bulk_edit if request.post? @@ -240,7 +247,7 @@ class IssuesController < ApplicationController else flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) end - redirect_to :controller => 'issues', :action => 'index', :project_id => @project + redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project}) return end # Find potential statuses the user could be allowed to switch issues to @@ -264,6 +271,7 @@ class IssuesController < ApplicationController new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) unsaved_issue_ids = [] @issues.each do |issue| + issue.init_journal(User.current) unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) end if unsaved_issue_ids.empty? @@ -318,19 +326,22 @@ class IssuesController < ApplicationController if (@issues.size == 1) @issue = @issues.first @allowed_statuses = @issue.new_statuses_allowed_to(User.current) - @assignables = @issue.assignable_users - @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) end projects = @issues.collect(&:project).compact.uniq @project = projects.first if projects.size == 1 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), - :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))), + :log_time => (@project && User.current.allowed_to?(:log_time, @project)), + :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), :move => (@project && User.current.allowed_to?(:move_issues, @project)), :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) } - + if @project + @assignables = @project.assignable_users + @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + end + @priorities = Enumeration.get_values('IPRI').reverse @statuses = IssueStatus.find(:all, :order => 'position') @back = request.env['HTTP_REFERER'] diff --git a/groups/app/controllers/journals_controller.rb b/groups/app/controllers/journals_controller.rb index 758b8507f..6df54f098 100644 --- a/groups/app/controllers/journals_controller.rb +++ b/groups/app/controllers/journals_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class JournalsController < ApplicationController - layout 'base' before_filter :find_journal def edit diff --git a/groups/app/controllers/mail_handler_controller.rb b/groups/app/controllers/mail_handler_controller.rb new file mode 100644 index 000000000..8bcfce630 --- /dev/null +++ b/groups/app/controllers/mail_handler_controller.rb @@ -0,0 +1,44 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MailHandlerController < ActionController::Base + before_filter :check_credential + + verify :method => :post, + :only => :index, + :render => { :nothing => true, :status => 405 } + + # Submits an incoming email to MailHandler + def index + options = params.dup + email = options.delete(:email) + if MailHandler.receive(email, options) + render :nothing => true, :status => :created + else + render :nothing => true, :status => :unprocessable_entity + end + end + + private + + def check_credential + User.current = nil + unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key + render :nothing => true, :status => 403 + end + end +end diff --git a/groups/app/controllers/members_controller.rb b/groups/app/controllers/members_controller.rb index 6a42ed05e..fcaede0a5 100644 --- a/groups/app/controllers/members_controller.rb +++ b/groups/app/controllers/members_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MembersController < ApplicationController - layout 'base' before_filter :find_member, :except => :new before_filter :find_project, :only => :new before_filter :authorize diff --git a/groups/app/controllers/messages_controller.rb b/groups/app/controllers/messages_controller.rb index 97cb2c3bf..554279d21 100644 --- a/groups/app/controllers/messages_controller.rb +++ b/groups/app/controllers/messages_controller.rb @@ -16,14 +16,15 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MessagesController < ApplicationController - layout 'base' menu_item :boards before_filter :find_board, :only => [:new, :preview] before_filter :find_message, :except => [:new, :preview] before_filter :authorize, :except => :preview verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } + verify :xhr => true, :only => :quote + helper :attachments include AttachmentsHelper @@ -83,6 +84,20 @@ class MessagesController < ApplicationController { :action => 'show', :id => @message.parent } end + def quote + user = @message.author + text = @message.content + content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " + content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" + render(:update) { |page| + page.<< "$('message_content').value = \"#{content}\";" + page.show 'reply' + page << "Form.Element.focus('message_content');" + page << "Element.scrollTo('reply');" + page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;" + } + end + def preview message = @board.messages.find_by_id(params[:id]) @attachements = message.attachments if message diff --git a/groups/app/controllers/my_controller.rb b/groups/app/controllers/my_controller.rb index ff3393e90..1cfa3e531 100644 --- a/groups/app/controllers/my_controller.rb +++ b/groups/app/controllers/my_controller.rb @@ -16,11 +16,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MyController < ApplicationController - helper :issues - - layout 'base' before_filter :require_login + helper :issues + BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, 'issuesreportedbyme' => :label_reported_issues, 'issueswatched' => :label_watched_issues, diff --git a/groups/app/controllers/news_controller.rb b/groups/app/controllers/news_controller.rb index c9ba6b991..b5f7ca1b2 100644 --- a/groups/app/controllers/news_controller.rb +++ b/groups/app/controllers/news_controller.rb @@ -16,9 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class NewsController < ApplicationController - layout 'base' before_filter :find_news, :except => [:new, :index, :preview] - before_filter :find_project, :only => :new + before_filter :find_project, :only => [:new, :preview] before_filter :authorize, :except => [:index, :preview] before_filter :find_optional_project, :only => :index accept_key_auth :index diff --git a/groups/app/controllers/projects_controller.rb b/groups/app/controllers/projects_controller.rb index ba6955221..747c26bd2 100644 --- a/groups/app/controllers/projects_controller.rb +++ b/groups/app/controllers/projects_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class ProjectsController < ApplicationController - layout 'base' menu_item :overview menu_item :activity, :only => :activity menu_item :roadmap, :only => :roadmap @@ -44,37 +43,36 @@ class ProjectsController < ApplicationController include RepositoriesHelper include ProjectsHelper - def index - list - render :action => 'list' unless request.xhr? - end - # Lists visible projects - def list + def index projects = Project.find :all, :conditions => Project.visible_by(User.current), :include => :parent - @project_tree = projects.group_by {|p| p.parent || p} - @project_tree.each_key {|p| @project_tree[p] -= [p]} + respond_to do |format| + format.html { + @project_tree = projects.group_by {|p| p.parent || p} + @project_tree.keys.each {|p| @project_tree[p] -= [p]} + } + format.atom { + render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), + :title => "#{Setting.app_title}: #{l(:label_project_latest)}") + } + end end # Add a new project def add - @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @trackers = Tracker.all @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", :order => 'name') @project = Project.new(params[:project]) if request.get? - @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) } @project.trackers = Tracker.all @project.is_public = Setting.default_projects_public? @project.enabled_module_names = Redmine::AccessControl.available_project_modules else - @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] - @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } - @project.custom_values = @custom_values @project.enabled_module_names = params[:enabled_modules] if @project.save flash[:notice] = l(:notice_successful_create) @@ -85,8 +83,7 @@ class ProjectsController < ApplicationController # Show @project def show - @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") - @subprojects = @project.active_children + @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current)) @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") @trackers = @project.rolled_up_trackers @@ -111,11 +108,10 @@ class ProjectsController < ApplicationController @root_projects = Project.find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], :order => 'name') - @custom_fields = IssueCustomField.find(:all) + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @issue_category ||= IssueCategory.new @member ||= @project.members.new @trackers = Tracker.all - @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } @repository ||= @project.repository @wiki ||= @project.wiki end @@ -123,10 +119,6 @@ class ProjectsController < ApplicationController # Edit @project def edit if request.post? - if params[:custom_fields] - @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) } - @project.custom_values = @custom_values - end @project.attributes = params[:project] if @project.save flash[:notice] = l(:notice_successful_update) @@ -232,91 +224,23 @@ class ProjectsController < ApplicationController @date_to ||= Date.today + 1 @date_from = @date_to - @days + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - @event_types = %w(issues news files documents changesets wiki_pages messages) - if @project - @event_types.delete('wiki_pages') unless @project.wiki - @event_types.delete('changesets') unless @project.repository - @event_types.delete('messages') unless @project.boards.any? - # only show what the user is allowed to view - @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} - @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - end - @scope = @event_types.select {|t| params["show_#{t}"]} - # default events if none is specified in parameters - @scope = (@event_types - %w(wiki_pages messages))if @scope.empty? - - @events = [] - - if @scope.include?('issues') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions) - - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{JournalDetail.table_name}.prop_key = 'status_id' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions) - end - - if @scope.include?('news') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions) - end - - if @scope.include?('files') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", - :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + - "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id", - :conditions => cond.conditions) - end - - if @scope.include?('documents') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Document.find(:all, :include => :project, :conditions => cond.conditions) - - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", - :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + - "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id", - :conditions => cond.conditions) - end - - if @scope.include?('wiki_pages') - select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + - "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + - "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + - "#{WikiContent.versioned_table_name}.id" - joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + - "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + - "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id" - - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions) - end + @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects) + @activity.scope_select {|t| !params["show_#{t}"].nil?} + @activity.default_scope! if @activity.scope.empty? - if @scope.include?('changesets') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions) - end - - if @scope.include?('messages') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions) - end - - @events_by_day = @events.group_by(&:event_date) + events = @activity.events(@date_from, @date_to) respond_to do |format| - format.html { render :layout => false if request.xhr? } - format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") } + format.html { + @events_by_day = events.group_by(&:event_date) + render :layout => false if request.xhr? + } + format.atom { + title = (@activity.scope.size == 1) ? l("label_#{@activity.scope.first.singularize}_plural") : l(:label_activity) + render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") + } end end @@ -381,11 +305,18 @@ class ProjectsController < ApplicationController @events = [] @project.issues_with_subprojects(@with_subprojects) do + # Issues that have start and due dates @events += Issue.find(:all, :order => "start_date, due_date", :include => [:tracker, :status, :assigned_to, :priority, :project], :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] ) unless @selected_tracker_ids.empty? + # Issues that don't have a due date but that are assigned to a version with a date + @events += Issue.find(:all, + :order => "start_date, effective_date", + :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], + :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] + ) unless @selected_tracker_ids.empty? @events += Version.find(:all, :include => :project, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to]) end diff --git a/groups/app/controllers/queries_controller.rb b/groups/app/controllers/queries_controller.rb index da2c4a2c8..8500e853a 100644 --- a/groups/app/controllers/queries_controller.rb +++ b/groups/app/controllers/queries_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class QueriesController < ApplicationController - layout 'base' menu_item :issues before_filter :find_query, :except => :new before_filter :find_optional_project, :only => :new diff --git a/groups/app/controllers/reports_controller.rb b/groups/app/controllers/reports_controller.rb index 338059a50..dd3ece930 100644 --- a/groups/app/controllers/reports_controller.rb +++ b/groups/app/controllers/reports_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class ReportsController < ApplicationController - layout 'base' menu_item :issues before_filter :find_project, :authorize diff --git a/groups/app/controllers/repositories_controller.rb b/groups/app/controllers/repositories_controller.rb index 64eb05793..2f96e2d66 100644 --- a/groups/app/controllers/repositories_controller.rb +++ b/groups/app/controllers/repositories_controller.rb @@ -23,20 +23,21 @@ class ChangesetNotFound < Exception; end class InvalidRevisionParam < Exception; end class RepositoriesController < ApplicationController - layout 'base' menu_item :repository before_filter :find_repository, :except => :edit before_filter :find_project, :only => :edit before_filter :authorize accept_key_auth :revisions + rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed + def edit @repository = @project.repository if !@repository @repository = Repository.factory(params[:repository_scm]) - @repository.project = @project + @repository.project = @project if @repository end - if request.post? + if request.post? && @repository @repository.attributes = params[:repository] @repository.save end @@ -56,8 +57,6 @@ class RepositoriesController < ApplicationController # latest changesets @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") show_error_not_found unless @entries || @changesets.any? - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def browse @@ -66,18 +65,16 @@ class RepositoriesController < ApplicationController @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) else show_error_not_found and return unless @entries + @properties = @repository.properties(@path, @rev) render :action => 'browse' end - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def changes - @entry = @repository.scm.entry(@path, @rev) + @entry = @repository.entry(@path, @rev) show_error_not_found and return unless @entry @changesets = @repository.changesets_for_path(@path) - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) + @properties = @repository.properties(@path, @rev) end def revisions @@ -96,13 +93,13 @@ class RepositoriesController < ApplicationController end def entry - @entry = @repository.scm.entry(@path, @rev) + @entry = @repository.entry(@path, @rev) show_error_not_found and return unless @entry # If the entry is a dir, show the browser browse and return if @entry.is_dir? - @content = @repository.scm.cat(@path, @rev) + @content = @repository.cat(@path, @rev) show_error_not_found and return unless @content if 'raw' == params[:format] || @content.is_binary_data? # Force the download if it's a binary file @@ -110,16 +107,12 @@ class RepositoriesController < ApplicationController else # Prevent empty lines when displaying a file with Windows style eol @content.gsub!("\r\n", "\n") - end - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) + end end def annotate @annotate = @repository.scm.annotate(@path, @rev) render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def revision @@ -137,27 +130,33 @@ class RepositoriesController < ApplicationController end rescue ChangesetNotFound show_error_not_found - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def diff - @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' - @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) - - # Save diff type as user preference - if User.current.logged? && @diff_type != User.current.pref[:diff_type] - User.current.pref[:diff_type] = @diff_type - User.current.preference.save - end - - @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}") - unless read_fragment(@cache_key) - @diff = @repository.diff(@path, @rev, @rev_to, @diff_type) - show_error_not_found unless @diff + if params[:format] == 'diff' + @diff = @repository.diff(@path, @rev, @rev_to) + show_error_not_found and return unless @diff + filename = "changeset_r#{@rev}" + filename << "_r#{@rev_to}" if @rev_to + send_data @diff.join, :filename => "#{filename}.diff", + :type => 'text/x-patch', + :disposition => 'attachment' + else + @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' + @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) + + # Save diff type as user preference + if User.current.logged? && @diff_type != User.current.pref[:diff_type] + User.current.pref[:diff_type] = @diff_type + User.current.preference.save + end + + @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}") + unless read_fragment(@cache_key) + @diff = @repository.diff(@path, @rev, @rev_to) + show_error_not_found unless @diff + end end - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def stats @@ -207,8 +206,9 @@ private render_error l(:error_scm_not_found) end - def show_error_command_failed(msg) - render_error l(:error_scm_command_failed, msg) + # Handler for Redmine::Scm::Adapters::CommandFailed exception + def show_error_command_failed(exception) + render_error l(:error_scm_command_failed, exception.message) end def graph_commits_per_month(repository) @@ -229,7 +229,7 @@ private graph = SVG::Graph::Bar.new( :height => 300, - :width => 500, + :width => 800, :fields => fields.reverse, :stack => :side, :scale_integers => true, @@ -271,8 +271,8 @@ private fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') } graph = SVG::Graph::BarHorizontal.new( - :height => 300, - :width => 500, + :height => 400, + :width => 800, :fields => fields, :stack => :side, :scale_integers => true, diff --git a/groups/app/controllers/roles_controller.rb b/groups/app/controllers/roles_controller.rb index 9fdd9701b..72555e5b0 100644 --- a/groups/app/controllers/roles_controller.rb +++ b/groups/app/controllers/roles_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class RolesController < ApplicationController - layout 'base' before_filter :require_admin verify :method => :post, :only => [ :destroy, :move ], diff --git a/groups/app/controllers/search_controller.rb b/groups/app/controllers/search_controller.rb index f15653b63..e6e66f05c 100644 --- a/groups/app/controllers/search_controller.rb +++ b/groups/app/controllers/search_controller.rb @@ -16,8 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class SearchController < ApplicationController - layout 'base' - before_filter :find_optional_project helper :messages @@ -29,6 +27,18 @@ class SearchController < ApplicationController @all_words = params[:all_words] || (params[:submit] ? false : true) @titles_only = !params[:titles_only].nil? + projects_to_search = + case params[:scope] + when 'all' + nil + when 'my_projects' + User.current.memberships.collect(&:project) + when 'subprojects' + @project ? ([ @project ] + @project.active_children) : nil + else + @project + end + offset = nil begin; offset = params[:offset].to_time if params[:offset]; rescue; end @@ -38,16 +48,16 @@ class SearchController < ApplicationController return end - if @project + @object_types = %w(issues news documents changesets wiki_pages messages projects) + if projects_to_search.is_a? Project + # don't search projects + @object_types.delete('projects') # only show what the user is allowed to view - @object_types = %w(issues news documents changesets wiki_pages messages) - @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} - - @scope = @object_types.select {|t| params[t]} - @scope = @object_types if @scope.empty? - else - @object_types = @scope = %w(projects) + @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)} end + + @scope = @object_types.select {|t| params[t]} + @scope = @object_types if @scope.empty? # extract tokens from the question # eg. hello "bye bye" => ["hello", "bye bye"] @@ -60,39 +70,34 @@ class SearchController < ApplicationController @tokens.slice! 5..-1 if @tokens.size > 5 # strings used in sql like statement like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} + @results = [] + @results_by_type = Hash.new {|h,k| h[k] = 0} + limit = 10 - if @project - @scope.each do |s| - @results += s.singularize.camelcase.constantize.search(like_tokens, @project, - :all_words => @all_words, - :titles_only => @titles_only, - :limit => (limit+1), - :offset => offset, - :before => params[:previous].nil?) - end - @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} - if params[:previous].nil? - @pagination_previous_date = @results[0].event_datetime if offset && @results[0] - if @results.size > limit - @pagination_next_date = @results[limit-1].event_datetime - @results = @results[0, limit] - end - else - @pagination_next_date = @results[-1].event_datetime if offset && @results[-1] - if @results.size > limit - @pagination_previous_date = @results[-(limit)].event_datetime - @results = @results[-(limit), limit] - end + @scope.each do |s| + r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search, + :all_words => @all_words, + :titles_only => @titles_only, + :limit => (limit+1), + :offset => offset, + :before => params[:previous].nil?) + @results += r + @results_by_type[s] += c + end + @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} + if params[:previous].nil? + @pagination_previous_date = @results[0].event_datetime if offset && @results[0] + if @results.size > limit + @pagination_next_date = @results[limit-1].event_datetime + @results = @results[0, limit] end else - operator = @all_words ? ' AND ' : ' OR ' - @results += Project.find(:all, - :limit => limit, - :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] - ) if @scope.include? 'projects' - # if only one project is found, user is redirected to its overview - redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 + @pagination_next_date = @results[-1].event_datetime if offset && @results[-1] + if @results.size > limit + @pagination_previous_date = @results[-(limit)].event_datetime + @results = @results[-(limit), limit] + end end else @question = "" diff --git a/groups/app/controllers/settings_controller.rb b/groups/app/controllers/settings_controller.rb index c7c8751dd..6482a3576 100644 --- a/groups/app/controllers/settings_controller.rb +++ b/groups/app/controllers/settings_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class SettingsController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -39,6 +38,7 @@ class SettingsController < ApplicationController end @options = {} @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } + @deliveries = ActionMailer::Base.perform_deliveries end def plugin @@ -49,7 +49,7 @@ class SettingsController < ApplicationController flash[:notice] = l(:notice_successful_update) redirect_to :action => 'plugin', :id => params[:id] end - @partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial] + @partial = @plugin.settings[:partial] @settings = Setting["plugin_#{plugin_id}"] end end diff --git a/groups/app/controllers/timelog_controller.rb b/groups/app/controllers/timelog_controller.rb index 29c2635d6..f331cdbe4 100644 --- a/groups/app/controllers/timelog_controller.rb +++ b/groups/app/controllers/timelog_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class TimelogController < ApplicationController - layout 'base' menu_item :issues before_filter :find_project, :authorize @@ -54,8 +53,15 @@ class TimelogController < ApplicationController } # Add list and boolean custom fields as available criterias - @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| - @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)", + @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| + @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)", + :format => cf.field_format, + :label => cf.name} + end + + # Add list and boolean time entry custom fields + TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf| + @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)", :format => cf.field_format, :label => cf.name} end @@ -154,6 +160,14 @@ class TimelogController < ApplicationController render :layout => !request.xhr? } + format.atom { + entries = TimeEntry.find(:all, + :include => [:project, :activity, :user, {:issue => :tracker}], + :conditions => cond.conditions, + :order => "#{TimeEntry.table_name}.created_on DESC", + :limit => Setting.feeds_limit.to_i) + render_feed(entries, :title => l(:label_spent_time)) + } format.csv { # Export all entries @entries = TimeEntry.find(:all, @@ -172,10 +186,9 @@ class TimelogController < ApplicationController @time_entry.attributes = params[:time_entry] if request.post? and @time_entry.save flash[:notice] = l(:notice_successful_update) - redirect_to(params[:back_url] || {:action => 'details', :project_id => @time_entry.project}) + redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url]) return end - @activities = Enumeration::get_values('ACTI') end def destroy diff --git a/groups/app/controllers/trackers_controller.rb b/groups/app/controllers/trackers_controller.rb index 3d7dbd5c5..8c02f9474 100644 --- a/groups/app/controllers/trackers_controller.rb +++ b/groups/app/controllers/trackers_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class TrackersController < ApplicationController - layout 'base' before_filter :require_admin def index diff --git a/groups/app/controllers/users_controller.rb b/groups/app/controllers/users_controller.rb index 3cd66d6a4..5c933c7de 100644 --- a/groups/app/controllers/users_controller.rb +++ b/groups/app/controllers/users_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class UsersController < ApplicationController - layout 'base' before_filter :require_admin helper :sort @@ -53,15 +52,12 @@ class UsersController < ApplicationController def add if request.get? @user = User.new(:language => Setting.default_language) - @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } else @user = User.new(params[:user]) @user.admin = params[:user][:admin] || false @user.login = params[:user][:login] @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id @user.group_id = params[:user][:group_id] - @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } - @user.custom_values = @custom_values if @user.save Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] flash[:notice] = l(:notice_successful_create) @@ -74,17 +70,11 @@ class UsersController < ApplicationController def edit @user = User.find(params[:id]) - if request.get? - @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } - else + if request.post? @user.admin = params[:user][:admin] if params[:user][:admin] @user.login = params[:user][:login] if params[:user][:login] @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id @user.group_id = params[:user][:group_id] if params[:user][:group_id] - if params[:custom_fields] - @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } - @user.custom_values = @custom_values - end if @user.update_attributes(params[:user]) flash[:notice] = l(:notice_successful_update) # Give a string to redirect_to otherwise it would use status param as the response code @@ -96,16 +86,15 @@ class UsersController < ApplicationController @roles = Role.find_all_givable @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects @membership ||= Member.new + @memberships = @user.memberships.select {|m| m.inherited_from.nil? } end def edit_membership @user = User.find(params[:id]) @membership = params[:membership_id] ? Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL') : Member.new(:principal => @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 + @membership.save if request.post? + redirect_to :action => 'edit', :id => @user, :tab => 'memberships' end def destroy_membership @@ -113,6 +102,6 @@ class UsersController < ApplicationController if request.post? and Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL').destroy flash[:notice] = l(:notice_successful_update) end - redirect_to :action => 'edit', :id => @user and return + redirect_to :action => 'edit', :id => @user, :tab => 'memberships' end end diff --git a/groups/app/controllers/versions_controller.rb b/groups/app/controllers/versions_controller.rb index aeb802ccb..ab2ccb773 100644 --- a/groups/app/controllers/versions_controller.rb +++ b/groups/app/controllers/versions_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class VersionsController < ApplicationController - layout 'base' menu_item :roadmap before_filter :find_project, :authorize @@ -37,15 +36,6 @@ class VersionsController < ApplicationController flash[:error] = "Unable to delete version" redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project end - - def download - @attachment = @version.attachments.find(params[:attachment_id]) - @attachment.increment_download - send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), - :type => @attachment.content_type - rescue - render_404 - end def destroy_file @version.attachments.find(params[:attachment_id]).destroy diff --git a/groups/app/controllers/watchers_controller.rb b/groups/app/controllers/watchers_controller.rb index 206dc0843..8e6ee3a9e 100644 --- a/groups/app/controllers/watchers_controller.rb +++ b/groups/app/controllers/watchers_controller.rb @@ -16,27 +16,38 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class WatchersController < ApplicationController - layout 'base' - before_filter :require_login, :find_project, :check_project_privacy + before_filter :find_project + before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch] + before_filter :authorize, :only => :new - def add - user = User.current - @watched.add_watcher(user) - respond_to do |format| - format.html { render :text => 'Watcher added.', :layout => true } - format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } - end + verify :method => :post, + :only => [ :watch, :unwatch ], + :render => { :nothing => true, :status => :method_not_allowed } + + def watch + set_watcher(User.current, true) end - def remove - user = User.current - @watched.remove_watcher(user) + def unwatch + set_watcher(User.current, false) + end + + def new + @watcher = Watcher.new(params[:watcher]) + @watcher.watchable = @watched + @watcher.save if request.post? respond_to do |format| - format.html { render :text => 'Watcher removed.', :layout => true } - format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + format.html { redirect_to :back } + format.js do + render :update do |page| + page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched} + end + end end + rescue ::ActionController::RedirectBackError + render :text => 'Watcher added.', :layout => true end - + private def find_project klass = Object.const_get(params[:object_type].camelcase) @@ -46,4 +57,14 @@ private rescue render_404 end + + def set_watcher(user, watching) + @watched.set_watcher(user, watching) + respond_to do |format| + format.html { redirect_to :back } + format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + end + rescue ::ActionController::RedirectBackError + render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true + end end diff --git a/groups/app/controllers/welcome_controller.rb b/groups/app/controllers/welcome_controller.rb index b4be7fb1c..b8108e8ac 100644 --- a/groups/app/controllers/welcome_controller.rb +++ b/groups/app/controllers/welcome_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class WelcomeController < ApplicationController - layout 'base' def index @news = News.latest User.current diff --git a/groups/app/controllers/wiki_controller.rb b/groups/app/controllers/wiki_controller.rb index 53c5ec53b..46df2931e 100644 --- a/groups/app/controllers/wiki_controller.rb +++ b/groups/app/controllers/wiki_controller.rb @@ -18,10 +18,9 @@ require 'diff' class WikiController < ApplicationController - layout 'base' before_filter :find_wiki, :authorize - verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index } + verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index } helper :attachments include AttachmentsHelper @@ -48,12 +47,14 @@ class WikiController < ApplicationController send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") return end + @editable = editable? render :action => 'show' end # edit an existing page or a new one def edit @page = @wiki.find_or_new_page(params[:page]) + return render_403 unless editable? @page.content = WikiContent.new(:page => @page) if @page.new_record? @content = @page.content_for_version(params[:version]) @@ -82,7 +83,8 @@ class WikiController < ApplicationController # rename a page def rename - @page = @wiki.find_page(params[:page]) + @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.redirect_existing_links = true # used to display the *original* title if some AR validation errors occur @original_title = @page.pretty_title @@ -92,6 +94,12 @@ class WikiController < ApplicationController end end + def protect + page = @wiki.find_page(params[:page]) + page.update_attribute :protected, params[:protected] + redirect_to :action => 'index', :id => @project, :page => page.title + end + # show page history def history @page = @wiki.find_page(params[:page]) @@ -122,6 +130,7 @@ class WikiController < ApplicationController # remove a wiki page and its history def destroy @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.destroy if @page redirect_to :action => 'special', :id => @project, :page => 'Page_index' end @@ -137,6 +146,7 @@ class WikiController < ApplicationController :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id", :order => 'title' @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} + @pages_by_parent_id = @pages.group_by(&:parent_id) # export wiki to a single html file when 'export' @pages = @wiki.pages.find :all, :order => 'title' @@ -152,19 +162,26 @@ class WikiController < ApplicationController def preview page = @wiki.find_page(params[:page]) - @attachements = page.attachments if page + # page is nil when previewing a new page + return render_403 unless page.nil? || editable?(page) + if page + @attachements = page.attachments + @previewed = page.content + end @text = params[:content][:text] render :partial => 'common/preview' end def add_attachment @page = @wiki.find_page(params[:page]) + return render_403 unless editable? attach_files(@page, params[:attachments]) redirect_to :action => 'index', :page => @page.title end def destroy_attachment @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.attachments.find(params[:attachment_id]).destroy redirect_to :action => 'index', :page => @page.title end @@ -178,4 +195,9 @@ private rescue ActiveRecord::RecordNotFound render_404 end + + # Returns true if the current user is allowed to edit the page, otherwise false + def editable?(page = @page) + page.editable_by?(User.current) + end end diff --git a/groups/app/controllers/wikis_controller.rb b/groups/app/controllers/wikis_controller.rb index 6054abd9a..215d39f4b 100644 --- a/groups/app/controllers/wikis_controller.rb +++ b/groups/app/controllers/wikis_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class WikisController < ApplicationController - layout 'base' menu_item :settings before_filter :find_project, :authorize diff --git a/groups/app/helpers/application_helper.rb b/groups/app/helpers/application_helper.rb index 47a251053..78e5bdc65 100644 --- a/groups/app/helpers/application_helper.rb +++ b/groups/app/helpers/application_helper.rb @@ -15,6 +15,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'coderay' +require 'coderay/helpers/file_type' + module ApplicationHelper include Redmine::WikiFormatting::Macros::Definitions @@ -31,6 +34,12 @@ module ApplicationHelper def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) end + + # Display a link to remote if user is authorized + def link_to_remote_if_authorized(name, options = {}, html_options = nil) + url = options[:url] || {} + link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) + end # Display a link to user's account page def link_to_user(user) @@ -38,9 +47,23 @@ module ApplicationHelper end def link_to_issue(issue, options={}) + options[:class] ||= '' + options[:class] << ' issue' + options[:class] << ' closed' if issue.closed? link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options end + # Generates a link to an attachment. + # Options: + # * :text - Link text (default to attachment filename) + # * :download - Force download (default: false) + def link_to_attachment(attachment, options={}) + text = options.delete(:text) || attachment.filename + action = options.delete(:download) ? 'download' : 'show' + + link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) + end + def toggle_link(name, id, options={}) onclick = "Element.toggle('#{id}'); " onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") @@ -48,14 +71,6 @@ module ApplicationHelper link_to(name, "#", :onclick => onclick) end - def show_and_goto_link(name, id, options={}) - onclick = "Element.show('#{id}'); " - onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") - onclick << "Element.scrollTo('#{id}'); " - onclick << "return false;" - link_to(name, "#", options.merge(:onclick => onclick)) - end - def image_to_function(name, function, html_options = {}) html_options.symbolize_keys! tag(:input, html_options.merge({ @@ -80,23 +95,25 @@ module ApplicationHelper return nil unless time time = time.to_time if time.is_a?(String) zone = User.current.time_zone - if time.utc? - local = zone ? zone.adjust(time) : time.getlocal - else - local = zone ? zone.adjust(time.getutc) : time - end + local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time) @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) end + # Truncates and returns the string as a single line + def truncate_single_line(string, *args) + truncate(string, *args).gsub(%r{[\r\n]+}m, ' ') + end + def html_hours(text) text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') end def authoring(created, author) time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) - l(:label_added_time_by, author || 'Anonymous', time_tag) + author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous') + l(:label_added_time_by, author_tag, time_tag) end def l_or_humanize(s) @@ -111,6 +128,15 @@ module ApplicationHelper l(:actionview_datehelper_select_month_names).split(',')[month-1] end + def syntax_highlight(name, content) + type = CodeRay::FileType[name] + type ? CodeRay.scan(content, type).html : h(content) + end + + def to_path_param(path) + path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} + end + def pagination_links_full(paginator, count=nil, options={}) page_param = options.delete(:page_param) || :page url_param = params.dup @@ -157,7 +183,8 @@ module ApplicationHelper end def breadcrumb(*args) - content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') + elements = args.flatten + elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil end def html_title(*args) @@ -185,7 +212,7 @@ module ApplicationHelper options = args.last.is_a?(Hash) ? args.pop : {} case args.size when 1 - obj = nil + obj = options[:object] text = args.shift when 2 obj = args.shift @@ -225,12 +252,12 @@ module ApplicationHelper case options[:wiki_links] when :local # used for local links to html files - format_wiki_link = Proc.new {|project, title| "#{title}.html" } + format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" } when :anchor # used for single-file wiki export - format_wiki_link = Proc.new {|project, title| "##{title}" } + format_wiki_link = Proc.new {|project, title, anchor| "##{title}" } else - format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) } + format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) } end project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) @@ -256,9 +283,14 @@ module ApplicationHelper end if link_project && link_project.wiki + # extract anchor + anchor = nil + if page =~ /^(.+?)\#(.+)$/ + page, anchor = $1, $2 + end # check if page exists wiki_page = link_project.wiki.find_page(page) - link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)), + link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor), :class => ('wiki-page' + (wiki_page ? '' : ' new'))) else # project or wiki doesn't exist @@ -293,7 +325,9 @@ module ApplicationHelper # source:some/file#L120 -> Link to line 120 of the file # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 # export:some/file -> Force the download of the file - text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m| + # Forum messages: + # message#1218 -> Link to message with id 1218 + text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m| leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8 link = nil if esc.nil? @@ -301,7 +335,7 @@ module ApplicationHelper if project && (changeset = project.changesets.find_by_revision(oid)) link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid}, :class => 'changeset', - :title => truncate(changeset.comments, 100)) + :title => truncate_single_line(changeset.comments, 100)) end elsif sep == '#' oid = oid.to_i @@ -323,6 +357,16 @@ module ApplicationHelper link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, :class => 'version' end + when 'message' + if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current)) + link = link_to h(truncate(message.subject, 60)), {:only_path => only_path, + :controller => 'messages', + :action => 'show', + :board_id => message.board, + :id => message.root, + :anchor => (message.parent ? "message-#{message.id}" : nil)}, + :class => 'message' + end end elsif sep == ':' # removes the double quotes if any @@ -340,13 +384,16 @@ module ApplicationHelper end when 'commit' if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"])) - link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, :class => 'changeset', :title => truncate(changeset.comments, 100) + link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, + :class => 'changeset', + :title => truncate_single_line(changeset.comments, 100) end when 'source', 'export' if project && project.repository name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} path, rev, anchor = $1, $3, $5 - link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :path => path, + link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, + :path => to_path_param(path), :rev => rev, :anchor => anchor, :format => (prefix == 'export' ? 'raw' : nil)}, @@ -428,7 +475,8 @@ module ApplicationHelper end def back_url_hidden_field_tag - hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER']) + back_url = params[:back_url] || request.env['HTTP_REFERER'] + hidden_field_tag('back_url', back_url) unless back_url.blank? end def check_all_links(form_name) diff --git a/groups/app/helpers/attachments_helper.rb b/groups/app/helpers/attachments_helper.rb index 989cd3e66..ebf417bab 100644 --- a/groups/app/helpers/attachments_helper.rb +++ b/groups/app/helpers/attachments_helper.rb @@ -22,4 +22,8 @@ module AttachmentsHelper render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options} end end + + def to_utf8(str) + str + end end diff --git a/groups/app/helpers/custom_fields_helper.rb b/groups/app/helpers/custom_fields_helper.rb index aae105b72..ed0c4126f 100644 --- a/groups/app/helpers/custom_fields_helper.rb +++ b/groups/app/helpers/custom_fields_helper.rb @@ -19,6 +19,7 @@ module CustomFieldsHelper def custom_fields_tabs tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural}, + {:name => 'TimeEntryCustomField', :label => :label_spent_time}, {:name => 'ProjectCustomField', :label => :label_project_plural}, {:name => 'UserCustomField', :label => :label_user_plural}, {:name => 'GroupCustomField', :label => :label_group_plural} @@ -26,37 +27,40 @@ module CustomFieldsHelper end # Return custom field html tag corresponding to its format - def custom_field_tag(custom_value) + def custom_field_tag(name, custom_value) custom_field = custom_value.custom_field - field_name = "custom_fields[#{custom_field.id}]" - field_id = "custom_fields_#{custom_field.id}" + field_name = "#{name}[custom_field_values][#{custom_field.id}]" + field_id = "#{name}_custom_field_values_#{custom_field.id}" case custom_field.field_format when "date" - text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) + + text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) + calendar_for(field_id) when "text" - text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%' + text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%') when "bool" - check_box 'custom_value', 'value', :name => field_name, :id => field_id + check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0') when "list" - select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id + blank_option = custom_field.is_required? ? + (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') : + '<option></option>' + select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id) else - text_field 'custom_value', 'value', :name => field_name, :id => field_id + text_field_tag(field_name, custom_value.value, :id => field_id) end end # Return custom field label tag - def custom_field_label_tag(custom_value) + def custom_field_label_tag(name, custom_value) content_tag "label", custom_value.custom_field.name + (custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""), - :for => "custom_fields_#{custom_value.custom_field.id}", + :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}", :class => (custom_value.errors.empty? ? nil : "error" ) end # Return custom field tag with its label tag - def custom_field_tag_with_label(custom_value) - custom_field_label_tag(custom_value) + custom_field_tag(custom_value) + def custom_field_tag_with_label(name, custom_value) + custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value) end # Return a string used to display a custom value diff --git a/groups/app/helpers/issues_helper.rb b/groups/app/helpers/issues_helper.rb index 6013f1ec8..c6de00c10 100644 --- a/groups/app/helpers/issues_helper.rb +++ b/groups/app/helpers/issues_helper.rb @@ -54,9 +54,15 @@ module IssuesHelper when 'due_date', 'start_date' value = format_date(detail.value.to_date) if detail.value old_value = format_date(detail.old_value.to_date) if detail.old_value + when 'project_id' + p = Project.find_by_id(detail.value) and value = p.name if detail.value + p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value when 'status_id' s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value + when 'tracker_id' + t = Tracker.find_by_id(detail.value) and value = t.name if detail.value + t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value when 'assigned_to_id' u = User.find_by_id(detail.value) and value = u.name if detail.value u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value @@ -69,6 +75,9 @@ module IssuesHelper when 'fixed_version_id' v = Version.find_by_id(detail.value) and value = v.name if detail.value v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value + when 'estimated_hours' + value = "%0.02f" % detail.value.to_f unless detail.value.blank? + old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? end when 'cf' custom_field = CustomField.find_by_id(detail.prop_key) @@ -89,9 +98,9 @@ module IssuesHelper label = content_tag('strong', label) old_value = content_tag("i", h(old_value)) if detail.old_value old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?) - if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key) + if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key) # Link to the attachment if it has not been removed - value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key) + value = link_to_attachment(a) else value = content_tag("i", h(value)) if value end @@ -120,6 +129,7 @@ module IssuesHelper def issues_to_csv(issues, project = nil) ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + decimal_separator = l(:general_csv_decimal_separator) export = StringIO.new CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| # csv header fields @@ -142,7 +152,7 @@ module IssuesHelper ] # Export project custom fields if project is given # otherwise export custom fields marked as "For all projects" - custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields + custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields custom_fields.each {|f| headers << f.name} # Description in the last column headers << l(:field_description) @@ -162,7 +172,7 @@ module IssuesHelper format_date(issue.start_date), format_date(issue.due_date), issue.done_ratio, - issue.estimated_hours, + issue.estimated_hours.to_s.gsub('.', decimal_separator), format_time(issue.created_on), format_time(issue.updated_on) ] diff --git a/groups/app/helpers/journals_helper.rb b/groups/app/helpers/journals_helper.rb index 234bfabc0..45579f771 100644 --- a/groups/app/helpers/journals_helper.rb +++ b/groups/app/helpers/journals_helper.rb @@ -19,13 +19,16 @@ module JournalsHelper def render_notes(journal, options={}) content = '' editable = journal.editable_by?(User.current) - if editable && !journal.notes.blank? - links = [] + links = [] + if !journal.notes.blank? + links << link_to_remote(image_tag('comment.png'), + { :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} }, + :title => l(:button_quote)) if options[:reply_links] links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", { :controller => 'journals', :action => 'edit', :id => journal }, - :title => l(:button_edit)) - content << content_tag('div', links.join(' '), :class => 'contextual') + :title => l(:button_edit)) if editable end + content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty? content << textilizable(journal, :notes) content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki')) end diff --git a/groups/app/helpers/mail_handler_helper.rb b/groups/app/helpers/mail_handler_helper.rb new file mode 100644 index 000000000..a29a6dd5a --- /dev/null +++ b/groups/app/helpers/mail_handler_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module MailHandlerHelper +end diff --git a/groups/app/helpers/projects_helper.rb b/groups/app/helpers/projects_helper.rb index 0a076be99..23e4ae6c7 100644 --- a/groups/app/helpers/projects_helper.rb +++ b/groups/app/helpers/projects_helper.rb @@ -21,18 +21,22 @@ module ProjectsHelper link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options end + def format_activity_title(text) + h(truncate_single_line(text, 100)) + end + def format_activity_day(date) date == Date.today ? l(:label_today).titleize : format_date(date) end def format_activity_description(text) - h(truncate(text, 250)) + h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...')) end # Renders the member list displayed on project overview def render_member_list(project) parts = [] - memberships_by_role = project.memberships.find(:all, :include => :role, :order => 'position').group_by {|m| m.role} + memberships_by_role = project.memberships.find(:all, :include => :role, :order => "#{Role.table_name}.position").group_by {|m| m.role} memberships_by_role.keys.sort.each do |role| role_parts = [] # Display group name (with its 5 first users) or user name diff --git a/groups/app/helpers/repositories_helper.rb b/groups/app/helpers/repositories_helper.rb index 22bdec9df..852ed18d7 100644 --- a/groups/app/helpers/repositories_helper.rb +++ b/groups/app/helpers/repositories_helper.rb @@ -15,20 +15,23 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -require 'coderay' -require 'coderay/helpers/file_type' require 'iconv' module RepositoriesHelper - def syntax_highlight(name, content) - type = CodeRay::FileType[name] - type ? CodeRay.scan(content, type).html : h(content) - end - def format_revision(txt) txt.to_s[0,8] end + def render_properties(properties) + unless properties.nil? || properties.empty? + content = '' + properties.keys.sort.each do |property| + content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>") + end + content_tag('ul', content, :class => 'properties') + end + end + def to_utf8(str) return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip) @@ -48,10 +51,13 @@ module RepositoriesHelper end def scm_select_tag(repository) - container = [[]] - REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]} + scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']] + REDMINE_SUPPORTED_SCM.each do |scm| + scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm) + end + select_tag('repository_scm', - options_for_select(container, repository.class.name.demodulize), + options_for_select(scm_options, repository.class.name.demodulize), :disabled => (repository && !repository.new_record?), :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)") ) @@ -95,4 +101,8 @@ module RepositoriesHelper def bazaar_field_tags(form, repository) content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) end + + def filesystem_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) + end end diff --git a/groups/app/helpers/search_helper.rb b/groups/app/helpers/search_helper.rb index ed2f40b69..cd96dbd3f 100644 --- a/groups/app/helpers/search_helper.rb +++ b/groups/app/helpers/search_helper.rb @@ -18,7 +18,8 @@ module SearchHelper def highlight_tokens(text, tokens) return text unless text && tokens && !tokens.empty? - regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE + re_tokens = tokens.collect {|t| Regexp.escape(t)} + regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE result = '' text.split(regexp).each_with_index do |words, i| if result.length > 1200 @@ -35,4 +36,28 @@ module SearchHelper end result end + + def type_label(t) + l("label_#{t.singularize}_plural") + end + + def project_select_tag + options = [[l(:label_project_all), 'all']] + options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? + options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty? + options << [@project.name, ''] unless @project.nil? + select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 + end + + def render_results_by_type(results_by_type) + links = [] + # Sorts types by results count + results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t| + c = results_by_type[t] + next if c == 0 + text = "#{type_label(t)} (#{c})" + links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1) + end + ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty? + end end diff --git a/groups/app/helpers/settings_helper.rb b/groups/app/helpers/settings_helper.rb index f4ec5a7a7..d88269f7d 100644 --- a/groups/app/helpers/settings_helper.rb +++ b/groups/app/helpers/settings_helper.rb @@ -21,6 +21,7 @@ module SettingsHelper {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication}, {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking}, {:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)}, + {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)}, {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural} ] end diff --git a/groups/app/helpers/sort_helper.rb b/groups/app/helpers/sort_helper.rb index f16ff3c7d..9ca5c11bd 100644 --- a/groups/app/helpers/sort_helper.rb +++ b/groups/app/helpers/sort_helper.rb @@ -83,7 +83,7 @@ module SortHelper # Use this to sort the controller's table items collection. # def sort_clause() - session[@sort_name][:key] + ' ' + session[@sort_name][:order] + session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC') end # Returns a link which sorts by the named column. diff --git a/groups/app/helpers/timelog_helper.rb b/groups/app/helpers/timelog_helper.rb index db13556a1..2c1225a7c 100644 --- a/groups/app/helpers/timelog_helper.rb +++ b/groups/app/helpers/timelog_helper.rb @@ -16,6 +16,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module TimelogHelper + def activity_collection_for_select_options + activities = Enumeration::get_values('ACTI') + collection = [] + collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default) + activities.each { |a| collection << [a.name, a.id] } + collection + end + def select_hours(data, criteria, value) data.select {|row| row[criteria] == value} end @@ -44,6 +52,8 @@ module TimelogHelper def entries_to_csv(entries) ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + decimal_separator = l(:general_csv_decimal_separator) + custom_fields = TimeEntryCustomField.find(:all) export = StringIO.new CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| # csv header fields @@ -57,6 +67,9 @@ module TimelogHelper l(:field_hours), l(:field_comments) ] + # Export custom fields + headers += custom_fields.collect(&:name) + csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } # csv lines entries.each do |entry| @@ -67,9 +80,11 @@ module TimelogHelper (entry.issue ? entry.issue.id : nil), (entry.issue ? entry.issue.tracker : nil), (entry.issue ? entry.issue.subject : nil), - entry.hours, + entry.hours.to_s.gsub('.', decimal_separator), entry.comments ] + fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) } + csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } end end diff --git a/groups/app/helpers/users_helper.rb b/groups/app/helpers/users_helper.rb index 250ed8ce8..5b113e880 100644 --- a/groups/app/helpers/users_helper.rb +++ b/groups/app/helpers/users_helper.rb @@ -16,11 +16,26 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module UsersHelper - def status_options_for_select(selected) + def users_status_options_for_select(selected) + user_count_by_status = User.count(:group => 'status').to_hash options_for_select([[l(:label_all), ''], - [l(:status_active), 1], - [l(:status_registered), 2], - [l(:status_locked), 3]], selected) + ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1], + ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2], + ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected) + end + + # Options for the new membership projects combo-box + def projects_options_for_select(projects) + options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + projects_by_root = projects.group_by(&:root) + projects_by_root.keys.sort.each do |root| + options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root))) + projects_by_root[root].sort.each do |project| + next if project == root + options << content_tag('option', '» ' + h(project.name), :value => project.id) + end + end + options end def change_status_link(user) @@ -30,8 +45,14 @@ module UsersHelper link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock' elsif user.registered? link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock' - else + elsif user != User.current link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock' end end + + def user_settings_tabs + tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, + {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} + ] + end end diff --git a/groups/app/helpers/watchers_helper.rb b/groups/app/helpers/watchers_helper.rb index c83c785fc..f4767ebed 100644 --- a/groups/app/helpers/watchers_helper.rb +++ b/groups/app/helpers/watchers_helper.rb @@ -24,7 +24,7 @@ module WatchersHelper return '' unless user && user.logged? && object.respond_to?('watched_by?') watched = object.watched_by?(user) url = {:controller => 'watchers', - :action => (watched ? 'remove' : 'add'), + :action => (watched ? 'unwatch' : 'watch'), :object_type => object.class.to_s.underscore, :object_id => object.id} link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), @@ -33,4 +33,9 @@ module WatchersHelper :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off')) end + + # Returns a comma separated list of users watching the given object + def watchers_list(object) + object.watcher_users.collect {|u| content_tag('span', link_to_user(u), :class => 'user') }.join(",\n") + end end diff --git a/groups/app/helpers/wiki_helper.rb b/groups/app/helpers/wiki_helper.rb index 980035bd4..0a6b810de 100644 --- a/groups/app/helpers/wiki_helper.rb +++ b/groups/app/helpers/wiki_helper.rb @@ -17,6 +17,22 @@ module WikiHelper + def render_page_hierarchy(pages, node=nil) + content = '' + if pages[node] + content << "<ul class=\"pages-hierarchy\">\n" + pages[node].each do |page| + content << "<li>" + content << link_to(h(page.pretty_title), {:action => 'index', :page => page.title}, + :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) + content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id] + content << "</li>\n" + end + content << "</ul>\n" + end + content + end + def html_diff(wdiff) words = wdiff.words.collect{|word| h(word)} words_add = 0 diff --git a/groups/app/models/attachment.rb b/groups/app/models/attachment.rb index 08f440816..95ba8491f 100644 --- a/groups/app/models/attachment.rb +++ b/groups/app/models/attachment.rb @@ -26,7 +26,19 @@ class Attachment < ActiveRecord::Base validates_length_of :disk_filename, :maximum => 255 acts_as_event :title => :filename, - :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}} + :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} + + acts_as_activity_provider :type => 'files', + :permission => :view_files, + :find_options => {:select => "#{Attachment.table_name}.*", + :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + + "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"} + + acts_as_activity_provider :type => 'documents', + :permission => :view_documents, + :find_options => {:select => "#{Attachment.table_name}.*", + :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + + "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} cattr_accessor :storage_path @@storage_path = "#{RAILS_ROOT}/files" @@ -40,7 +52,7 @@ class Attachment < ActiveRecord::Base @temp_file = incoming_file if @temp_file.size > 0 self.filename = sanitize_filename(@temp_file.original_filename) - self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename + self.disk_filename = Attachment.disk_filename(filename) self.content_type = @temp_file.content_type.to_s.chomp self.filesize = @temp_file.size end @@ -68,9 +80,7 @@ class Attachment < ActiveRecord::Base # Deletes file on the disk def after_destroy - if self.filename? - File.delete(diskfile) if File.exist?(diskfile) - end + File.delete(diskfile) if !filename.blank? && File.exist?(diskfile) end # Returns file's location on disk @@ -90,6 +100,14 @@ class Attachment < ActiveRecord::Base self.filename =~ /\.(jpe?g|gif|png)$/i end + def is_text? + Redmine::MimeType.is_type?('text', filename) + end + + def is_diff? + self.filename =~ /\.(patch|diff)$/i + end + private def sanitize_filename(value) # get only the filename, not the whole path @@ -100,4 +118,17 @@ private # Finally, replace all non alphanumeric, hyphens or periods with underscore @filename = just_filename.gsub(/[^\w\.\-]/,'_') end + + # Returns an ASCII or hashed filename + def self.disk_filename(filename) + df = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} + df << filename + else + df << Digest::MD5.hexdigest(filename) + # keep the extension if any + df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$} + end + df + end end diff --git a/groups/app/models/auth_source.rb b/groups/app/models/auth_source.rb index 47c121a13..a0a2cdc5f 100644 --- a/groups/app/models/auth_source.rb +++ b/groups/app/models/auth_source.rb @@ -20,10 +20,7 @@ class AuthSource < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name - validates_length_of :name, :host, :maximum => 60 - validates_length_of :account_password, :maximum => 60, :allow_nil => true - validates_length_of :account, :base_dn, :maximum => 255 - validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30 + validates_length_of :name, :maximum => 60 def authenticate(login, password) end diff --git a/groups/app/models/auth_source_ldap.rb b/groups/app/models/auth_source_ldap.rb index a438bd3c7..655ffd6d5 100644 --- a/groups/app/models/auth_source_ldap.rb +++ b/groups/app/models/auth_source_ldap.rb @@ -20,7 +20,10 @@ require 'iconv' class AuthSourceLdap < AuthSource validates_presence_of :host, :port, :attr_login - validates_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? } + validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true + validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true + validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true + validates_numericality_of :port, :only_integer => true def after_initialize self.port = 389 if self.port == 0 diff --git a/groups/app/models/change.rb b/groups/app/models/change.rb index d14f435a4..385fe5acb 100644 --- a/groups/app/models/change.rb +++ b/groups/app/models/change.rb @@ -19,4 +19,8 @@ class Change < ActiveRecord::Base belongs_to :changeset validates_presence_of :changeset_id, :action, :path + + def relative_path + changeset.repository.relative_path(path) + end end diff --git a/groups/app/models/changeset.rb b/groups/app/models/changeset.rb index 3e95ce111..c4258c88b 100644 --- a/groups/app/models/changeset.rb +++ b/groups/app/models/changeset.rb @@ -15,6 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'iconv' + class Changeset < ActiveRecord::Base belongs_to :repository has_many :changes, :dependent => :delete_all @@ -27,9 +29,12 @@ class Changeset < ActiveRecord::Base :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} acts_as_searchable :columns => 'comments', - :include => :repository, + :include => {:repository => :project}, :project_key => "#{Repository.table_name}.project_id", :date_column => 'committed_on' + + acts_as_activity_provider :timestamp => "#{table_name}.committed_on", + :find_options => {:include => {:repository => :project}} validates_presence_of :repository_id, :revision, :committed_on, :commit_date validates_uniqueness_of :revision, :scope => :repository_id @@ -40,7 +45,7 @@ class Changeset < ActiveRecord::Base end def comments=(comment) - write_attribute(:comments, comment.strip) + write_attribute(:comments, Changeset.normalize_comments(comment)) end def committed_on=(date) @@ -75,7 +80,7 @@ class Changeset < ActiveRecord::Base if ref_keywords.delete('*') # find any issue ID in the comments target_issue_ids = [] - comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] } + comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] } referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids) end @@ -128,4 +133,24 @@ class Changeset < ActiveRecord::Base def next @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC') end + + # Strips and reencodes a commit log before insertion into the database + def self.normalize_comments(str) + to_utf8(str.to_s.strip) + end + + private + + def self.to_utf8(str) + return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii + encoding = Setting.commit_logs_encoding.to_s.strip + unless encoding.blank? || encoding == 'UTF-8' + begin + return Iconv.conv('UTF-8', encoding, str) + rescue Iconv::Failure + # do nothing here + end + end + str + end end diff --git a/groups/app/models/custom_field.rb b/groups/app/models/custom_field.rb index 990adf9e2..4759b714b 100644 --- a/groups/app/models/custom_field.rb +++ b/groups/app/models/custom_field.rb @@ -30,9 +30,9 @@ class CustomField < ActiveRecord::Base }.freeze validates_presence_of :name, :field_format - validates_uniqueness_of :name + validates_uniqueness_of :name, :scope => :type validates_length_of :name, :maximum => 30 - validates_format_of :name, :with => /^[\w\s\'\-]*$/i + validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys def initialize(attributes = nil) @@ -66,7 +66,7 @@ class CustomField < ActiveRecord::Base # to move in project_custom_field def self.for_all - find(:all, :conditions => ["is_for_all=?", true]) + find(:all, :conditions => ["is_for_all=?", true], :order => 'position') end def type_name diff --git a/groups/app/models/custom_value.rb b/groups/app/models/custom_value.rb index 98ce6b168..1d453baf0 100644 --- a/groups/app/models/custom_value.rb +++ b/groups/app/models/custom_value.rb @@ -25,6 +25,11 @@ class CustomValue < ActiveRecord::Base end end + # Returns true if the boolean custom value is true + def true? + self.value == '1' + end + protected def validate if value.blank? diff --git a/groups/app/models/document.rb b/groups/app/models/document.rb index 7a432b46b..627a2418f 100644 --- a/groups/app/models/document.rb +++ b/groups/app/models/document.rb @@ -20,11 +20,12 @@ class Document < ActiveRecord::Base belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" has_many :attachments, :as => :container, :dependent => :destroy - acts_as_searchable :columns => ['title', 'description'] + acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil }, :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}} - + acts_as_activity_provider :find_options => {:include => :project} + validates_presence_of :project, :title, :category validates_length_of :title, :maximum => 60 end diff --git a/groups/app/models/enumeration.rb b/groups/app/models/enumeration.rb index 400681a43..d32a0c049 100644 --- a/groups/app/models/enumeration.rb +++ b/groups/app/models/enumeration.rb @@ -23,12 +23,12 @@ class Enumeration < ActiveRecord::Base validates_presence_of :opt, :name validates_uniqueness_of :name, :scope => [:opt] validates_length_of :name, :maximum => 30 - validates_format_of :name, :with => /^[\w\s\'\-]*$/i + # Single table inheritance would be an option OPTIONS = { - "IPRI" => :enumeration_issue_priorities, - "DCAT" => :enumeration_doc_categories, - "ACTI" => :enumeration_activities + "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id}, + "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id}, + "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id} }.freeze def self.get_values(option) @@ -40,13 +40,32 @@ class Enumeration < ActiveRecord::Base end def option_name - OPTIONS[self.opt] + OPTIONS[self.opt][:label] end def before_save Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default? end + def objects_count + OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}") + end + + def in_use? + self.objects_count != 0 + end + + alias :destroy_without_reassign :destroy + + # Destroy the enumeration + # If a enumeration is specified, objects are reassigned + def destroy(reassign_to = nil) + if reassign_to && reassign_to.is_a?(Enumeration) + OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}") + end + destroy_without_reassign + end + def <=>(enumeration) position <=> enumeration.position end @@ -55,13 +74,6 @@ class Enumeration < ActiveRecord::Base private def check_integrity - case self.opt - when "IPRI" - raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id]) - when "DCAT" - raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id]) - when "ACTI" - raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id]) - end + raise "Can't delete enumeration" if self.in_use? end end diff --git a/groups/app/models/issue.rb b/groups/app/models/issue.rb index 8082e43b7..4701e41f1 100644 --- a/groups/app/models/issue.rb +++ b/groups/app/models/issue.rb @@ -28,23 +28,26 @@ class Issue < ActiveRecord::Base has_many :journals, :as => :journalized, :dependent => :destroy has_many :attachments, :as => :container, :dependent => :destroy has_many :time_entries, :dependent => :delete_all - has_many :custom_values, :dependent => :delete_all, :as => :customized - has_many :custom_fields, :through => :custom_values - has_and_belongs_to_many :changesets, :order => "revision ASC" + has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all + acts_as_customizable acts_as_watchable - acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue} + acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], + :include => [:project, :journals], + # sort by id so that limited eager loading doesn't break with postgresql + :order_column => "#{table_name}.id" acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} + acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]} + validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates_numericality_of :estimated_hours, :allow_nil => true - validates_associated :custom_values, :on => :update def after_initialize if new_record? @@ -54,6 +57,11 @@ class Issue < ActiveRecord::Base end end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : [] + end + def copy_from(arg) issue = arg.is_a?(Issue) ? arg : Issue.find(arg) self.attributes = issue.attributes.dup @@ -71,7 +79,9 @@ class Issue < ActiveRecord::Base self.relations_to.clear end # issue is moved to another project - self.category = nil + # reassign to the category with same name if any + new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name) + self.category = new_category self.fixed_version = nil self.project = new_project end @@ -168,17 +178,14 @@ class Issue < ActiveRecord::Base end end - def custom_value_for(custom_field) - self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id } - return nil - end - def init_journal(user, notes = "") @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) @issue_before_change = self.clone @issue_before_change.status = self.status @custom_values_before_change = {} self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } + # Make sure updated_on is updated when adding a note. + updated_on_will_change! @current_journal end @@ -225,9 +232,15 @@ class Issue < ActiveRecord::Base dependencies end - # Returns an array of the duplicate issues + # Returns an array of issues that duplicate this one def duplicates - relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)} + relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} + end + + # Returns the due date or the target due date if any + # Used on gantt chart + def due_before + due_date || (fixed_version ? fixed_version.effective_date : nil) end def duration diff --git a/groups/app/models/issue_relation.rb b/groups/app/models/issue_relation.rb index 07e940b85..49329e0bb 100644 --- a/groups/app/models/issue_relation.rb +++ b/groups/app/models/issue_relation.rb @@ -25,7 +25,7 @@ class IssueRelation < ActiveRecord::Base TYPE_PRECEDES = "precedes" TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, - TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 }, + TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, }.freeze diff --git a/groups/app/models/journal.rb b/groups/app/models/journal.rb index 1376d349e..71a51290b 100644 --- a/groups/app/models/journal.rb +++ b/groups/app/models/journal.rb @@ -25,17 +25,18 @@ class Journal < ActiveRecord::Base has_many :details, :class_name => "JournalDetail", :dependent => :delete_all attr_accessor :indice - acts_as_searchable :columns => 'notes', - :include => :issue, - :project_key => "#{Issue.table_name}.project_id", - :date_column => "#{Issue.table_name}.created_on" - - acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') }, + acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" }, :description => :notes, :author => :user, - :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' }, + :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} + acts_as_activity_provider :type => 'issues', + :permission => :view_issues, + :find_options => {:include => [{:issue => :project}, :details, :user], + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + + " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} + def save # Do not save an empty journal (details.empty? && notes.blank?) ? false : super diff --git a/groups/app/models/mail_handler.rb b/groups/app/models/mail_handler.rb index 7a1d73244..2f1eba3e9 100644 --- a/groups/app/models/mail_handler.rb +++ b/groups/app/models/mail_handler.rb @@ -16,25 +16,138 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MailHandler < ActionMailer::Base + + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end + + attr_reader :email, :user + + def self.receive(email, options={}) + @@handler_options = options.dup + + @@handler_options[:issue] ||= {} + + @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) + @@handler_options[:allow_override] ||= [] + # Project needs to be overridable if not specified + @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) + # Status needs to be overridable if not specified + @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) + super email + end # Processes incoming emails - # Currently, it only supports adding a note to an existing issue - # by replying to the initial notification message def receive(email) - # find related issue by parsing the subject - m = email.subject.match %r{\[.*#(\d+)\]} - return unless m - issue = Issue.find_by_id(m[1]) - return unless issue - - # find user - user = User.find_active(:first, :conditions => {:mail => email.from.first}) - return unless user + @email = email + @user = User.find_active(:first, :conditions => {:mail => email.from.first}) + unless @user + # Unknown user => the email is ignored + # TODO: ability to create the user's account + logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info + return false + end + User.current = @user + dispatch + end + + private + + ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]} + + def dispatch + if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) + receive_issue_update(m[1].to_i) + else + receive_issue + end + rescue ActiveRecord::RecordInvalid => e + # TODO: send a email to the user + logger.error e.message if logger + false + rescue MissingInformation => e + logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "MailHandler: unauthorized attempt from #{user}" if logger + false + end + + # Creates a new issue + def receive_issue + project = target_project + tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) + category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) + priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority))) + status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) || IssueStatus.default + # check permission - return unless user.allowed_to?(:add_issue_notes, issue.project) + raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) + issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :status => status) + issue.subject = email.subject.chomp + issue.description = email.plain_text_body.chomp + issue.save! + add_attachments(issue) + logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info + Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') + issue + end + + def target_project + # TODO: other ways to specify project: + # * parse the email To field + # * specific project (eg. Setting.mail_handler_target_project) + target = Project.find_by_identifier(get_keyword(:project)) + raise MissingInformation.new('Unable to determine target project') if target.nil? + target + end + + # Adds a note to an existing issue + def receive_issue_update(issue_id) + status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) + issue = Issue.find_by_id(issue_id) + return unless issue + # check permission + raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) + raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project) + # add the note - issue.init_journal(user, email.body.chomp) - issue.save + journal = issue.init_journal(user, email.plain_text_body.chomp) + add_attachments(issue) + issue.status = status unless status.nil? + issue.save! + logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + journal + end + + def add_attachments(obj) + if email.has_attachments? + email.attachments.each do |attachment| + Attachment.create(:container => obj, + :file => attachment, + :author => user, + :content_type => attachment.content_type) + end + end + end + + def get_keyword(attr) + if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i + $1.strip + elsif !@@handler_options[:issue][attr].blank? + @@handler_options[:issue][attr] + end end end + +class TMail::Mail + # Returns body of the first plain text part found if any + def plain_text_body + return @plain_text_body unless @plain_text_body.nil? + p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten + plain = p.detect {|c| c.content_type == 'text/plain'} + @plain_text_body = plain.nil? ? self.body : plain.body + end +end + diff --git a/groups/app/models/mailer.rb b/groups/app/models/mailer.rb index 6fc879a15..61e5d596c 100644 --- a/groups/app/models/mailer.rb +++ b/groups/app/models/mailer.rb @@ -51,6 +51,15 @@ class Mailer < ActionMailer::Base :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) end + def reminder(user, issues, days) + set_language_if_valid user.language + recipients user.mail + subject l(:mail_subject_reminder, issues.size) + body :issues => issues, + :days => days, + :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc') + end + def document_added(document) redmine_headers 'Project' => document.project.identifier recipients document.project.recipients @@ -144,6 +153,30 @@ class Mailer < ActionMailer::Base (bcc.nil? || bcc.empty?) super end + + # Sends reminders to issue assignees + # Available options: + # * :days => how many days in the future to remind about (defaults to 7) + # * :tracker => id of tracker for filtering issues (defaults to all trackers) + # * :project => id or identifier of project to process (defaults to all projects) + def self.reminders(options={}) + days = options[:days] || 7 + project = options[:project] ? Project.find(options[:project]) : nil + tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil + + s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] + s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" + s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" + s << "#{Issue.table_name}.project_id = #{project.id}" if project + s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker + + issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker], + :conditions => s.conditions + ).group_by(&:assigned_to) + issues_by_assignee.each do |assignee, issues| + deliver_reminder(assignee, issues, days) unless assignee.nil? + end + end private def initialize_defaults(method_name) diff --git a/groups/app/models/message.rb b/groups/app/models/message.rb index a18d126c9..80df7a33a 100644 --- a/groups/app/models/message.rb +++ b/groups/app/models/message.rb @@ -23,14 +23,17 @@ class Message < ActiveRecord::Base belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' acts_as_searchable :columns => ['subject', 'content'], - :include => :board, + :include => {:board, :project}, :project_key => 'project_id', - :date_column => 'created_on' + :date_column => "#{table_name}.created_on" acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, :description => :content, :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, - :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}} - + :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : + {:id => o.parent_id, :anchor => "message-#{o.id}"})} + + acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} + attr_protected :locked, :sticky validates_presence_of :subject, :content validates_length_of :subject, :maximum => 255 diff --git a/groups/app/models/news.rb b/groups/app/models/news.rb index 3d8c4d661..4c4943b78 100644 --- a/groups/app/models/news.rb +++ b/groups/app/models/news.rb @@ -24,9 +24,10 @@ class News < ActiveRecord::Base validates_length_of :title, :maximum => 60 validates_length_of :summary, :maximum => 255 - acts_as_searchable :columns => ['title', 'description'] + acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} - + acts_as_activity_provider :find_options => {:include => [:project, :author]} + # returns latest news for projects visible by user def self.latest(user=nil, count=5) find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") diff --git a/groups/app/models/project.rb b/groups/app/models/project.rb index 3deac3231..f7feb7349 100644 --- a/groups/app/models/project.rb +++ b/groups/app/models/project.rb @@ -23,7 +23,6 @@ class Project < ActiveRecord::Base has_many :members, :include => :user, :conditions => "#{Member.table_name}.principal_type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" has_many :memberships, :class_name => 'Member' has_many :users, :through => :members, :uniq => true - has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :enabled_modules, :dependent => :delete_all has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] @@ -39,7 +38,7 @@ class Project < ActiveRecord::Base has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy # Custom field for the project issues - has_and_belongs_to_many :custom_fields, + has_and_belongs_to_many :issue_custom_fields, :class_name => 'IssueCustomField', :order => "#{CustomField.table_name}.position", :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", @@ -47,18 +46,19 @@ class Project < ActiveRecord::Base acts_as_tree :order => "name", :counter_cache => true - acts_as_searchable :columns => ['name', 'description'], :project_key => 'id' + acts_as_customizable + acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, - :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}, + :author => nil attr_protected :status, :enabled_module_names validates_presence_of :name, :identifier validates_uniqueness_of :name, :identifier - validates_associated :custom_values, :on => :update validates_associated :repository, :wiki validates_length_of :name, :maximum => 30 - validates_length_of :homepage, :maximum => 60 + validates_length_of :homepage, :maximum => 255 validates_length_of :identifier, :in => 3..20 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ @@ -74,9 +74,9 @@ class Project < ActiveRecord::Base def issues_with_subprojects(include_subprojects=false) conditions = nil - if include_subprojects && !active_children.empty? - ids = [id] + active_children.collect {|c| c.id} - conditions = ["#{Project.table_name}.id IN (#{ids.join(',')})"] + if include_subprojects + ids = [id] + child_ids + conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] end conditions ||= ["#{Project.table_name}.id = ?", id] # Quick and dirty fix for Rails 2 compatibility @@ -94,6 +94,7 @@ class Project < ActiveRecord::Base end def self.visible_by(user=nil) + user ||= User.current if user && user.admin? return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" elsif user && user.memberships.any? @@ -113,16 +114,18 @@ class Project < ActiveRecord::Base end if user.admin? # no restriction - elsif user.logged? - statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission) - allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id} - statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? - elsif Role.anonymous.allowed_to?(permission) - # anonymous user allowed on public project - statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" else - # anonymous user is not authorized statements << "1=0" + if user.logged? + statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission) + allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id} + statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? + elsif Role.anonymous.allowed_to?(permission) + # anonymous user allowed on public project + statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" + else + # anonymous user is not authorized + end end statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" end @@ -144,7 +147,8 @@ class Project < ActiveRecord::Base end def to_param - identifier + # id is used for projects with a numeric identifier (compatibility) + @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier) end def active? @@ -194,12 +198,12 @@ class Project < ActiveRecord::Base # Returns an array of all custom fields enabled for project issues # (explictly associated custom fields and custom fields enabled for all projects) - def custom_fields_for_issues(tracker) - all_custom_fields.select {|c| tracker.custom_fields.include? c } + def all_issue_custom_fields + @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort end - def all_custom_fields - @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq + def project + self end def <=>(project) diff --git a/groups/app/models/query.rb b/groups/app/models/query.rb index 641c0d17b..0ce9a6a21 100644 --- a/groups/app/models/query.rb +++ b/groups/app/models/query.rb @@ -88,7 +88,7 @@ class Query < ActiveRecord::Base :date_past => [ ">t-", "<t-", "t-", "t", "w" ], :string => [ "=", "~", "!", "!~" ], :text => [ "~", "!~" ], - :integer => [ "=", ">=", "<=" ] } + :integer => [ "=", ">=", "<=", "!*", "*" ] } cattr_reader :operators_by_filter_type @@ -125,7 +125,7 @@ class Query < ActiveRecord::Base filters.each_key do |field| errors.add label_for(field), :activerecord_error_blank unless # filter requires one or more values - (values_for(field) and !values_for(field).first.empty?) or + (values_for(field) and !values_for(field).first.blank?) or # filter doesn't require any value ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) end if filters @@ -152,7 +152,8 @@ class Query < ActiveRecord::Base "updated_on" => { :type => :date_past, :order => 10 }, "start_date" => { :type => :date, :order => 11 }, "due_date" => { :type => :date, :order => 12 }, - "done_ratio" => { :type => :integer, :order => 13 }} + "estimated_hours" => { :type => :integer, :order => 13 }, + "done_ratio" => { :type => :integer, :order => 14 }} user_values = [] user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? @@ -166,29 +167,20 @@ class Query < ActiveRecord::Base @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? if project - # project specific filters - @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } - @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } + # project specific filters + unless @project.issue_categories.empty? + @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } + end + unless @project.versions.empty? + @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } + end unless @project.active_children.empty? @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } end - @project.all_custom_fields.select(&:is_filter?).each do |field| - case field.field_format - when "text" - options = { :type => :text, :order => 20 } - when "list" - options = { :type => :list_optional, :values => field.possible_values, :order => 20} - when "date" - options = { :type => :date, :order => 20 } - when "bool" - options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } - else - options = { :type => :string, :order => 20 } - end - @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) - end - # remove category filter if no category defined - @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? + add_custom_fields_filters(@project.all_issue_custom_fields) + else + # global filters for cross project issue list + add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) end @available_filters end @@ -227,7 +219,7 @@ class Query < ActiveRecord::Base end def label_for(field) - label = @available_filters[field][:name] if @available_filters.has_key?(field) + label = available_filters[field][:name] if available_filters.has_key?(field) label ||= field.gsub(/\_id$/, "") end @@ -235,7 +227,7 @@ class Query < ActiveRecord::Base return @available_columns if @available_columns @available_columns = Query.available_columns @available_columns += (project ? - project.all_custom_fields : + project.all_issue_custom_fields : IssueCustomField.find(:all, :conditions => {:is_for_all => true}) ).collect {|cf| QueryCustomFieldColumn.new(cf) } end @@ -265,7 +257,7 @@ class Query < ActiveRecord::Base def statement # project/subprojects clause - clause = '' + project_clauses = [] if project && !@project.active_children.empty? ids = [project.id] if has_filter?("subproject_id") @@ -277,17 +269,16 @@ class Query < ActiveRecord::Base # main project only else # all subprojects - ids += project.active_children.collect{|p| p.id} + ids += project.child_ids end elsif Setting.display_subprojects_issues? - ids += project.active_children.collect{|p| p.id} + ids += project.child_ids end - clause << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',') + project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',') elsif project - clause << "#{Issue.table_name}.project_id = %d" % project.id - else - clause << Project.visible_by(User.current) + project_clauses << "#{Issue.table_name}.project_id = %d" % project.id end + project_clauses << Project.visible_by(User.current) # filters clauses filters_clauses = [] @@ -296,11 +287,13 @@ class Query < ActiveRecord::Base v = values_for(field).clone next unless v and !v.empty? - sql = '' + sql = '' + is_custom_filter = false if field =~ /^cf_(\d+)$/ # custom field db_table = CustomValue.table_name db_field = 'value' + is_custom_filter = true sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE " else # regular field @@ -320,9 +313,11 @@ class Query < ActiveRecord::Base when "!" sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" when "!*" - sql = sql + "#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} = ''" + sql = sql + "#{db_table}.#{db_field} IS NULL" + sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter when "*" - sql = sql + "#{db_table}.#{db_field} IS NOT NULL AND #{db_table}.#{db_field} <> ''" + sql = sql + "#{db_table}.#{db_field} IS NOT NULL" + sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter when ">=" sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}" when "<=" @@ -361,8 +356,28 @@ class Query < ActiveRecord::Base filters_clauses << sql end if filters and valid? - clause << ' AND ' unless clause.empty? - clause << filters_clauses.join(' AND ') unless filters_clauses.empty? - clause + (project_clauses + filters_clauses).join(' AND ') + end + + private + + def add_custom_fields_filters(custom_fields) + @available_filters ||= {} + + custom_fields.select(&:is_filter?).each do |field| + case field.field_format + when "text" + options = { :type => :text, :order => 20 } + when "list" + options = { :type => :list_optional, :values => field.possible_values, :order => 20} + when "date" + options = { :type => :date, :order => 20 } + when "bool" + options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } + else + options = { :type => :string, :order => 20 } + end + @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) + end end end diff --git a/groups/app/models/repository.rb b/groups/app/models/repository.rb index 8b1f8d0af..9768e3e3c 100644 --- a/groups/app/models/repository.rb +++ b/groups/app/models/repository.rb @@ -17,9 +17,16 @@ class Repository < ActiveRecord::Base belongs_to :project - has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" + has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" has_many :changes, :through => :changesets - + + # Raw SQL to delete changesets and changes in the database + # has_many :changesets, :dependent => :destroy is too slow for big repositories + before_destroy :clear_changesets + + # Checks if the SCM is enabled when creating a repository + validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) } + # Removes leading and trailing whitespace def url=(arg) write_attribute(:url, arg ? arg.to_s.strip : nil) @@ -48,12 +55,24 @@ class Repository < ActiveRecord::Base scm.supports_annotate? end + def entry(path=nil, identifier=nil) + scm.entry(path, identifier) + end + def entries(path=nil, identifier=nil) scm.entries(path, identifier) end - def diff(path, rev, rev_to, type) - scm.diff(path, rev, rev_to, type) + def properties(path, identifier=nil) + scm.properties(path, identifier) + end + + def cat(path, identifier=nil) + scm.cat(path, identifier) + end + + def diff(path, rev, rev_to) + scm.diff(path, rev, rev_to) end # Default behaviour: we search in cached changesets @@ -64,6 +83,11 @@ class Repository < ActiveRecord::Base :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset) end + # Returns a path relative to the url of the repository + def relative_path(path) + path + end + def latest_changeset @latest_changeset ||= changesets.find(:first) end @@ -107,4 +131,9 @@ class Repository < ActiveRecord::Base root_url.strip! true end + + def clear_changesets + connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})") + connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}") + end end diff --git a/groups/app/models/repository/bazaar.rb b/groups/app/models/repository/bazaar.rb index 1b75066c2..ec953bd45 100644 --- a/groups/app/models/repository/bazaar.rb +++ b/groups/app/models/repository/bazaar.rb @@ -34,6 +34,11 @@ class Repository::Bazaar < Repository if entries entries.each do |e| next if e.lastrev.revision.blank? + # Set the filesize unless browsing a specific revision + if identifier.nil? && e.is_file? + full_path = File.join(root_url, e.path) + e.size = File.stat(full_path).size if File.file?(full_path) + end c = Change.find(:first, :include => :changeset, :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id], diff --git a/groups/app/models/repository/cvs.rb b/groups/app/models/repository/cvs.rb index c2d8be977..82082b3d6 100644 --- a/groups/app/models/repository/cvs.rb +++ b/groups/app/models/repository/cvs.rb @@ -29,9 +29,9 @@ class Repository::Cvs < Repository 'CVS' end - def entry(path, identifier) - e = entries(path, identifier) - e ? e.first : nil + def entry(path=nil, identifier=nil) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.entry(path, rev.nil? ? nil : rev.committed_on) end def entries(path=nil, identifier=nil) @@ -53,7 +53,12 @@ class Repository::Cvs < Repository entries end - def diff(path, rev, rev_to, type) + def cat(path, identifier=nil) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.cat(path, rev.nil? ? nil : rev.committed_on) + end + + def diff(path, rev, rev_to) #convert rev to revision. CVS can't handle changesets here diff=[] changeset_from=changesets.find_by_revision(rev) @@ -76,7 +81,8 @@ class Repository::Cvs < Repository unless revision_to revision_to=scm.get_previous_revision(revision_from) end - diff=diff+scm.diff(change_from.path, revision_from, revision_to, type) + file_diff = scm.diff(change_from.path, revision_from, revision_to) + diff = diff + file_diff unless file_diff.nil? end end return diff @@ -103,7 +109,7 @@ class Repository::Cvs < Repository cs = changesets.find(:first, :conditions=>{ :committed_on=>revision.time-time_delta..revision.time+time_delta, :committer=>revision.author, - :comments=>revision.message + :comments=>Changeset.normalize_comments(revision.message) }) # create a new changeset.... diff --git a/groups/app/models/repository/darcs.rb b/groups/app/models/repository/darcs.rb index c7c14a397..855a403fc 100644 --- a/groups/app/models/repository/darcs.rb +++ b/groups/app/models/repository/darcs.rb @@ -28,6 +28,11 @@ class Repository::Darcs < Repository 'Darcs' end + def entry(path=nil, identifier=nil) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.entry(path, patch.nil? ? nil : patch.scmid) + end + def entries(path=nil, identifier=nil) patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) entries = scm.entries(path, patch.nil? ? nil : patch.scmid) @@ -46,14 +51,19 @@ class Repository::Darcs < Repository entries end - def diff(path, rev, rev_to, type) + def cat(path, identifier=nil) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.cat(path, patch.nil? ? nil : patch.scmid) + end + + def diff(path, rev, rev_to) patch_from = changesets.find_by_revision(rev) return nil if patch_from.nil? patch_to = changesets.find_by_revision(rev_to) if rev_to if path.blank? path = patch_from.changes.collect{|change| change.path}.join(' ') end - patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil + patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil end def fetch_changesets diff --git a/groups/app/models/repository/filesystem.rb b/groups/app/models/repository/filesystem.rb new file mode 100644 index 000000000..da096cc09 --- /dev/null +++ b/groups/app/models/repository/filesystem.rb @@ -0,0 +1,43 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# FileSystem adapter +# File written by Paul Rivier, at Demotera. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/filesystem_adapter' + +class Repository::Filesystem < Repository + attr_protected :root_url + validates_presence_of :url + + def scm_adapter + Redmine::Scm::Adapters::FilesystemAdapter + end + + def self.scm_name + 'Filesystem' + end + + def entries(path=nil, identifier=nil) + scm.entries(path, identifier) + end + + def fetch_changesets + nil + end + +end diff --git a/groups/app/models/repository/git.rb b/groups/app/models/repository/git.rb index 7213588ac..2f440fe29 100644 --- a/groups/app/models/repository/git.rb +++ b/groups/app/models/repository/git.rb @@ -44,10 +44,8 @@ class Repository::Git < Repository scm_revision = scm_info.lastrev.scmid unless changesets.find_by_scmid(scm_revision) - - revisions = scm.revisions('', db_revision, nil) - transaction do - revisions.reverse_each do |revision| + scm.revisions('', db_revision, nil, :reverse => true) do |revision| + transaction do changeset = Changeset.create(:repository => self, :revision => revision.identifier, :scmid => revision.scmid, diff --git a/groups/app/models/repository/subversion.rb b/groups/app/models/repository/subversion.rb index 0c2239c43..3981d6f4c 100644 --- a/groups/app/models/repository/subversion.rb +++ b/groups/app/models/repository/subversion.rb @@ -35,6 +35,11 @@ class Repository::Subversion < Repository revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : [] end + # Returns a path relative to the url of the repository + def relative_path(path) + path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '') + end + def fetch_changesets scm_info = scm.info if scm_info @@ -71,4 +76,14 @@ class Repository::Subversion < Repository end end end + + private + + # Returns the relative url of the repository + # Eg: root_url = file:///var/svn/foo + # url = file:///var/svn/foo/bar + # => returns /bar + def relative_url + @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url)}"), '') + end end diff --git a/groups/app/models/setting.rb b/groups/app/models/setting.rb index 185991d9b..072afa0db 100644 --- a/groups/app/models/setting.rb +++ b/groups/app/models/setting.rb @@ -33,6 +33,45 @@ class Setting < ActiveRecord::Base '%H:%M', '%I:%M %p' ] + + ENCODINGS = %w(US-ASCII + windows-1250 + windows-1251 + windows-1252 + windows-1253 + windows-1254 + windows-1255 + windows-1256 + windows-1257 + windows-1258 + windows-31j + ISO-2022-JP + ISO-2022-KR + ISO-8859-1 + ISO-8859-2 + ISO-8859-3 + ISO-8859-4 + ISO-8859-5 + ISO-8859-6 + ISO-8859-7 + ISO-8859-8 + ISO-8859-9 + ISO-8859-13 + ISO-8859-15 + KOI8-R + UTF-8 + UTF-16 + UTF-16BE + UTF-16LE + EUC-JP + Shift_JIS + GB18030 + GBK + ISCII91 + EUC-KR + Big5 + Big5-HKSCS + TIS-620) cattr_accessor :available_settings @@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) diff --git a/groups/app/models/time_entry.rb b/groups/app/models/time_entry.rb index ddaff2b60..57a75604d 100644 --- a/groups/app/models/time_entry.rb +++ b/groups/app/models/time_entry.rb @@ -24,11 +24,25 @@ class TimeEntry < ActiveRecord::Base belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek + + acts_as_customizable + acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"}, + :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}}, + :author => :user, + :description => :comments validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on validates_numericality_of :hours, :allow_nil => true - validates_length_of :comments, :maximum => 255 + validates_length_of :comments, :maximum => 255, :allow_nil => true + def after_initialize + if new_record? && self.activity.nil? + if default_activity = Enumeration.default('ACTI') + self.activity_id = default_activity.id + end + end + end + def before_validation self.project = issue.project if issue && project.nil? end diff --git a/groups/app/models/time_entry_custom_field.rb b/groups/app/models/time_entry_custom_field.rb new file mode 100644 index 000000000..2ec3d27be --- /dev/null +++ b/groups/app/models/time_entry_custom_field.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimeEntryCustomField < CustomField + def type_name + :label_spent_time + end +end + diff --git a/groups/app/models/user.rb b/groups/app/models/user.rb index 3743fcb3f..0e02cb78c 100644 --- a/groups/app/models/user.rb +++ b/groups/app/models/user.rb @@ -19,8 +19,6 @@ require "digest/sha1" class User < ActiveRecord::Base - class OnTheFlyCreationFailure < Exception; end - # Account statuses STATUS_ANONYMOUS = 0 STATUS_ACTIVE = 1 @@ -39,17 +37,17 @@ class User < ActiveRecord::Base :as => :principal, :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", - :order => "#{Project.table_name}.name, inherited_from ASC", - :dependent => :delete_all - + :order => "#{Project.table_name}.name, inherited_from ASC" + has_many :members, :as => :principal, :dependent => :delete_all has_many :projects, :through => :memberships - has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" belongs_to :auth_source belongs_to :group + acts_as_customizable + attr_accessor :password, :password_confirmation attr_accessor :last_before_login_on # Prevents unauthorized assignments @@ -61,13 +59,12 @@ class User < ActiveRecord::Base # Login must contain lettres, numbers, underscores only validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i validates_length_of :login, :maximum => 30 - validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i + validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i validates_length_of :firstname, :lastname, :maximum => 30 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true validates_length_of :mail, :maximum => 60, :allow_nil => true validates_length_of :password, :minimum => 4, :allow_nil => true validates_confirmation_of :password, :allow_nil => true - validates_associated :custom_values, :on => :update def before_create self.mail_notification = false @@ -96,6 +93,7 @@ class User < ActiveRecord::Base def group_id=(gid) @group_changed = true unless gid == group_id + group_id_will_change! write_attribute(:group_id, gid) end @@ -130,19 +128,16 @@ class User < ActiveRecord::Base # user is not yet registered, try to authenticate with available sources attrs = AuthSource.authenticate(login, password) if attrs - onthefly = new(*attrs) - onthefly.login = login - onthefly.language = Setting.default_language - if onthefly.save - user = find(:first, :conditions => ["login=?", login]) + user = new(*attrs) + user.login = login + user.language = Setting.default_language + if user.save + user.reload logger.info("User '#{user.login}' created from the LDAP") if logger - else - logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger - raise OnTheFlyCreationFailure.new end end end - user.update_attribute(:last_login_on, Time.now) if user + user.update_attribute(:last_login_on, Time.now) if user && !user.new_record? user rescue => text raise text @@ -228,6 +223,10 @@ class User < ActiveRecord::Base true end + def anonymous? + !logged? + end + # Return user's role for project def role_for_project(project) # No role on archived projects @@ -285,13 +284,12 @@ class User < ActiveRecord::Base end def self.anonymous - return @anonymous_user if @anonymous_user anonymous_user = AnonymousUser.find(:first) if anonymous_user.nil? anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) raise 'Unable to create the anonymous user.' if anonymous_user.new_record? end - @anonymous_user = anonymous_user + anonymous_user end private @@ -308,6 +306,10 @@ class AnonymousUser < User errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first) end + def available_custom_fields + [] + end + # Overrides a few properties def logged?; false end def admin; false end diff --git a/groups/app/models/user_preference.rb b/groups/app/models/user_preference.rb index 73e4a50c6..3daa7a740 100644 --- a/groups/app/models/user_preference.rb +++ b/groups/app/models/user_preference.rb @@ -42,8 +42,10 @@ class UserPreference < ActiveRecord::Base if attribute_present? attr_name super else - self.others ||= {} - self.others.store attr_name, value + h = read_attribute(:others).dup || {} + h.update(attr_name => value) + write_attribute(:others, h) + value end end diff --git a/groups/app/models/watcher.rb b/groups/app/models/watcher.rb index cb6ff52ea..38110c584 100644 --- a/groups/app/models/watcher.rb +++ b/groups/app/models/watcher.rb @@ -19,5 +19,12 @@ class Watcher < ActiveRecord::Base belongs_to :watchable, :polymorphic => true belongs_to :user + validates_presence_of :user validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] + + protected + + def validate + errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active? + end end diff --git a/groups/app/models/wiki.rb b/groups/app/models/wiki.rb index b6d6a9b50..3432a2bc7 100644 --- a/groups/app/models/wiki.rb +++ b/groups/app/models/wiki.rb @@ -17,7 +17,7 @@ class Wiki < ActiveRecord::Base belongs_to :project - has_many :pages, :class_name => 'WikiPage', :dependent => :destroy + has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title' has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all validates_presence_of :start_page diff --git a/groups/app/models/wiki_content.rb b/groups/app/models/wiki_content.rb index 724354ad6..f2ee39c4d 100644 --- a/groups/app/models/wiki_content.rb +++ b/groups/app/models/wiki_content.rb @@ -35,6 +35,17 @@ class WikiContent < ActiveRecord::Base :type => 'wiki-page', :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} + acts_as_activity_provider :type => 'wiki_pages', + :timestamp => "#{WikiContent.versioned_table_name}.updated_on", + :permission => :view_wiki_pages, + :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + + "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + + "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + + "#{WikiContent.versioned_table_name}.id", + :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " + + "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " + + "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"} + def text=(plain) case Setting.wiki_compression when 'gzip' diff --git a/groups/app/models/wiki_page.rb b/groups/app/models/wiki_page.rb index 8ce71cb80..2416fab74 100644 --- a/groups/app/models/wiki_page.rb +++ b/groups/app/models/wiki_page.rb @@ -22,14 +22,15 @@ class WikiPage < ActiveRecord::Base belongs_to :wiki has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy has_many :attachments, :as => :container, :dependent => :destroy - + acts_as_tree :order => 'title' + acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, :description => :text, :datetime => :created_on, :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}} acts_as_searchable :columns => ['title', 'text'], - :include => [:wiki, :content], + :include => [{:wiki => :project}, :content], :project_key => "#{Wiki.table_name}.project_id" attr_accessor :redirect_existing_links @@ -105,6 +106,29 @@ class WikiPage < ActiveRecord::Base def text content.text if content end + + # Returns true if usr is allowed to edit the page, otherwise false + def editable_by?(usr) + !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) + end + + def parent_title + @parent_title || (self.parent && self.parent.pretty_title) + end + + def parent_title=(t) + @parent_title = t + parent_page = t.blank? ? nil : self.wiki.find_page(t) + self.parent = parent_page + end + + protected + + def validate + errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil? + errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self)) + errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id) + end end class WikiDiff diff --git a/groups/app/views/account/login.rhtml b/groups/app/views/account/login.rhtml index ea1a1cd44..d8c1f313f 100644 --- a/groups/app/views/account/login.rhtml +++ b/groups/app/views/account/login.rhtml @@ -1,5 +1,6 @@ <div id="login-form"> <% form_tag({:action=> "login"}) do %> +<%= back_url_hidden_field_tag %> <table> <tr> <td align="right"><label for="username"><%=l(:field_login)%>:</label></td> diff --git a/groups/app/views/account/register.rhtml b/groups/app/views/account/register.rhtml index 7cf4b6da3..755a7ad4b 100644 --- a/groups/app/views/account/register.rhtml +++ b/groups/app/views/account/register.rhtml @@ -5,8 +5,9 @@ <div class="box"> <!--[form:user]--> +<% if @user.auth_source_id.nil? %> <p><label for="user_login"><%=l(:field_login)%> <span class="required">*</span></label> -<%= text_field 'user', 'login', :size => 25 %></p> +<%= text_field 'user', 'login', :size => 25 %></p> <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label> <%= password_field_tag 'password', nil, :size => 25 %><br /> @@ -14,6 +15,7 @@ <p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label> <%= password_field_tag 'password_confirmation', nil, :size => 25 %></p> +<% end %> <p><label for="user_firstname"><%=l(:field_firstname)%> <span class="required">*</span></label> <%= text_field 'user', 'firstname' %></p> @@ -27,8 +29,8 @@ <p><label for="user_language"><%=l(:field_language)%></label> <%= select("user", "language", lang_options_for_select) %></p> -<% for @custom_value in @custom_values %> - <p><%= custom_field_tag_with_label @custom_value %></p> +<% @user.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :user, value %></p> <% end %> <!--[eoform:user]--> </div> diff --git a/groups/app/views/account/show.rhtml b/groups/app/views/account/show.rhtml index 97212b377..1160a5d8c 100644 --- a/groups/app/views/account/show.rhtml +++ b/groups/app/views/account/show.rhtml @@ -1,7 +1,11 @@ +<div class="contextual"> +<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %> +</div> + <h2><%=h @user.name %></h2> <p> -<%= mail_to @user.mail unless @user.pref.hide_mail %> +<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %> <ul> <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li> <% for custom_value in @custom_values %> @@ -16,8 +20,8 @@ <h3><%=l(:label_project_plural)%></h3> <ul> <% for membership in @memberships %> - <li><%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %> - (<%= membership.role.name %>, <%= format_date(membership.created_on) %>)</li> + <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %> + (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li> <% end %> </ul> <% end %> diff --git a/groups/app/views/attachments/_links.rhtml b/groups/app/views/attachments/_links.rhtml index 4d485548b..9aae909fe 100644 --- a/groups/app/views/attachments/_links.rhtml +++ b/groups/app/views/attachments/_links.rhtml @@ -1,6 +1,6 @@ <div class="attachments"> <% for attachment in attachments %> -<p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' -%> +<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%> <%= h(" - #{attachment.description}") unless attachment.description.blank? %> <span class="size">(<%= number_to_human_size attachment.filesize %>)</span> <% if options[:delete_url] %> diff --git a/groups/app/views/attachments/diff.rhtml b/groups/app/views/attachments/diff.rhtml new file mode 100644 index 000000000..7b64dca17 --- /dev/null +++ b/groups/app/views/attachments/diff.rhtml @@ -0,0 +1,15 @@ +<h2><%=h @attachment.filename %></h2> + +<div class="attachments"> +<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p> +<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> + <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p> + +</div> + +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/groups/app/views/attachments/file.rhtml b/groups/app/views/attachments/file.rhtml new file mode 100644 index 000000000..468c6b666 --- /dev/null +++ b/groups/app/views/attachments/file.rhtml @@ -0,0 +1,15 @@ +<h2><%=h @attachment.filename %></h2> + +<div class="attachments"> +<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p> +<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> + <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p> + +</div> + +<%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/groups/app/views/auth_sources/_form.rhtml b/groups/app/views/auth_sources/_form.rhtml index 3d148c11f..9ffffafc7 100644 --- a/groups/app/views/auth_sources/_form.rhtml +++ b/groups/app/views/auth_sources/_form.rhtml @@ -22,14 +22,12 @@ <p><label for="auth_source_base_dn"><%=l(:field_base_dn)%> <span class="required">*</span></label> <%= text_field 'auth_source', 'base_dn', :size => 60 %></p> -</div> -<div class="box"> <p><label for="auth_source_onthefly_register"><%=l(:field_onthefly)%></label> <%= check_box 'auth_source', 'onthefly_register' %></p> +</div> -<p> -<fieldset><legend><%=l(:label_attribute_plural)%></legend> +<fieldset class="box"><legend><%=l(:label_attribute_plural)%></legend> <p><label for="auth_source_attr_login"><%=l(:field_login)%> <span class="required">*</span></label> <%= text_field 'auth_source', 'attr_login', :size => 20 %></p> @@ -42,7 +40,5 @@ <p><label for="auth_source_attr_mail"><%=l(:field_mail)%></label> <%= text_field 'auth_source', 'attr_mail', :size => 20 %></p> </fieldset> -</p> -</div> <!--[eoform:auth_source]--> diff --git a/groups/app/views/boards/index.rhtml b/groups/app/views/boards/index.rhtml index 8d4560653..655352a96 100644 --- a/groups/app/views/boards/index.rhtml +++ b/groups/app/views/boards/index.rhtml @@ -38,3 +38,5 @@ <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> <% end %> + +<% html_title l(:label_board_plural) %> diff --git a/groups/app/views/boards/show.rhtml b/groups/app/views/boards/show.rhtml index 26d17ae56..96818df34 100644 --- a/groups/app/views/boards/show.rhtml +++ b/groups/app/views/boards/show.rhtml @@ -33,7 +33,7 @@ <th><%= l(:field_subject) %></th> <th><%= l(:field_author) %></th> <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %> - <th><%= l(:label_reply_plural) %></th> + <%= sort_header_tag("#{Message.table_name}.replies_count", :caption => l(:label_reply_plural)) %> <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %> </tr></thead> <tbody> @@ -57,3 +57,5 @@ <% else %> <p class="nodata"><%= l(:label_no_data) %></p> <% end %> + +<% html_title h(@board.name) %> diff --git a/groups/app/views/common/_diff.rhtml b/groups/app/views/common/_diff.rhtml new file mode 100644 index 000000000..0b28101b7 --- /dev/null +++ b/groups/app/views/common/_diff.rhtml @@ -0,0 +1,64 @@ +<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%> +<div class="autoscroll"> +<% if diff_type == 'sbs' -%> +<table class="filecontent CodeRay"> +<thead> +<tr><th colspan="4" class="filename"><%= table_file.file_name %></th></tr> +</thead> +<tbody> +<% prev_line_left, prev_line_right = nil, nil -%> +<% table_file.keys.sort.each do |key| -%> +<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> +<tr class="spacing"> +<th class="line-num">...</th><td></td><th class="line-num">...</th><td></td> +<% end -%> +<tr> + <th class="line-num"><%= table_file[key].nb_line_left %></th> + <td class="line-code <%= table_file[key].type_diff_left %>"> + <pre><%=to_utf8 table_file[key].line_left %></pre> + </td> + <th class="line-num"><%= table_file[key].nb_line_right %></th> + <td class="line-code <%= table_file[key].type_diff_right %>"> + <pre><%=to_utf8 table_file[key].line_right %></pre> + </td> +</tr> +<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> +<% end -%> +</tbody> +</table> + +<% else -%> +<table class="filecontent CodeRay"> +<thead> +<tr><th colspan="3" class="filename"><%= table_file.file_name %></th></tr> +</thead> +<tbody> +<% prev_line_left, prev_line_right = nil, nil -%> +<% table_file.keys.sort.each do |key, line| %> +<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> +<tr class="spacing"> +<th class="line-num">...</th><th class="line-num">...</th><td></td> +</tr> +<% end -%> +<tr> + <th class="line-num"><%= table_file[key].nb_line_left %></th> + <th class="line-num"><%= table_file[key].nb_line_right %></th> + <% if table_file[key].line_left.empty? -%> + <td class="line-code <%= table_file[key].type_diff_right %>"> + <pre><%=to_utf8 table_file[key].line_right %></pre> + </td> + <% else -%> + <td class="line-code <%= table_file[key].type_diff_left %>"> + <pre><%=to_utf8 table_file[key].line_left %></pre> + </td> + <% end -%> +</tr> +<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%> +<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%> +<% end -%> +</tbody> +</table> +<% end -%> + +</div> +<% end -%> diff --git a/groups/app/views/common/_file.rhtml b/groups/app/views/common/_file.rhtml new file mode 100644 index 000000000..43f5c6c4b --- /dev/null +++ b/groups/app/views/common/_file.rhtml @@ -0,0 +1,11 @@ +<div class="autoscroll"> +<table class="filecontent CodeRay"> +<tbody> +<% line_num = 1 %> +<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %> +<tr><th class="line-num" id="L<%= line_num %>"><%= line_num %></th><td class="line-code"><pre><%= line %></pre></td></tr> +<% line_num += 1 %> +<% end %> +</tbody> +</table> +</div> diff --git a/groups/app/views/common/_preview.rhtml b/groups/app/views/common/_preview.rhtml index e3bfc3a25..fd95f1188 100644 --- a/groups/app/views/common/_preview.rhtml +++ b/groups/app/views/common/_preview.rhtml @@ -1,3 +1,3 @@ <fieldset class="preview"><legend><%= l(:label_preview) %></legend> -<%= textilizable @text, :attachments => @attachements %> +<%= textilizable @text, :attachments => @attachements, :object => @previewed %> </fieldset> diff --git a/groups/app/views/common/feed.atom.rxml b/groups/app/views/common/feed.atom.rxml index b5cbeeed9..c1b88a28e 100644 --- a/groups/app/views/common/feed.atom.rxml +++ b/groups/app/views/common/feed.atom.rxml @@ -1,6 +1,6 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do - xml.title @title + xml.title truncate_single_line(@title, 100) xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false})) xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) xml.id url_for(:controller => 'welcome', :only_path => false) @@ -10,11 +10,15 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do @items.each do |item| xml.entry do url = url_for(item.event_url(:only_path => false)) - xml.title truncate(item.event_title, 100) + if @project + xml.title truncate_single_line(item.event_title, 100) + else + xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100) + end xml.link "rel" => "alternate", "href" => url xml.id url xml.updated item.event_datetime.xmlschema - author = item.event_author if item.respond_to?(:author) + author = item.event_author if item.respond_to?(:event_author) xml.author do xml.name(author) xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank? diff --git a/groups/app/views/custom_fields/_form.rhtml b/groups/app/views/custom_fields/_form.rhtml index b3731fac7..db87c9217 100644 --- a/groups/app/views/custom_fields/_form.rhtml +++ b/groups/app/views/custom_fields/_form.rhtml @@ -102,6 +102,9 @@ when "IssueCustomField" %> <% else %> <p><%= f.check_box :is_required %></p> +<% when "TimeEntryCustomField" %> + <p><%= f.check_box :is_required %></p> + <% end %> </div> <%= javascript_tag "toggle_custom_field_format();" %> diff --git a/groups/app/views/enumerations/destroy.rhtml b/groups/app/views/enumerations/destroy.rhtml new file mode 100644 index 000000000..657df8322 --- /dev/null +++ b/groups/app/views/enumerations/destroy.rhtml @@ -0,0 +1,12 @@ +<h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2> + +<% form_tag({}) do %> +<div class="box"> +<p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p> +<p><%= l(:text_enumeration_category_reassign_to) %> +<%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p> +</div> + +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'enumerations', :action => 'index' %> +<% end %> diff --git a/groups/app/views/enumerations/list.rhtml b/groups/app/views/enumerations/list.rhtml index 9de9bf37c..7f3886b44 100644 --- a/groups/app/views/enumerations/list.rhtml +++ b/groups/app/views/enumerations/list.rhtml @@ -1,14 +1,14 @@ <h2><%=l(:label_enumerations)%></h2> -<% Enumeration::OPTIONS.each do |option, name| %> -<h3><%= l(name) %></h3> +<% Enumeration::OPTIONS.each do |option, params| %> +<h3><%= l(params[:label]) %></h3> <% enumerations = Enumeration.get_values(option) %> <% if enumerations.any? %> <table class="list"> <% enumerations.each do |enumeration| %> <tr class="<%= cycle('odd', 'even') %>"> - <td><%= link_to enumeration.name, :action => 'edit', :id => enumeration %></td> + <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td> <td style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td> <td style="width:15%;"> <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> @@ -16,6 +16,9 @@ <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> </td> + <td align="center" style="width:10%;"> + <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %> + </td> </tr> <% end %> </table> diff --git a/groups/app/views/issues/_edit.rhtml b/groups/app/views/issues/_edit.rhtml index 2e00ab520..2c7a4286e 100644 --- a/groups/app/views/issues/_edit.rhtml +++ b/groups/app/views/issues/_edit.rhtml @@ -4,6 +4,7 @@ :class => nil, :multipart => true} do |f| %> <%= error_messages_for 'issue' %> + <%= error_messages_for 'time_entry' %> <div class="box"> <% if @edit_allowed || !@allowed_statuses.empty? %> <fieldset class="tabular"><legend><%= l(:label_change_properties) %> @@ -21,9 +22,12 @@ <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p> </div> <div class="splitcontentright"> - <p><%= time_entry.text_field :comments, :size => 40 %></p> - <p><%= time_entry.select :activity_id, (@activities.collect {|p| [p.name, p.id]}) %></p> + <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p> </div> + <p><%= time_entry.text_field :comments, :size => 60 %></p> + <% @time_entry.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :time_entry, value %></p> + <% end %> <% end %> </fieldset> <% end %> diff --git a/groups/app/views/issues/_form.rhtml b/groups/app/views/issues/_form.rhtml index 9bb74fd34..4eca3cb4a 100644 --- a/groups/app/views/issues/_form.rhtml +++ b/groups/app/views/issues/_form.rhtml @@ -42,7 +42,7 @@ </div> <div style="clear:both;"> </div> -<%= render :partial => 'form_custom_fields', :locals => {:values => @custom_values} %> +<%= render :partial => 'form_custom_fields' %> <% if @issue.new_record? %> <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> diff --git a/groups/app/views/issues/_form_custom_fields.rhtml b/groups/app/views/issues/_form_custom_fields.rhtml index 1268bb1f9..752fb4d37 100644 --- a/groups/app/views/issues/_form_custom_fields.rhtml +++ b/groups/app/views/issues/_form_custom_fields.rhtml @@ -1,11 +1,12 @@ <div class="splitcontentleft"> -<% i = 1 %> -<% for @custom_value in values %> - <p><%= custom_field_tag_with_label @custom_value %></p> - <% if i == values.size / 2 %> +<% i = 0 %> +<% split_on = @issue.custom_field_values.size / 2 %> +<% @issue.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :issue, value %></p> +<% if i == split_on -%> </div><div class="splitcontentright"> - <% end %> - <% i += 1 %> -<% end %> +<% end -%> +<% i += 1 -%> +<% end -%> </div> <div style="clear:both;"> </div> diff --git a/groups/app/views/issues/_history.rhtml b/groups/app/views/issues/_history.rhtml index f29a44daf..b8efdb400 100644 --- a/groups/app/views/issues/_history.rhtml +++ b/groups/app/views/issues/_history.rhtml @@ -1,3 +1,4 @@ +<% reply_links = authorize_for('issues', 'edit') -%> <% for journal in journals %> <div id="change-<%= journal.id %>" class="journal"> <h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div> @@ -8,6 +9,6 @@ <li><%= show_detail(detail) %></li> <% end %> </ul> - <%= render_notes(journal) unless journal.notes.blank? %> + <%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %> </div> <% end %> diff --git a/groups/app/views/issues/_list.rhtml b/groups/app/views/issues/_list.rhtml index 000f79853..b42357894 100644 --- a/groups/app/views/issues/_list.rhtml +++ b/groups/app/views/issues/_list.rhtml @@ -1,7 +1,7 @@ <% form_tag({}) do -%> <table class="list issues"> <thead><tr> - <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;', + <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> </th> <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> diff --git a/groups/app/views/issues/_pdf.rfpdf b/groups/app/views/issues/_pdf.rfpdf deleted file mode 100644 index 6830506f6..000000000 --- a/groups/app/views/issues/_pdf.rfpdf +++ /dev/null @@ -1,118 +0,0 @@ -<% pdf.SetFontStyle('B',11)
- pdf.Cell(190,10, "#{issue.project.name} - #{issue.tracker.name} # #{issue.id}: #{issue.subject}")
- pdf.Ln
-
- y0 = pdf.GetY
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_status) + ":","LT")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, issue.status.name,"RT")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_priority) + ":","LT")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, issue.priority.name,"RT")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_author) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, issue.author.name,"R")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_category) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, (issue.category ? issue.category.name : "-"),"R")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_created_on) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, format_date(issue.created_on),"R")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, (issue.assigned_to ? issue.assigned_to.name : "-"),"R")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, format_date(issue.updated_on),"RB")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_due_date) + ":","LB")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, format_date(issue.due_date),"RB")
- pdf.Ln
-
- for custom_value in issue.custom_values
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
- pdf.SetFontStyle('',9)
- pdf.MultiCell(155,5, (show_value custom_value),"R")
- end
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_subject) + ":","LTB")
- pdf.SetFontStyle('',9)
- pdf.Cell(155,5, issue.subject,"RTB")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_description) + ":")
- pdf.SetFontStyle('',9)
- pdf.MultiCell(155,5, issue.description,"BR")
-
- pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
- pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
-
- pdf.Ln
-
- if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
- pdf.SetFontStyle('B',9)
- pdf.Cell(190,5, l(:label_associated_revisions), "B")
- pdf.Ln
- for changeset in @issue.changesets
- pdf.SetFontStyle('B',8)
- pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.committer)
- pdf.Ln
- unless changeset.comments.blank?
- pdf.SetFontStyle('',8)
- pdf.MultiCell(190,5, changeset.comments)
- end
- pdf.Ln
- end
- end
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(190,5, l(:label_history), "B")
- pdf.Ln
- for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
- pdf.SetFontStyle('B',8)
- pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
- pdf.Ln
- pdf.SetFontStyle('I',8)
- for detail in journal.details
- pdf.Cell(190,5, "- " + show_detail(detail, true))
- pdf.Ln
- end
- if journal.notes?
- pdf.SetFontStyle('',8)
- pdf.MultiCell(190,5, journal.notes)
- end
- pdf.Ln
- end
-
- if issue.attachments.any?
- pdf.SetFontStyle('B',9)
- pdf.Cell(190,5, l(:label_attachment_plural), "B")
- pdf.Ln
- for attachment in issue.attachments
- pdf.SetFontStyle('',8)
- pdf.Cell(80,5, attachment.filename)
- pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
- pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
- pdf.Cell(65,5, attachment.author.name,0,0,"R")
- pdf.Ln
- end
- end
-%>
diff --git a/groups/app/views/issues/context_menu.rhtml b/groups/app/views/issues/context_menu.rhtml index f42f254e8..671655db7 100644 --- a/groups/app/views/issues/context_menu.rhtml +++ b/groups/app/views/issues/context_menu.rhtml @@ -6,47 +6,83 @@ <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a> <ul> <% @statuses.each do |s| -%> - <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, + <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post, :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li> <% end -%> </ul> </li> +<% else %> + <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %></li> +<% end %> + <li class="folder"> <a href="#" class="submenu"><%= l(:field_priority) %></a> <ul> <% @priorities.each do |p| -%> - <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post, - :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li> + <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post, + :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li> + <% end -%> + </ul> + </li> + <% unless @project.nil? || @project.versions.empty? -%> + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_fixed_version) %></a> + <ul> + <% @project.versions.sort.each do |v| -%> + <li><%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post, + :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li> <% end -%> + <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li> </ul> </li> + <% end %> + <% unless @assignables.nil? || @assignables.empty? -%> <li class="folder"> <a href="#" class="submenu"><%= l(:field_assigned_to) %></a> <ul> <% @assignables.each do |u| -%> - <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post, - :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %></li> + <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post, + :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li> <% end -%> - <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post, - :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %></li> + <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li> </ul> </li> + <% end %> + <% unless @project.nil? || @project.issue_categories.empty? -%> + <li class="folder"> + <a href="#" class="submenu"><%= l(:field_category) %></a> + <ul> + <% @project.issue_categories.each do |u| -%> + <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post, + :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li> + <% end -%> + <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li> + </ul> + </li> + <% end -%> <li class="folder"> <a href="#" class="submenu"><%= l(:field_done_ratio) %></a> <ul> <% (0..10).map{|x|x*10}.each do |p| -%> - <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[done_ratio]' => p, :back_to => @back}, :method => :post, - :selected => (p == @issue.done_ratio), :disabled => !@can[:edit] %></li> + <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post, + :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li> <% end -%> </ul> </li> + +<% if !@issue.nil? %> <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon-copy', :disabled => !@can[:copy] %></li> -<% else -%> - <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, - :class => 'icon-edit', :disabled => !@can[:edit] %></li> -<% end -%> - + <% if @can[:log_time] -%> + <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, + :class => 'icon-time' %></li> + <% end %> +<% end %> + <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, :class => 'icon-move', :disabled => !@can[:move] %></li> <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)}, diff --git a/groups/app/views/issues/index.rhtml b/groups/app/views/issues/index.rhtml index 027f3f006..973f3eb25 100644 --- a/groups/app/views/issues/index.rhtml +++ b/groups/app/views/issues/index.rhtml @@ -45,7 +45,7 @@ <p class="other-formats"> <%= l(:label_export_to) %> -<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +<span><%= link_to 'Atom', {:query_id => @query, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> <span><%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %></span> <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span> </p> diff --git a/groups/app/views/issues/show.rfpdf b/groups/app/views/issues/show.rfpdf index 08f2cb92d..73d9d66b5 100644 --- a/groups/app/views/issues/show.rfpdf +++ b/groups/app/views/issues/show.rfpdf @@ -4,7 +4,123 @@ pdf.footer_date = format_date(Date.today)
pdf.AddPage
- render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => @issue }
+ pdf.SetFontStyle('B',11)
+ pdf.Cell(190,10, "#{@issue.project} - #{@issue.tracker} # #{@issue.id}: #{@issue.subject}")
+ pdf.Ln
+
+ y0 = pdf.GetY
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_status) + ":","LT")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, @issue.status.name,"RT")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_priority) + ":","LT")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, @issue.priority.name,"RT")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_author) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, @issue.author.name,"R")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_category) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, (@issue.category ? @issue.category.name : "-"),"R")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_created_on) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(@issue.created_on),"R")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, (@issue.assigned_to ? @issue.assigned_to.name : "-"),"R")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(@issue.updated_on),"RB")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_due_date) + ":","LB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(@issue.due_date),"RB")
+ pdf.Ln
+
+ for custom_value in @issue.custom_values
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.MultiCell(155,5, (show_value custom_value),"R")
+ end
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_subject) + ":","LTB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(155,5, @issue.subject,"RTB")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_description) + ":")
+ pdf.SetFontStyle('',9)
+ pdf.MultiCell(155,5, @issue.description,"BR")
+
+ pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
+ pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
+
+ pdf.Ln
+
+ if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @issue.project)
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_associated_revisions), "B")
+ pdf.Ln
+ for changeset in @issue.changesets
+ pdf.SetFontStyle('B',8)
+ pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.committer)
+ pdf.Ln
+ unless changeset.comments.blank?
+ pdf.SetFontStyle('',8)
+ pdf.MultiCell(190,5, changeset.comments)
+ end
+ pdf.Ln
+ end
+ end
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_history), "B")
+ pdf.Ln
+ for journal in @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
+ pdf.SetFontStyle('B',8)
+ pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
+ pdf.Ln
+ pdf.SetFontStyle('I',8)
+ for detail in journal.details
+ pdf.Cell(190,5, "- " + show_detail(detail, true))
+ pdf.Ln
+ end
+ if journal.notes?
+ pdf.SetFontStyle('',8)
+ pdf.MultiCell(190,5, journal.notes)
+ end
+ pdf.Ln
+ end
+
+ if @issue.attachments.any?
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_attachment_plural), "B")
+ pdf.Ln
+ for attachment in @issue.attachments
+ pdf.SetFontStyle('',8)
+ pdf.Cell(80,5, attachment.filename)
+ pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
+ pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
+ pdf.Cell(65,5, attachment.author.name,0,0,"R")
+ pdf.Ln
+ end
+ end
%>
<%= pdf.Output %>
diff --git a/groups/app/views/issues/show.rhtml b/groups/app/views/issues/show.rhtml index f788d0ec8..2dd1bacaa 100644 --- a/groups/app/views/issues/show.rhtml +++ b/groups/app/views/issues/show.rhtml @@ -1,5 +1,5 @@ <div class="contextual"> -<%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %> +<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> <%= watcher_tag(@issue, User.current) %> <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> @@ -18,34 +18,34 @@ <table width="100%"> <tr> - <td style="width:15%"><b><%=l(:field_status)%> :</b></td><td style="width:35%"><%= @issue.status.name %></td> - <td style="width:15%"><b><%=l(:field_start_date)%> :</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td> + <td style="width:15%"><b><%=l(:field_status)%>:</b></td><td style="width:35%"><%= @issue.status.name %></td> + <td style="width:15%"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td> </tr> <tr> - <td><b><%=l(:field_priority)%> :</b></td><td><%= @issue.priority.name %></td> - <td><b><%=l(:field_due_date)%> :</b></td><td><%= format_date(@issue.due_date) %></td> + <td><b><%=l(:field_priority)%>:</b></td><td><%= @issue.priority.name %></td> + <td><b><%=l(:field_due_date)%>:</b></td><td><%= format_date(@issue.due_date) %></td> </tr> <tr> - <td><b><%=l(:field_assigned_to)%> :</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td> - <td><b><%=l(:field_done_ratio)%> :</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td> + <td><b><%=l(:field_assigned_to)%>:</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td> + <td><b><%=l(:field_done_ratio)%>:</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td> </tr> <tr> - <td><b><%=l(:field_category)%> :</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td> + <td><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td> <% if User.current.allowed_to?(:view_time_entries, @project) %> - <td><b><%=l(:label_spent_time)%> :</b></td> + <td><b><%=l(:label_spent_time)%>:</b></td> <td><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td> <% end %> </tr> <tr> - <td><b><%=l(:field_fixed_version)%> :</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td> + <td><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td> <% if @issue.estimated_hours %> - <td><b><%=l(:field_estimated_hours)%> :</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td> + <td><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td> <% end %> </tr> <tr> -<% n = 0 -for custom_value in @custom_values %> - <td valign="top"><b><%= custom_value.custom_field.name %> :</b></td><td valign="top"><%= simple_format(h(show_value(custom_value))) %></td> +<% n = 0 -%> +<% @issue.custom_values.each do |value| -%> + <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td> <% n = n + 1 if (n > 1) n = 0 %> @@ -56,6 +56,10 @@ end %> </table> <hr /> +<div class="contextual"> +<%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %> +</div> + <p><strong><%=l(:field_description)%></strong></p> <div class="wiki"> <%= textilizable @issue, :description, :attachments => @issue.attachments %> @@ -72,6 +76,14 @@ end %> </div> <% end %> +<% if User.current.allowed_to?(:add_issue_watchers, @project) || + (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %> +<hr /> +<div id="watchers"> +<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %> +</div> +<% end %> + </div> <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %> @@ -110,4 +122,5 @@ end %> <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %> + <%= stylesheet_link_tag 'scm' %> <% end %> diff --git a/groups/app/views/layouts/base.rhtml b/groups/app/views/layouts/base.rhtml index 0b9d31512..62d542b7b 100644 --- a/groups/app/views/layouts/base.rhtml +++ b/groups/app/views/layouts/base.rhtml @@ -36,7 +36,7 @@ <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %> </div> - <h1><%= h(@project ? @project.name : Setting.app_title) %></h1> + <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1> <div id="main-menu"> <%= render_main_menu(@project) %> diff --git a/groups/app/views/mailer/layout.text.html.rhtml b/groups/app/views/mailer/layout.text.html.rhtml index b78e92bdd..c95c94501 100644 --- a/groups/app/views/mailer/layout.text.html.rhtml +++ b/groups/app/views/mailer/layout.text.html.rhtml @@ -1,12 +1,32 @@ <html> <head> <style> -body { font-family: Verdana, sans-serif; font-size: 0.8em; color:#484848; } -body h1 { font-family: "Trebuchet MS", Verdana, sans-serif; font-size: 1.2em; margin: 0;} -a, a:link, a:visited{ color: #2A5685; } -a:hover, a:active{ color: #c61a1a; } -hr { width: 100%; height: 1px; background: #ccc; border: 0; } -.footer { font-size: 0.8em; font-style: italic; } +body { + font-family: Verdana, sans-serif; + font-size: 0.8em; + color:#484848; +} +h1 { + font-family: "Trebuchet MS", Verdana, sans-serif; + font-size: 1.2em; + margin: 0px; +} +a, a:link, a:visited { + color: #2A5685; +} +a:hover, a:active { + color: #c61a1a; +} +hr { + width: 100%; + height: 1px; + background: #ccc; + border: 0; +} +.footer { + font-size: 0.8em; + font-style: italic; +} </style> </head> <body> diff --git a/groups/app/views/mailer/lost_password.text.html.rhtml b/groups/app/views/mailer/lost_password.text.html.rhtml index 26eacfa92..4dd570c94 100644 --- a/groups/app/views/mailer/lost_password.text.html.rhtml +++ b/groups/app/views/mailer/lost_password.text.html.rhtml @@ -1,2 +1,4 @@ <p><%= l(:mail_body_lost_password) %><br /> <%= auto_link(@url) %></p> + +<p><%= l(:field_login) %>: <b><%= @token.user.login %></b></p> diff --git a/groups/app/views/mailer/lost_password.text.plain.rhtml b/groups/app/views/mailer/lost_password.text.plain.rhtml index aec1b5b86..f5000ed7e 100644 --- a/groups/app/views/mailer/lost_password.text.plain.rhtml +++ b/groups/app/views/mailer/lost_password.text.plain.rhtml @@ -1,2 +1,4 @@ <%= l(:mail_body_lost_password) %> <%= @url %> + +<%= l(:field_login) %>: <%= @token.user.login %> diff --git a/groups/app/views/mailer/reminder.text.html.rhtml b/groups/app/views/mailer/reminder.text.html.rhtml new file mode 100644 index 000000000..1e33fbe43 --- /dev/null +++ b/groups/app/views/mailer/reminder.text.html.rhtml @@ -0,0 +1,9 @@ +<p><%= l(:mail_body_reminder, @issues.size, @days) %></p> + +<ul> +<% @issues.each do |issue| -%> + <li><%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %></li> +<% end -%> +</ul> + +<p><%= link_to l(:label_issue_view_all), @issues_url %></p> diff --git a/groups/app/views/mailer/reminder.text.plain.rhtml b/groups/app/views/mailer/reminder.text.plain.rhtml new file mode 100644 index 000000000..7e6a2e585 --- /dev/null +++ b/groups/app/views/mailer/reminder.text.plain.rhtml @@ -0,0 +1,7 @@ +<%= l(:mail_body_reminder, @issues.size, @days) %>: + +<% @issues.each do |issue| -%> +* <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %> +<% end -%> + +<%= @issues_url %> diff --git a/groups/app/views/messages/show.rhtml b/groups/app/views/messages/show.rhtml index 251b7c7a5..c24be7a21 100644 --- a/groups/app/views/messages/show.rhtml +++ b/groups/app/views/messages/show.rhtml @@ -2,6 +2,7 @@ link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %> <div class="contextual"> + <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %> <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %> <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> </div> @@ -17,10 +18,12 @@ </div> <br /> +<% unless @replies.empty? %> <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3> <% @replies.each do |message| %> <a name="<%= "message-#{message.id}" %>"></a> <div class="contextual"> + <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %> <%= link_to_if_authorized image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit) %> <%= link_to_if_authorized image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete) %> </div> @@ -30,6 +33,7 @@ <%= link_to_attachments message.attachments, :no_author => true %> </div> <% end %> +<% end %> <% if !@topic.locked? && authorize_for('messages', 'reply') %> <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p> @@ -48,3 +52,9 @@ <div id="preview" class="wiki"></div> </div> <% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% html_title h(@topic.subject) %> diff --git a/groups/app/views/my/blocks/_documents.rhtml b/groups/app/views/my/blocks/_documents.rhtml index a34be936f..d222e4203 100644 --- a/groups/app/views/my/blocks/_documents.rhtml +++ b/groups/app/views/my/blocks/_documents.rhtml @@ -1,8 +1,9 @@ <h3><%=l(:label_document_plural)%></h3> +<% project_ids = @user.projects.select {|p| @user.allowed_to?(:view_documents, p)}.collect(&:id) %> <%= render(:partial => 'documents/document', :collection => Document.find(:all, :limit => 10, :order => "#{Document.table_name}.created_on DESC", - :conditions => "#{Document.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", - :include => [:project])) unless @user.projects.empty? %>
\ No newline at end of file + :conditions => "#{Document.table_name}.project_id in (#{project_ids.join(',')})", + :include => [:project])) unless project_ids.empty? %>
\ No newline at end of file diff --git a/groups/app/views/news/edit.rhtml b/groups/app/views/news/edit.rhtml index a7e5e6e36..4be566e0b 100644 --- a/groups/app/views/news/edit.rhtml +++ b/groups/app/views/news/edit.rhtml @@ -5,7 +5,7 @@ <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'news', :action => 'preview' }, + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, :method => 'post', :update => 'preview', :with => "Form.serialize('news-form')" diff --git a/groups/app/views/news/index.rhtml b/groups/app/views/news/index.rhtml index 87db8a5f7..9cac39002 100644 --- a/groups/app/views/news/index.rhtml +++ b/groups/app/views/news/index.rhtml @@ -12,7 +12,7 @@ <%= render :partial => 'news/form', :locals => { :f => f } %> <%= submit_tag l(:button_create) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'news', :action => 'preview' }, + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, :method => 'post', :update => 'preview', :with => "Form.serialize('news-form')" diff --git a/groups/app/views/news/new.rhtml b/groups/app/views/news/new.rhtml index 9208d8840..a4d29a0a9 100644 --- a/groups/app/views/news/new.rhtml +++ b/groups/app/views/news/new.rhtml @@ -5,7 +5,7 @@ <%= render :partial => 'news/form', :locals => { :f => f } %> <%= submit_tag l(:button_create) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'news', :action => 'preview' }, + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, :method => 'post', :update => 'preview', :with => "Form.serialize('news-form')" diff --git a/groups/app/views/news/show.rhtml b/groups/app/views/news/show.rhtml index a55b56f0b..78be9c247 100644 --- a/groups/app/views/news/show.rhtml +++ b/groups/app/views/news/show.rhtml @@ -15,7 +15,7 @@ <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'news', :action => 'preview' }, + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, :method => 'post', :update => 'preview', :with => "Form.serialize('news-form')" @@ -55,3 +55,7 @@ <% end %> <% html_title @news.title -%> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/groups/app/views/projects/_form.rhtml b/groups/app/views/projects/_form.rhtml index 32e4dcd44..11f7e3933 100644 --- a/groups/app/views/projects/_form.rhtml +++ b/groups/app/views/projects/_form.rhtml @@ -13,12 +13,12 @@ <% unless @project.identifier_frozen? %> <br /><em><%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %></em> <% end %></p> -<p><%= f.text_field :homepage, :size => 40 %></p> +<p><%= f.text_field :homepage, :size => 60 %></p> <p><%= f.check_box :is_public %></p> <%= wikitoolbar_for 'project_description' %> -<% for @custom_value in @custom_values %> - <p><%= custom_field_tag_with_label @custom_value %></p> +<% @project.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :project, value %></p> <% end %> </div> @@ -34,15 +34,15 @@ </fieldset> <% end %> -<% unless @custom_fields.empty? %> +<% unless @issue_custom_fields.empty? %> <fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend> -<% for custom_field in @custom_fields %> +<% @issue_custom_fields.each do |custom_field| %> <label class="floating"> - <%= check_box_tag 'project[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"} : {}) %> + <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %> <%= custom_field.name %> </label> <% end %> -<%= hidden_field_tag 'project[custom_field_ids][]', '' %> +<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %> </fieldset> <% end %> <!--[eoform:project]--> diff --git a/groups/app/views/projects/activity.rhtml b/groups/app/views/projects/activity.rhtml index c2f2f9ebd..fa25812ac 100644 --- a/groups/app/views/projects/activity.rhtml +++ b/groups/app/views/projects/activity.rhtml @@ -6,11 +6,11 @@ <h3><%= format_activity_day(day) %></h3> <dl> <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> - <dt class="<%= e.event_type %>"><span class="time"><%= format_time(e.event_datetime, false) %></span> - <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %></dt> - <dd><% unless e.event_description.blank? -%> - <span class="description"><%= format_activity_description(e.event_description) %></span><br /> - <% end %> + <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>"> + <span class="time"><%= format_time(e.event_datetime, false) %></span> + <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> + <%= link_to format_activity_title(e.event_title), e.event_url %></dt> + <dd><span class="description"><%= format_activity_description(e.event_description) %></span> <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd> <% end -%> </dl> @@ -44,8 +44,8 @@ <% content_for :sidebar do %> <% form_tag({}, :method => :get) do %> <h3><%= l(:label_activity) %></h3> -<p><% @event_types.each do |t| %> -<label><%= check_box_tag "show_#{t}", 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> +<p><% @activity.event_types.each do |t| %> +<label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> <% end %></p> <% if @project && @project.active_children.any? %> <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> diff --git a/groups/app/views/projects/calendar.rhtml b/groups/app/views/projects/calendar.rhtml index 743721cb3..048d8a5df 100644 --- a/groups/app/views/projects/calendar.rhtml +++ b/groups/app/views/projects/calendar.rhtml @@ -23,7 +23,7 @@ <% content_for :sidebar do %> <h3><%= l(:label_calendar) %></h3> - <% form_tag() do %> + <% form_tag({}, :method => :get) do %> <p><%= select_month(@month, :prefix => "month", :discard_type => true) %> <%= select_year(@year, :prefix => "year", :discard_type => true) %></p> diff --git a/groups/app/views/projects/gantt.rfpdf b/groups/app/views/projects/gantt.rfpdf index a293906ba..e94fc5814 100644 --- a/groups/app/views/projects/gantt.rfpdf +++ b/groups/app/views/projects/gantt.rfpdf @@ -124,9 +124,9 @@ pdf.SetFontStyle('B',7) if i.is_a? Issue
i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
- i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to )
+ i_end_date = (i.due_before <= @date_to ? i.due_before : @date_to )
- i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
+ i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
diff --git a/groups/app/views/projects/gantt.rhtml b/groups/app/views/projects/gantt.rhtml index d941d2777..b18bca34c 100644 --- a/groups/app/views/projects/gantt.rhtml +++ b/groups/app/views/projects/gantt.rhtml @@ -166,9 +166,9 @@ top = headers_height + 10 @events.each do |i| if i.is_a? Issue i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) - i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to ) + i_end_date = (i.due_before <= @date_to ? i.due_before : @date_to ) - i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor + i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date ) @@ -190,7 +190,6 @@ top = headers_height + 10 <%= i.status.name %> <%= (i.done_ratio).to_i %>% </div> - <% # === tooltip === %> <div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;"> <span class="tip"> <%= render_issue_tooltip i %> @@ -235,7 +234,7 @@ if Date.today >= @date_from and Date.today <= @date_to %> <% content_for :sidebar do %> <h3><%= l(:label_gantt) %></h3> - <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil)) do %> + <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil), :method => :get) do %> <% @trackers.each do |tracker| %> <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %> <%= tracker.name %></label><br /> <% end %> @@ -243,7 +242,7 @@ if Date.today >= @date_from and Date.today <= @date_to %> <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label> <%= hidden_field_tag 'with_subprojects', 0 %> <% end %> - <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p> + <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p> <% end %> <% end %> diff --git a/groups/app/views/projects/list.rhtml b/groups/app/views/projects/index.rhtml index b8bb62ebb..4c68717f5 100644 --- a/groups/app/views/projects/list.rhtml +++ b/groups/app/views/projects/index.rhtml @@ -1,4 +1,5 @@ <div class="contextual"> + <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.admin? %> <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> | <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%> </div> @@ -17,9 +18,14 @@ <% end %> <% if User.current.logged? %> -<div class="contextual"> +<p style="text-align:right;"> <span class="icon icon-fav"><%= l(:label_my_projects) %></span> -</div> +</p> <% end %> +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span> +</p> + <% html_title(l(:label_project_plural)) -%> diff --git a/groups/app/views/projects/list_files.rhtml b/groups/app/views/projects/list_files.rhtml index f385229ae..79e41f16d 100644 --- a/groups/app/views/projects/list_files.rhtml +++ b/groups/app/views/projects/list_files.rhtml @@ -23,8 +23,7 @@ <% for file in version.attachments %> <tr class="<%= cycle("odd", "even") %>"> <td></td> - <td><%= link_to(file.filename, {:controller => 'versions', :action => 'download', :id => version, :attachment_id => file}, - :title => file.description) %></td> + <td><%= link_to_attachment file, :download => true, :title => file.description %></td> <td align="center"><%= format_time(file.created_on) %></td> <td align="center"><%= number_to_human_size(file.filesize) %></td> <td align="center"><%= file.downloads %></td> diff --git a/groups/app/views/projects/roadmap.rhtml b/groups/app/views/projects/roadmap.rhtml index d9329d109..0778d8138 100644 --- a/groups/app/views/projects/roadmap.rhtml +++ b/groups/app/views/projects/roadmap.rhtml @@ -20,7 +20,7 @@ <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend> <ul> <%- issues.each do |issue| -%> - <li class="issue <%= 'closed' if issue.closed? %>"><%= link_to_issue(issue) %>: <%=h issue.subject %></li> + <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li> <%- end -%> </ul> </fieldset> @@ -30,7 +30,7 @@ <% end %> <% content_for :sidebar do %> -<% form_tag do %> +<% form_tag({}, :method => :get) do %> <h3><%= l(:label_roadmap) %></h3> <% @trackers.each do |tracker| %> <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %> @@ -38,12 +38,12 @@ <% end %> <br /> <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label> -<p><%= submit_tag l(:button_apply), :class => 'button-small' %></p> +<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p> <% end %> <h3><%= l(:label_version_plural) %></h3> <% @versions.each do |version| %> -<%= link_to version.name, :anchor => version.name %><br /> +<%= link_to version.name, "##{version.name}" %><br /> <% end %> <% end %> diff --git a/groups/app/views/projects/settings/_repository.rhtml b/groups/app/views/projects/settings/_repository.rhtml index 95830ab98..dcfabbbf0 100644 --- a/groups/app/views/projects/settings/_repository.rhtml +++ b/groups/app/views/projects/settings/_repository.rhtml @@ -17,5 +17,5 @@ :class => 'icon icon-del') if @repository && !@repository.new_record? %> </div> -<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save)) %> +<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save), :disabled => @repository.nil?) %> <% end %> diff --git a/groups/app/views/projects/show.rhtml b/groups/app/views/projects/show.rhtml index 66c4838d6..778c8c220 100644 --- a/groups/app/views/projects/show.rhtml +++ b/groups/app/views/projects/show.rhtml @@ -3,14 +3,14 @@ <div class="splitcontentleft"> <%= textilizable @project.description %> <ul> - <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link @project.homepage %></li><% end %> + <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)) %></li><% end %> <% if @subprojects.any? %> <li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li> <% end %> <% if @project.parent %> <li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li> <% end %> - <% for custom_value in @custom_values %> + <% @project.custom_values.each do |custom_value| %> <% if !custom_value.value.empty? %> <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li> <% end %> diff --git a/groups/app/views/queries/_filters.rhtml b/groups/app/views/queries/_filters.rhtml index ec9d4fef6..c9d612364 100644 --- a/groups/app/views/queries/_filters.rhtml +++ b/groups/app/views/queries/_filters.rhtml @@ -78,7 +78,7 @@ function toggle_multi_select(field) { <select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="values[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;"> <%= options_for_select options[:values], query.values_for(field) %> </select> - <%= link_to_function image_tag('expand.png'), "toggle_multi_select('#{field}');" %> + <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %> <% when :date, :date_past %> <%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %> <% when :string, :text %> diff --git a/groups/app/views/repositories/_dir_list_content.rhtml b/groups/app/views/repositories/_dir_list_content.rhtml index 3564e52ab..20473a264 100644 --- a/groups/app/views/repositories/_dir_list_content.rhtml +++ b/groups/app/views/repositories/_dir_list_content.rhtml @@ -1,32 +1,24 @@ <% @entries.each do |entry| %> <% tr_id = Digest::MD5.hexdigest(entry.path) depth = params[:depth].to_i %> -<tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry"> -<td class="filename"> -<%= if entry.is_dir? - link_to_remote h(entry.name), - {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id}, +<tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry <%= entry.kind %>"> +<td style="padding-left: <%=18 * depth%>px;" class="filename"> +<% if entry.is_dir? %> +<span class="expander" onclick="<%= remote_function :url => {:action => 'browse', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id}, :update => { :success => tr_id }, :position => :after, :success => "scmEntryLoaded('#{tr_id}')", - :condition => "scmEntryClick('#{tr_id}')" - }, - {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}), - :class => ('icon icon-folder'), - :style => "margin-left: #{18 * depth}px;" - } -else - link_to h(entry.name), - {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev}, - :class => 'icon icon-file', - :style => "margin-left: #{18 * depth}px;" -end %> + :condition => "scmEntryClick('#{tr_id}')"%>"> </span> +<% end %> +<%= link_to h(entry.name), + {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev}, + :class => (entry.is_dir? ? 'icon icon-folder' : 'icon icon-file')%> </td> <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td> <td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td> <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td> <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td> -<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %> +<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> <td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td> </tr> <% end %> diff --git a/groups/app/views/repositories/_navigation.rhtml b/groups/app/views/repositories/_navigation.rhtml index b7ac989bc..25a15f496 100644 --- a/groups/app/views/repositories/_navigation.rhtml +++ b/groups/app/views/repositories/_navigation.rhtml @@ -10,10 +10,10 @@ 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 %> + / <%= link_to h(dir), :action => 'browse', :id => @project, :path => to_path_param(link_path), :rev => @rev %> <% end %> <% if filename %> - / <%= link_to h(filename), :action => 'changes', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %> + / <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %> <% end %> <%= "@ #{revision}" if revision %> diff --git a/groups/app/views/repositories/_revisions.rhtml b/groups/app/views/repositories/_revisions.rhtml index 1bcf0208c..a938fecb8 100644 --- a/groups/app/views/repositories/_revisions.rhtml +++ b/groups/app/views/repositories/_revisions.rhtml @@ -1,4 +1,4 @@ -<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => path}, :method => :get) do %> +<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %> <table class="list changesets"> <thead><tr> <th>#</th> diff --git a/groups/app/views/repositories/browse.rhtml b/groups/app/views/repositories/browse.rhtml index 868388f11..4029a77d2 100644 --- a/groups/app/views/repositories/browse.rhtml +++ b/groups/app/views/repositories/browse.rhtml @@ -7,6 +7,7 @@ <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2> <%= render :partial => 'dir_list' %> +<%= render_properties(@properties) %> <% content_for :header_tags do %> <%= stylesheet_link_tag "scm" %> diff --git a/groups/app/views/repositories/changes.rhtml b/groups/app/views/repositories/changes.rhtml index 2d7462b29..ca5c58328 100644 --- a/groups/app/views/repositories/changes.rhtml +++ b/groups/app/views/repositories/changes.rhtml @@ -1,18 +1,19 @@ <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2> -<h3><%=h @entry.name %></h3> - <p> <% if @repository.supports_cat? %> - <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> | + <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | <% end %> <% if @repository.supports_annotate? %> - <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> | + <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | <% end %> -<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %> +<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %> <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %> </p> -<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%> +<%= render_properties(@properties) %> + +<%= render(:partial => 'revisions', + :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %> <% html_title(l(:label_change_plural)) -%> diff --git a/groups/app/views/repositories/diff.rhtml b/groups/app/views/repositories/diff.rhtml index eaef1abf5..52a5d6057 100644 --- a/groups/app/views/repositories/diff.rhtml +++ b/groups/app/views/repositories/diff.rhtml @@ -11,82 +11,14 @@ <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p> <% end %> -<% cache(@cache_key) do %> -<% @diff.each do |table_file| %> -<div class="autoscroll"> -<% if @diff_type == 'sbs' %> - <table class="filecontent CodeRay"> - <thead> - <tr> - <th colspan="4" class="filename"> - <%= table_file.file_name %> - </th> - </tr> - <tr> - <th colspan="2">@<%= format_revision @rev %></th> - <th colspan="2">@<%= format_revision @rev_to %></th> - </tr> - </thead> - <tbody> - <% table_file.keys.sort.each do |key| %> - <tr> - <th class="line-num"> - <%= table_file[key].nb_line_left %> - </th> - <td class="line-code <%= table_file[key].type_diff_left %>"> - <pre><%=to_utf8 table_file[key].line_left %></pre> - </td> - <th class="line-num"> - <%= table_file[key].nb_line_right %> - </th> - <td class="line-code <%= table_file[key].type_diff_right %>"> - <pre><%=to_utf8 table_file[key].line_right %></pre> - </td> - </tr> - <% end %> - </tbody> - </table> +<% cache(@cache_key) do -%> +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> +<% end -%> -<% else %> - <table class="filecontent CodeRay"> - <thead> - <tr> - <th colspan="3" class="filename"> - <%= table_file.file_name %> - </th> - </tr> - <tr> - <th>@<%= format_revision @rev %></th> - <th>@<%= format_revision @rev_to %></th> - <th></th> - </tr> - </thead> - <tbody> - <% table_file.keys.sort.each do |key, line| %> - <tr> - <th class="line-num"> - <%= table_file[key].nb_line_left %> - </th> - <th class="line-num"> - <%= table_file[key].nb_line_right %> - </th> - <% if table_file[key].line_left.empty? %> - <td class="line-code <%= table_file[key].type_diff_right %>"> - <pre><%=to_utf8 table_file[key].line_right %></pre> - </td> - <% else %> - <td class="line-code <%= table_file[key].type_diff_left %>"> - <pre><%=to_utf8 table_file[key].line_left %></pre> - </td> - <% end %> - </tr> - <% end %> - </tbody> - </table> -<% end %> -</div> -<% end %> -<% end %> +<p class="other-formats"> +<%= l(:label_export_to) %> +<span><%= link_to 'Unified diff', params.merge(:format => 'diff') %></span> +</p> <% html_title(with_leading_slash(@path), 'Diff') -%> diff --git a/groups/app/views/repositories/entry.rhtml b/groups/app/views/repositories/entry.rhtml index 309da76fc..8e1e1992c 100644 --- a/groups/app/views/repositories/entry.rhtml +++ b/groups/app/views/repositories/entry.rhtml @@ -1,16 +1,6 @@ <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2> -<div class="autoscroll"> -<table class="filecontent CodeRay"> -<tbody> -<% line_num = 1 %> -<% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %> -<tr><th class="line-num" id="L<%= line_num %>"><%= line_num %></th><td class="line-code"><pre><%= line %></pre></td></tr> -<% line_num += 1 %> -<% end %> -</tbody> -</table> -</div> +<%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %> <% content_for :header_tags do %> <%= stylesheet_link_tag "scm" %> diff --git a/groups/app/views/repositories/revision.rhtml b/groups/app/views/repositories/revision.rhtml index f1e176669..80ac3bd1a 100644 --- a/groups/app/views/repositories/revision.rhtml +++ b/groups/app/views/repositories/revision.rhtml @@ -13,9 +13,9 @@ <% end -%> » - <% form_tag do %> + <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %> <%= text_field_tag 'rev', @rev, :size => 5 %> - <%= submit_tag 'OK' %> + <%= submit_tag 'OK', :name => nil %> <% end %> </div> @@ -46,10 +46,16 @@ <tbody> <% @changes.each do |change| %> <tr class="<%= cycle 'odd', 'even' %>"> -<td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td> +<td><div class="square action_<%= change.action %>"></div> +<% if change.action == "D" -%> + <%= change.path -%> +<% else -%> + <%= link_to change.path, :action => 'entry', :id => @project, :path => to_path_param(change.relative_path), :rev => @changeset.revision -%> +<% end -%> +<%= "(#{change.revision})" unless change.revision.blank? %></td> <td align="right"> <% if change.action == "M" %> -<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.path), :rev => @changeset.revision %> +<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => to_path_param(change.relative_path), :rev => @changeset.revision %> <% end %> </td> </tr> diff --git a/groups/app/views/repositories/show.rhtml b/groups/app/views/repositories/show.rhtml index 469ac063e..9a73183e8 100644 --- a/groups/app/views/repositories/show.rhtml +++ b/groups/app/views/repositories/show.rhtml @@ -1,11 +1,16 @@ <div class="contextual"> <%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %> + +<% if !@entries.nil? && authorize_for('repositories', 'browse') -%> +<% form_tag(:action => 'browse', :id => @project) do -%> +| <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<% end -%> +<% end -%> </div> <h2><%= l(:label_repository) %> (<%= @repository.scm_name %>)</h2> <% if !@entries.nil? && authorize_for('repositories', 'browse') %> -<h3><%= l(:label_browse) %></h3> <%= render :partial => 'dir_list' %> <% end %> diff --git a/groups/app/views/repositories/stats.rhtml b/groups/app/views/repositories/stats.rhtml index 76ce892d5..1e617577e 100644 --- a/groups/app/views/repositories/stats.rhtml +++ b/groups/app/views/repositories/stats.rhtml @@ -1,13 +1,12 @@ <h2><%= l(:label_statistics) %></h2>
-<table width="100%">
-<tr><td>
-<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
-</td><td>
-<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
-</td></tr>
-</table>
-<br />
+<p>
+<%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
+</p>
+<p>
+<%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
+</p>
+
<p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
<% html_title(l(:label_repository), l(:label_statistics)) -%>
diff --git a/groups/app/views/roles/_form.rhtml b/groups/app/views/roles/_form.rhtml index 58dc2af41..4aad45471 100644 --- a/groups/app/views/roles/_form.rhtml +++ b/groups/app/views/roles/_form.rhtml @@ -12,7 +12,7 @@ <% end %> <h3><%= l(:label_permissions) %></h3> -<div class="box"> +<div class="box" id="permissions"> <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> <% perms_by_module.keys.sort.each do |mod| %> <fieldset><legend><%= mod.blank? ? l(:label_project) : mod.humanize %></legend> @@ -24,6 +24,6 @@ <% end %> </fieldset> <% end %> -<br /><%= check_all_links 'role_form' %> +<br /><%= check_all_links 'permissions' %> <%= hidden_field_tag 'role[permissions][]', '' %> </div> diff --git a/groups/app/views/roles/report.rhtml b/groups/app/views/roles/report.rhtml index 98c3b651e..8e254379e 100644 --- a/groups/app/views/roles/report.rhtml +++ b/groups/app/views/roles/report.rhtml @@ -1,13 +1,17 @@ <h2><%=l(:label_permissions_report)%></h2> <% form_tag({:action => 'report'}, :id => 'permissions_form') do %> -<%= hidden_field_tag 'permissions[0]', '' %> +<%= hidden_field_tag 'permissions[0]', '', :id => nil %> <table class="list"> <thead> <tr> <th><%=l(:label_permissions)%></th> <% @roles.each do |role| %> - <th><%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %></th> + <th> + <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %> + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + </th> <% end %> </tr> </thead> @@ -18,12 +22,16 @@ <tr><%= content_tag('th', mod.humanize, :colspan => (@roles.size + 1), :align => 'left') %></tr> <% end %> <% perms_by_module[mod].each do |permission| %> - <tr class="<%= cycle('odd', 'even') %>"> - <td><%= permission.name.to_s.humanize %></td> + <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>"> + <td> + <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')", + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %> + <%= permission.name.to_s.humanize %> + </td> <% @roles.each do |role| %> <td align="center"> <% if role.setable_permissions.include? permission %> - <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name) %> + <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %> <% end %> </td> <% end %> diff --git a/groups/app/views/search/index.rhtml b/groups/app/views/search/index.rhtml index 29c604a21..cb5b70a4c 100644 --- a/groups/app/views/search/index.rhtml +++ b/groups/app/views/search/index.rhtml @@ -4,27 +4,33 @@ <% form_tag({}, :method => :get) do %>
<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
<%= javascript_tag "Field.focus('search-input')" %>
-
-<% @object_types.each do |t| %>
-<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
-<% end %>
-<br />
+<%= project_select_tag %>
<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
</p>
-<%= submit_tag l(:button_submit), :name => 'submit' %>
+<p>
+<% @object_types.each do |t| %>
+<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
+<% end %>
+</p>
+
+<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
<% end %>
</div>
<% if @results %>
- <h3><%= l(:label_result_plural) %></h3>
- <ul>
+ <div id="search-results-counts">
+ <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
+ </div>
+
+ <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
+ <dl id="search-results">
<% @results.each do |e| %>
- <li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />
- <%= highlight_tokens(e.event_description, @tokens) %><br />
- <span class="author"><%= format_time(e.event_datetime) %></span></p></li>
+ <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %></dt>
+ <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
+ <span class="author"><%= format_time(e.event_datetime) %></span></dd>
<% end %>
- </ul>
+ </dl>
<% end %>
<p><center>
diff --git a/groups/app/views/settings/_mail_handler.rhtml b/groups/app/views/settings/_mail_handler.rhtml new file mode 100644 index 000000000..830b1ba4a --- /dev/null +++ b/groups/app/views/settings/_mail_handler.rhtml @@ -0,0 +1,18 @@ +<% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %> + +<div class="box tabular settings"> +<p><label><%= l(:setting_mail_handler_api_enabled) %></label> +<%= check_box_tag 'settings[mail_handler_api_enabled]', 1, Setting.mail_handler_api_enabled?, + :onclick => "if (this.checked) { Form.Element.enable('settings_mail_handler_api_key'); } else { Form.Element.disable('settings_mail_handler_api_key'); }" %> +<%= hidden_field_tag 'settings[mail_handler_api_enabled]', 0 %></p> + +<p><label><%= l(:setting_mail_handler_api_key) %></label> +<%= text_field_tag 'settings[mail_handler_api_key]', Setting.mail_handler_api_key, + :size => 30, + :id => 'settings_mail_handler_api_key', + :disabled => !Setting.mail_handler_api_enabled? %> +<%= link_to_function l(:label_generate_key), "if ($('settings_mail_handler_api_key').disabled == false) { $('settings_mail_handler_api_key').value = randomKey(20) }" %></p> +</div> + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/settings/_notifications.rhtml b/groups/app/views/settings/_notifications.rhtml index ac3213853..36701463a 100644 --- a/groups/app/views/settings/_notifications.rhtml +++ b/groups/app/views/settings/_notifications.rhtml @@ -1,3 +1,4 @@ +<% if @deliveries %> <% form_tag({:action => 'edit', :tab => 'notifications'}) do %> <div class="box tabular settings"> @@ -9,13 +10,13 @@ <%= hidden_field_tag 'settings[bcc_recipients]', 0 %></p> </div> -<fieldset class="box"><legend><%=l(:text_select_mail_notifications)%></legend> +<fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend> <% @notifiables.each do |notifiable| %> <label><%= check_box_tag 'settings[notified_events][]', notifiable, Setting.notified_events.include?(notifiable) %> <%= l_or_humanize(notifiable) %></label><br /> <% end %> <%= hidden_field_tag 'settings[notified_events][]', '' %> -<p><%= check_all_links('mail-options-form') %></p> +<p><%= check_all_links('notified_events') %></p> </fieldset> <fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend> @@ -28,3 +29,8 @@ <%= submit_tag l(:button_save) %> <% end %> +<% else %> +<div class="nodata"> +<%= simple_format(l(:text_email_delivery_not_configured)) %> +</div> +<% end %> diff --git a/groups/app/views/settings/_repositories.rhtml b/groups/app/views/settings/_repositories.rhtml index 59b3b51de..a8c924430 100644 --- a/groups/app/views/settings/_repositories.rhtml +++ b/groups/app/views/settings/_repositories.rhtml @@ -7,8 +7,18 @@ <p><label><%= l(:setting_sys_api_enabled) %></label> <%= check_box_tag 'settings[sys_api_enabled]', 1, Setting.sys_api_enabled? %><%= hidden_field_tag 'settings[sys_api_enabled]', 0 %></p> +<p><label><%= l(:setting_enabled_scm) %></label> +<% REDMINE_SUPPORTED_SCM.each do |scm| -%> +<%= check_box_tag 'settings[enabled_scm][]', scm, Setting.enabled_scm.include?(scm) %> <%= scm %> +<% end -%> +<%= hidden_field_tag 'settings[enabled_scm][]', '' %> +</p> + <p><label><%= l(:setting_repositories_encodings) %></label> <%= text_field_tag 'settings[repositories_encodings]', Setting.repositories_encodings, :size => 60 %><br /><em><%= l(:text_comma_separated) %></em></p> + +<p><label><%= l(:setting_commit_logs_encoding) %></label> +<%= select_tag 'settings[commit_logs_encoding]', options_for_select(Setting::ENCODINGS, Setting.commit_logs_encoding) %></p> </div> <fieldset class="box tabular settings"><legend><%= l(:text_issues_ref_in_commit_messages) %></legend> diff --git a/groups/app/views/timelog/_list.rhtml b/groups/app/views/timelog/_list.rhtml index 189f4f5e8..8aebd75de 100644 --- a/groups/app/views/timelog/_list.rhtml +++ b/groups/app/views/timelog/_list.rhtml @@ -27,9 +27,9 @@ <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td> <td align="center"> <% if entry.editable_by?(User.current) -%> - <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry}, + <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil}, :title => l(:button_edit) %> - <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry}, + <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil}, :confirm => l(:text_are_you_sure), :method => :post, :title => l(:button_delete) %> diff --git a/groups/app/views/timelog/_report_criteria.rhtml b/groups/app/views/timelog/_report_criteria.rhtml index 94f3d20f9..c9a1cfb45 100644 --- a/groups/app/views/timelog/_report_criteria.rhtml +++ b/groups/app/views/timelog/_report_criteria.rhtml @@ -3,7 +3,7 @@ <% next if hours_for_value.empty? -%> <tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>"> <%= '<td></td>' * level %> -<td><%= format_criteria_value(criterias[level], value) %></td> +<td><%= h(format_criteria_value(criterias[level], value)) %></td> <%= '<td></td>' * (criterias.length - level - 1) -%> <% total = 0 -%> <% @periods.each do |period| -%> diff --git a/groups/app/views/timelog/details.rhtml b/groups/app/views/timelog/details.rhtml index f02da9959..f111cbfc0 100644 --- a/groups/app/views/timelog/details.rhtml +++ b/groups/app/views/timelog/details.rhtml @@ -24,8 +24,13 @@ <p class="other-formats">
<%= l(:label_export_to) %>
+<span><%= link_to 'Atom', {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
<span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
</p>
<% end %>
<% html_title l(:label_spent_time), l(:label_details) %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
+<% end %>
diff --git a/groups/app/views/timelog/edit.rhtml b/groups/app/views/timelog/edit.rhtml index f9dae8a99..0dd3503ec 100644 --- a/groups/app/views/timelog/edit.rhtml +++ b/groups/app/views/timelog/edit.rhtml @@ -9,7 +9,10 @@ <p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
<p><%= f.text_field :hours, :size => 6, :required => true %></p>
<p><%= f.text_field :comments, :size => 100 %></p>
-<p><%= f.select :activity_id, (@activities.collect {|p| [p.name, p.id]}), :required => true %></p>
+<p><%= f.select :activity_id, activity_collection_for_select_options, :required => true %></p>
+<% @time_entry.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :time_entry, value %></p>
+<% end %>
</div>
<%= submit_tag l(:button_save) %>
diff --git a/groups/app/views/users/_form.rhtml b/groups/app/views/users/_form.rhtml index f2b330828..e305581fe 100644 --- a/groups/app/views/users/_form.rhtml +++ b/groups/app/views/users/_form.rhtml @@ -2,7 +2,6 @@ <!--[form:user]--> <div class="box"> -<h3><%=l(:label_information_plural)%></h3> <p><%= f.text_field :login, :required => true, :size => 25 %></p> <p><%= f.text_field :firstname, :required => true %></p> <p><%= f.text_field :lastname, :required => true %></p> @@ -12,11 +11,11 @@ <% end -%> <p><%= f.select :language, lang_options_for_select %></p> -<% for @custom_value in @custom_values %> - <p><%= custom_field_tag_with_label @custom_value %></p> -<% end if @custom_values%> +<% @user.custom_field_values.each do |value| %> + <p><%= custom_field_tag_with_label :user, value %></p> +<% end %> -<p><%= f.check_box :admin %></p> +<p><%= f.check_box :admin, :disabled => (@user == User.current) %></p> </div> <div class="box"> diff --git a/groups/app/views/users/_general.rhtml b/groups/app/views/users/_general.rhtml new file mode 100644 index 000000000..80615ff6c --- /dev/null +++ b/groups/app/views/users/_general.rhtml @@ -0,0 +1,4 @@ +<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %> +<%= render :partial => 'form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/groups/app/views/users/_memberships.rhtml b/groups/app/views/users/_memberships.rhtml index 06d3f6029..94b49159e 100644 --- a/groups/app/views/users/_memberships.rhtml +++ b/groups/app/views/users/_memberships.rhtml @@ -1,33 +1,40 @@ -<div class="box" style="margin-top: 16px;"> -<h3><%= l(:label_project_plural) %></h3> - -<% @user.memberships.select {|m| m.inherited_from.nil? }.each do |membership| %> -<% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }, :class => "tabular") do %> -<p style="margin:0;padding-top:0;"> - <label><%= membership.project.name %></label> - <select name="membership[role_id]"> - <%= options_from_collection_for_select @roles, "id", "name", membership.role_id %> - </select> - <%= submit_tag l(:button_change), :class => "button-small" %> - <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> -</p> -<% end %> +<% if @memberships.any? %> +<table class="list memberships"> + <thead> + <th><%= l(:label_project) %></th> + <th><%= l(:label_role) %></th> + <th style="width:15%"></th> + </thead> + <tbody> + <% @memberships.each do |membership| %> + <% next if membership.new_record? %> + <tr class="<%= cycle 'odd', 'even' %>"> + <td><%=h membership.project %></td> + <td align="center"> + <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %> + <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %> + <%= submit_tag l(:button_change), :class => "small" %> + <% end %> + </td> + <td align="center"> + <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %> + </td> + </tr> + </tbody> +<% end; reset_cycle %> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> <% end %> -<% unless @projects.empty? || @roles.empty? %> -<hr /> +<% if @projects.any? %> <p> <label><%=l(:label_project_new)%></label><br/> <% form_tag({ :action => 'edit_membership', :id => @user }) do %> -<select name="membership[project_id]"> -<%= options_from_collection_for_select @projects, "id", "name", @membership.project_id %> -</select> +<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %> <%= l(:label_role) %>: -<select name="membership[role_id]"> -<%= options_from_collection_for_select @roles, "id", "name", @membership.role_id %> -</select> +<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %> <%= submit_tag l(:button_add) %> <% end %> </p> <% end %> -</div>
\ No newline at end of file diff --git a/groups/app/views/users/edit.rhtml b/groups/app/views/users/edit.rhtml index 0da99d0d2..4714bcecb 100644 --- a/groups/app/views/users/edit.rhtml +++ b/groups/app/views/users/edit.rhtml @@ -1,8 +1,27 @@ -<h2><%=l(:label_user)%></h2> +<div class="contextual"> +<%= change_status_link(@user) %> +</div> -<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %> -<%= render :partial => 'form', :locals => { :f => f } %> -<%= submit_tag l(:button_save) %> -<% end %> +<h2><%=l(:label_user)%>: <%=h @user.login %></h2> -<%= render :partial => 'memberships' %>
\ No newline at end of file +<% selected_tab = params[:tab] ? params[:tab].to_s : user_settings_tabs.first[:name] %> + +<div class="tabs"> +<ul> +<% user_settings_tabs.each do |tab| -%> + <li><%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li> +<% end -%> +</ul> +</div> + +<% user_settings_tabs.each do |tab| -%> +<%= content_tag('div', render(:partial => tab[:partial]), + :id => "tab-content-#{tab[:name]}", + :style => (tab[:name] != selected_tab ? 'display:none' : nil), + :class => 'tab-content') %> +<% end -%> + +<% html_title(l(:label_user), @user.login, l(:label_administration)) -%> diff --git a/groups/app/views/users/list.rhtml b/groups/app/views/users/list.rhtml index 47a629469..6e6861ea9 100644 --- a/groups/app/views/users/list.rhtml +++ b/groups/app/views/users/list.rhtml @@ -7,7 +7,7 @@ <% form_tag({}, :method => :get) do %> <fieldset><legend><%= l(:label_filter_plural) %></legend> <label><%= l(:field_status) %> :</label> -<%= select_tag 'status', status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> +<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> </fieldset> <% end %> @@ -27,10 +27,10 @@ <tbody> <% for user in @users -%> <tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>"> - <td class="username"><%= link_to user.login, :action => 'edit', :id => user %></td> - <td class="firstname"><%=h user.firstname %></td> - <td class="lastname"><%=h user.lastname %></td> - <td class="email"><%=h user.mail %></td> + <td class="username"><%= link_to h(user.login), :action => 'edit', :id => user %></td> + <td class="firstname"><%= h(user.firstname) %></td> + <td class="lastname"><%= h(user.lastname) %></td> + <td class="email"><%= mail_to(h(user.mail)) %></td> <td class="group"><%=h user.group %></td> <td align="center"><%= image_tag('true.png') if user.admin? %></td> <td class="created_on" align="center"><%= format_time(user.created_on) %></td> diff --git a/groups/app/views/versions/show.rhtml b/groups/app/views/versions/show.rhtml index 7f81cf503..7f9518af8 100644 --- a/groups/app/views/versions/show.rhtml +++ b/groups/app/views/versions/show.rhtml @@ -38,7 +38,7 @@ <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend> <ul> <% issues.each do |issue| -%> - <li class="issue <%= 'closed' if issue.closed? %>"><%= link_to_issue(issue) %>: <%=h issue.subject %></li> + <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li> <% end -%> </ul> </fieldset> diff --git a/groups/app/views/watchers/_watchers.rhtml b/groups/app/views/watchers/_watchers.rhtml new file mode 100644 index 000000000..14bb5fc6b --- /dev/null +++ b/groups/app/views/watchers/_watchers.rhtml @@ -0,0 +1,25 @@ +<div class="contextual"> +<%= link_to_remote l(:button_add), + :url => {:controller => 'watchers', + :action => 'new', + :object_type => watched.class.name.underscore, + :object_id => watched} if User.current.allowed_to?(:add_issue_watchers, @project) %> +</div> + +<p><strong><%= l(:label_issue_watchers) %></strong></p> +<%= watchers_list(watched) %> + +<% unless @watcher.nil? %> +<% remote_form_for(:watcher, @watcher, + :url => {:controller => 'watchers', + :action => 'new', + :object_type => watched.class.name.underscore, + :object_id => watched}, + :method => :post, + :html => {:id => 'new-watcher-form'}) do |f| %> +<p><%= f.select :user_id, (watched.addable_watcher_users.collect {|m| [m.name, m.id]}), :prompt => true %> + +<%= submit_tag l(:button_add) %> +<%= toggle_link l(:button_cancel), 'new-watcher-form'%></p> +<% end %> +<% end %> diff --git a/groups/app/views/welcome/index.rhtml b/groups/app/views/welcome/index.rhtml index 5da5a1ed3..855248c5e 100644 --- a/groups/app/views/welcome/index.rhtml +++ b/groups/app/views/welcome/index.rhtml @@ -12,17 +12,19 @@ </div> <div class="splitcontentright"> + <% if @projects.any? %> <div class="box"> <h3 class="icon22 icon22-projects"><%=l(:label_project_latest)%></h3> <ul> <% for project in @projects %> <li> - <%= link_to project.name, :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>) + <%= link_to h(project.name), :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>) <%= textilizable project.short_description, :project => project %> </li> <% end %> </ul> - </div> + </div> + <% end %> </div> <% content_for :header_tags do %> diff --git a/groups/app/views/wiki/export.rhtml b/groups/app/views/wiki/export.rhtml index 1ab5c13e4..94b4e6f0d 100644 --- a/groups/app/views/wiki/export.rhtml +++ b/groups/app/views/wiki/export.rhtml @@ -6,6 +6,10 @@ <style> body { font:80% Verdana,Tahoma,Arial,sans-serif; } h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; } +ul.toc { padding: 4px; margin-left: 0; } +ul.toc li { list-style-type:none; } +ul.toc li.heading2 { margin-left: 1em; } +ul.toc li.heading3 { margin-left: 2em; } </style> </head> <body> diff --git a/groups/app/views/wiki/history.rhtml b/groups/app/views/wiki/history.rhtml index 6462e9fdd..7ce78a0f2 100644 --- a/groups/app/views/wiki/history.rhtml +++ b/groups/app/views/wiki/history.rhtml @@ -18,7 +18,7 @@ <% line_num = 1 %> <% @versions.each do |ver| %> <tr class="<%= cycle("odd", "even") %>"> - <td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></th> + <td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></td> <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td> <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true || $('version_from').value > #{ver.version}) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td> <td align="center"><%= format_time(ver.updated_on) %></td> @@ -30,6 +30,6 @@ <% end %> </tbody> </table> -<%= submit_tag l(:label_view_diff), :class => 'small' %> +<%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %> <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span> <% end %> diff --git a/groups/app/views/wiki/rename.rhtml b/groups/app/views/wiki/rename.rhtml index 0c069f43d..260f9af8b 100644 --- a/groups/app/views/wiki/rename.rhtml +++ b/groups/app/views/wiki/rename.rhtml @@ -4,8 +4,9 @@ <% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %> <div class="box"> -<p><%= f.text_field :title, :required => true, :size => 255 %></p> +<p><%= f.text_field :title, :required => true, :size => 100 %></p> <p><%= f.check_box :redirect_existing_links %></p> +<p><%= f.text_field :parent_title, :size => 100 %></p> </div> <%= submit_tag l(:button_rename) %> <% end %> diff --git a/groups/app/views/wiki/show.rhtml b/groups/app/views/wiki/show.rhtml index e4413d090..255b904f5 100644 --- a/groups/app/views/wiki/show.rhtml +++ b/groups/app/views/wiki/show.rhtml @@ -1,11 +1,17 @@ <div class="contextual"> +<% if @editable %> <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %> +<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %> +<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %> <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %> <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %> +<% end %> <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> </div> +<%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %> + <% if @content.version != @page.content.version %> <p> <%= link_to(('« ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %> @@ -22,9 +28,9 @@ <%= render(:partial => "wiki/content", :locals => {:content => @content}) %> -<%= link_to_attachments @page.attachments, :delete_url => (authorize_for('wiki', 'destroy_attachment') ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %> +<%= link_to_attachments @page.attachments, :delete_url => ((@editable && authorize_for('wiki', 'destroy_attachment')) ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %> -<% if authorize_for('wiki', 'add_attachment') %> +<% if @editable && authorize_for('wiki', 'add_attachment') %> <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", :id => 'attach_files_link' %></p> <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> diff --git a/groups/app/views/wiki/special_page_index.rhtml b/groups/app/views/wiki/special_page_index.rhtml index f21cc3423..72b395ef7 100644 --- a/groups/app/views/wiki/special_page_index.rhtml +++ b/groups/app/views/wiki/special_page_index.rhtml @@ -4,11 +4,7 @@ <p class="nodata"><%= l(:label_no_data) %></p> <% end %> -<ul><% @pages.each do |page| %> - <li><%= link_to page.pretty_title, {:action => 'index', :page => page.title}, - :title => l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) %> - </li> -<% end %></ul> +<%= render_page_hierarchy(@pages_by_parent_id) %> <% content_for :sidebar do %> <%= render :partial => 'sidebar' %> diff --git a/groups/config/boot.rb b/groups/config/boot.rb index 9fcd50fe3..cd21fb9ea 100644 --- a/groups/config/boot.rb +++ b/groups/config/boot.rb @@ -1,19 +1,109 @@ -# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb +# Don't change this file! +# Configure your app 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 +RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) + +module Rails + class << self + def boot! + unless booted? + preinitialize + pick_boot.run + end + end + + def booted? + defined? Rails::Initializer + end + + def pick_boot + (vendor_rails? ? VendorBoot : GemBoot).new + end + + def vendor_rails? + File.exist?("#{RAILS_ROOT}/vendor/rails") + end + + def preinitialize + load(preinitializer_path) if File.exist?(preinitializer_path) + end + + def preinitializer_path + "#{RAILS_ROOT}/config/preinitializer.rb" + end end - 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' + class Boot + def run + load_initializer + Rails::Initializer.run(:set_load_path) + end + end + + class VendorBoot < Boot + def load_initializer + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + Rails::Initializer.run(:install_gem_spec_stubs) + end + end + + class GemBoot < Boot + def load_initializer + self.class.load_rubygems + load_rails_gem + require 'initializer' + end + + def load_rails_gem + if version = self.class.gem_version + gem 'rails', version + else + gem 'rails' + end + rescue Gem::LoadError => load_error + $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) + exit 1 + end + + class << self + def rubygems_version + Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion + end + + def gem_version + if defined? RAILS_GEM_VERSION + RAILS_GEM_VERSION + elsif ENV.include?('RAILS_GEM_VERSION') + ENV['RAILS_GEM_VERSION'] + else + parse_gem_version(read_environment_rb) + end + end + + def load_rubygems + require 'rubygems' + + unless rubygems_version >= '0.9.4' + $stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.) + exit 1 + end + + rescue LoadError + $stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org) + exit 1 + end + + def parse_gem_version(text) + $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ + end + + private + def read_environment_rb + File.read("#{RAILS_ROOT}/config/environment.rb") + end + end + end end -Rails::Initializer.run(:set_load_path) +# All that for this: +Rails.boot! diff --git a/groups/config/database.yml.example b/groups/config/database.yml.example index f72844a07..1dc678131 100644 --- a/groups/config/database.yml.example +++ b/groups/config/database.yml.example @@ -12,6 +12,7 @@ production: host: localhost username: root password: + encoding: utf8 development: adapter: mysql @@ -19,6 +20,7 @@ development: host: localhost username: root password: + encoding: utf8 test: adapter: mysql @@ -26,6 +28,7 @@ test: host: localhost username: root password: + encoding: utf8 test_pgsql: adapter: postgresql diff --git a/groups/config/email.yml.example b/groups/config/email.yml.example new file mode 100644 index 000000000..685096da4 --- /dev/null +++ b/groups/config/email.yml.example @@ -0,0 +1,21 @@ +# Outgoing email settings + +production: + delivery_method: :smtp + smtp_settings: + address: smtp.example.net + port: 25 + domain: example.net + authentication: :login + user_name: redmine@example.net + password: redmine + +development: + delivery_method: :smtp + smtp_settings: + address: 127.0.0.1 + port: 25 + domain: example.net + authentication: :login + user_name: redmine@example.net + password: redmine diff --git a/groups/config/environment.rb b/groups/config/environment.rb index 7878eca47..9a3bf4b1d 100644 --- a/groups/config/environment.rb +++ b/groups/config/environment.rb @@ -5,7 +5,7 @@ # ENV['RAILS_ENV'] ||= 'production' # Specifies gem version of Rails to use when vendor/rails is not present -RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION +RAILS_GEM_VERSION = '2.1.0' unless defined? RAILS_GEM_VERSION # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') @@ -31,7 +31,7 @@ Rails::Initializer.run do |config| # config.log_level = :debug # Use the database for sessions instead of the file system - # (create the session table with 'rake create_sessions_table') + # (create the session table with 'rake db:sessions:create') # config.action_controller.session_store = :active_record_store config.action_controller.session_store = :PStore @@ -49,54 +49,9 @@ Rails::Initializer.run do |config| # Use Active Record's schema dumper instead of SQL when creating the test database # (enables use of different database adapters for development and test environments) # config.active_record.schema_format = :ruby - - # See Rails::Configuration for more options - # SMTP server configuration - config.action_mailer.smtp_settings = { - :address => "127.0.0.1", - :port => 25, - :domain => "somenet.foo", - :authentication => :login, - :user_name => "redmine@somenet.foo", - :password => "redmine", - } - - config.action_mailer.perform_deliveries = true - - # Tell ActionMailer not to deliver emails to the real world. - # The :test delivery method accumulates sent emails in the - # ActionMailer::Base.deliveries array. - #config.action_mailer.delivery_method = :test - config.action_mailer.delivery_method = :smtp - + # Deliveries are disabled by default. Do NOT modify this section. + # Define your email configuration in email.yml instead. + # It will automatically turn deliveries on + config.action_mailer.perform_deliveries = false end - -ActiveRecord::Errors.default_error_messages = { - :inclusion => "activerecord_error_inclusion", - :exclusion => "activerecord_error_exclusion", - :invalid => "activerecord_error_invalid", - :confirmation => "activerecord_error_confirmation", - :accepted => "activerecord_error_accepted", - :empty => "activerecord_error_empty", - :blank => "activerecord_error_blank", - :too_long => "activerecord_error_too_long", - :too_short => "activerecord_error_too_short", - :wrong_length => "activerecord_error_wrong_length", - :taken => "activerecord_error_taken", - :not_a_number => "activerecord_error_not_a_number" -} - -ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" } - -Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV) -Mime::Type.register 'application/pdf', :pdf - -GLoc.set_config :default_language => :en -GLoc.clear_strings -GLoc.set_kcode -GLoc.load_localized_strings -GLoc.set_config(:raise_string_not_found_errors => false) - -require 'redmine' - diff --git a/groups/config/environments/test.rb b/groups/config/environments/test.rb index 9ba9ae0f8..7c821da07 100644 --- a/groups/config/environments/test.rb +++ b/groups/config/environments/test.rb @@ -13,4 +13,5 @@ config.whiny_nils = true config.action_controller.consider_all_requests_local = true config.action_controller.perform_caching = false +config.action_mailer.perform_deliveries = true config.action_mailer.delivery_method = :test diff --git a/groups/config/environments/test_pgsql.rb b/groups/config/environments/test_pgsql.rb index 35bb19bee..7c821da07 100644 --- a/groups/config/environments/test_pgsql.rb +++ b/groups/config/environments/test_pgsql.rb @@ -13,4 +13,5 @@ config.whiny_nils = true 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 +config.action_mailer.perform_deliveries = true +config.action_mailer.delivery_method = :test diff --git a/groups/config/environments/test_sqlite3.rb b/groups/config/environments/test_sqlite3.rb index 35bb19bee..7c821da07 100644 --- a/groups/config/environments/test_sqlite3.rb +++ b/groups/config/environments/test_sqlite3.rb @@ -13,4 +13,5 @@ config.whiny_nils = true 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 +config.action_mailer.perform_deliveries = true +config.action_mailer.delivery_method = :test diff --git a/groups/config/initializers/10-patches.rb b/groups/config/initializers/10-patches.rb new file mode 100644 index 000000000..fcc091997 --- /dev/null +++ b/groups/config/initializers/10-patches.rb @@ -0,0 +1,17 @@ + +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}" } diff --git a/groups/config/initializers/20-mime_types.rb b/groups/config/initializers/20-mime_types.rb new file mode 100644 index 000000000..269742b16 --- /dev/null +++ b/groups/config/initializers/20-mime_types.rb @@ -0,0 +1,4 @@ +# Add new mime types for use in respond_to blocks: + +Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV) +Mime::Type.register 'application/pdf', :pdf diff --git a/groups/config/initializers/30-redmine.rb b/groups/config/initializers/30-redmine.rb new file mode 100644 index 000000000..f2a9f6a30 --- /dev/null +++ b/groups/config/initializers/30-redmine.rb @@ -0,0 +1,7 @@ +GLoc.set_config :default_language => :en +GLoc.clear_strings +GLoc.set_kcode +GLoc.load_localized_strings +GLoc.set_config(:raise_string_not_found_errors => false) + +require 'redmine' diff --git a/groups/config/initializers/40-email.rb b/groups/config/initializers/40-email.rb new file mode 100644 index 000000000..5b388ec59 --- /dev/null +++ b/groups/config/initializers/40-email.rb @@ -0,0 +1,17 @@ +# Loads action_mailer settings from email.yml +# and turns deliveries on if configuration file is found + +filename = File.join(File.dirname(__FILE__), '..', 'email.yml') +if File.file?(filename) + mailconfig = YAML::load_file(filename) + + if mailconfig.is_a?(Hash) && mailconfig.has_key?(Rails.env) + # Enable deliveries + ActionMailer::Base.perform_deliveries = true + + mailconfig[Rails.env].each do |k, v| + v.symbolize_keys! if v.respond_to?(:symbolize_keys!) + ActionMailer::Base.send("#{k}=", v) + end + end +end diff --git a/groups/config/routes.rb b/groups/config/routes.rb index bc4247837..4213df915 100644 --- a/groups/config/routes.rb +++ b/groups/config/routes.rb @@ -31,8 +31,13 @@ ActionController::Routing::Routes.draw do |map| omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff' omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry' omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate' + omap.repositories_revision 'repositories/revision/:id/:rev', :action => 'revision' end + map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/ + map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/ + map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/ + # Allow downloading Web Service WSDL as a file with an extension # instead of a file named 'wsdl' map.connect ':controller/service.wsdl', :action => 'wsdl' diff --git a/groups/config/settings.yml b/groups/config/settings.yml index bb501823e..ac79edb8d 100644 --- a/groups/config/settings.yml +++ b/groups/config/settings.yml @@ -43,7 +43,7 @@ activity_days_default: per_page_options: default: '25,50,100' mail_from: - default: redmine@somenet.foo + default: redmine@example.net bcc_recipients: default: 1 text_formatting: @@ -59,6 +59,15 @@ protocol: feeds_limit: format: int default: 15 +enabled_scm: + serialized: true + default: + - Subversion + - Darcs + - Mercurial + - Cvs + - Bazaar + - Git autofetch_changesets: default: 1 sys_api_enabled: @@ -92,6 +101,10 @@ notified_events: default: - issue_added - issue_updated +mail_handler_api_enabled: + default: 0 +mail_handler_api_key: + default: issue_list_default_columns: serialized: true default: @@ -109,6 +122,9 @@ default_projects_public: # multiple values accepted, comma separated repositories_encodings: default: '' +# encoding used to convert commit logs to UTF-8 +commit_logs_encoding: + default: 'UTF-8' ui_theme: default: '' emails_footer: diff --git a/groups/db/migrate/001_setup.rb b/groups/db/migrate/001_setup.rb index 1160dd5ef..d49e0e444 100644 --- a/groups/db/migrate/001_setup.rb +++ b/groups/db/migrate/001_setup.rb @@ -16,7 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Setup < ActiveRecord::Migration - + + class User < ActiveRecord::Base; end # model removed class Permission < ActiveRecord::Base; end @@ -284,13 +285,15 @@ class Setup < ActiveRecord::Migration 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
-
-
+ user = User.create :login => "admin", + :hashed_password => "d033e22ae348aeb5660fc2140aec35850c4da997", + :admin => true, + :firstname => "Redmine", + :lastname => "Admin", + :mail => "admin@example.net", + :mail_notification => true, + :language => "en", + :status => 1 end def self.down
diff --git a/groups/db/migrate/072_add_enumerations_position.rb b/groups/db/migrate/072_add_enumerations_position.rb index e0beaf395..22558a6e9 100644 --- a/groups/db/migrate/072_add_enumerations_position.rb +++ b/groups/db/migrate/072_add_enumerations_position.rb @@ -1,7 +1,7 @@ class AddEnumerationsPosition < ActiveRecord::Migration def self.up add_column(:enumerations, :position, :integer, :default => 1) unless Enumeration.column_names.include?('position') - Enumeration.find(:all).group_by(&:opt).each_value do |enums| + Enumeration.find(:all).group_by(&:opt).each do |opt, enums| enums.each_with_index do |enum, i| # do not call model callbacks Enumeration.update_all "position = #{i+1}", {:id => enum.id} diff --git a/groups/db/migrate/078_add_custom_fields_position.rb b/groups/db/migrate/078_add_custom_fields_position.rb index 7ee8abb58..1c42ae732 100644 --- a/groups/db/migrate/078_add_custom_fields_position.rb +++ b/groups/db/migrate/078_add_custom_fields_position.rb @@ -1,7 +1,7 @@ class AddCustomFieldsPosition < ActiveRecord::Migration def self.up add_column(:custom_fields, :position, :integer, :default => 1) - CustomField.find(:all).group_by(&:type).each_value do |fields| + CustomField.find(:all).group_by(&:type).each do |t, fields| fields.each_with_index do |field, i| # do not call model callbacks CustomField.update_all "position = #{i+1}", {:id => field.id} diff --git a/groups/db/migrate/096_add_wiki_pages_protected.rb b/groups/db/migrate/096_add_wiki_pages_protected.rb new file mode 100644 index 000000000..49720fbb7 --- /dev/null +++ b/groups/db/migrate/096_add_wiki_pages_protected.rb @@ -0,0 +1,9 @@ +class AddWikiPagesProtected < ActiveRecord::Migration + def self.up + add_column :wiki_pages, :protected, :boolean, :default => false, :null => false + end + + def self.down + remove_column :wiki_pages, :protected + end +end diff --git a/groups/db/migrate/097_change_projects_homepage_limit.rb b/groups/db/migrate/097_change_projects_homepage_limit.rb new file mode 100644 index 000000000..98374aa4e --- /dev/null +++ b/groups/db/migrate/097_change_projects_homepage_limit.rb @@ -0,0 +1,9 @@ +class ChangeProjectsHomepageLimit < ActiveRecord::Migration + def self.up + change_column :projects, :homepage, :string, :limit => nil, :default => '' + end + + def self.down + change_column :projects, :homepage, :string, :limit => 60, :default => '' + end +end diff --git a/groups/db/migrate/098_add_wiki_pages_parent_id.rb b/groups/db/migrate/098_add_wiki_pages_parent_id.rb new file mode 100644 index 000000000..36b922ec1 --- /dev/null +++ b/groups/db/migrate/098_add_wiki_pages_parent_id.rb @@ -0,0 +1,9 @@ +class AddWikiPagesParentId < ActiveRecord::Migration + def self.up + add_column :wiki_pages, :parent_id, :integer, :default => nil + end + + def self.down + remove_column :wiki_pages, :parent_id + end +end diff --git a/groups/doc/CHANGELOG b/groups/doc/CHANGELOG index b39185151..ac8cb6673 100644 --- a/groups/doc/CHANGELOG +++ b/groups/doc/CHANGELOG @@ -5,6 +5,86 @@ Copyright (C) 2006-2008 Jean-Philippe Lang http://www.redmine.org/ +== 2008-07-06 v0.7.3 + +* Allow dot in firstnames and lastnames +* Add project name to cross-project Atom feeds +* Encoding set to utf8 in example database.yml +* HTML titles on forums related views +* Fixed: various XSS vulnerabilities +* Fixed: Entourage (and some old client) fails to correctly render notification styles +* Fixed: Fixed: timelog redirects inappropriately when :back_url is blank +* Fixed: wrong relative paths to images in wiki_syntax.html + + +== 2008-06-15 v0.7.2 + +* "New Project" link on Projects page +* Links to repository directories on the repo browser +* Move status to front in Activity View +* Remove edit step from Status context menu +* Fixed: No way to do textile horizontal rule +* Fixed: Repository: View differences doesn't work +* Fixed: attachement's name maybe invalid. +* Fixed: Error when creating a new issue +* Fixed: NoMethodError on @available_filters.has_key? +* Fixed: Check All / Uncheck All in Email Settings +* Fixed: "View differences" of one file at /repositories/revision/ fails +* Fixed: Column width in "my page" +* Fixed: private subprojects are listed on Issues view +* Fixed: Textile: bold, italics, underline, etc... not working after parentheses +* Fixed: Update issue form: comment field from log time end out of screen +* Fixed: Editing role: "issue can be assigned to this role" out of box +* Fixed: Unable use angular braces after include word +* Fixed: Using '*' as keyword for repository referencing keywords doesn't work +* Fixed: Subversion repository "View differences" on each file rise ERROR +* Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root +* Fixed: It is possible to lock out the last admin account +* Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access +* Fixed: Issue number display clipped on 'my issues' +* Fixed: Roadmap version list links not carrying state +* Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default +* Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master" +* Fixed: browser's language subcodes ignored +* Fixed: Error on project selection with numeric (only) identifier. +* Fixed: Link to PDF doesn't work after creating new issue +* Fixed: "Replies" should not be shown on forum threads that are locked +* Fixed: SVN errors lead to svn username/password being displayed to end users (security issue) +* Fixed: http links containing hashes don't display correct +* Fixed: Allow ampersands in Enumeration names +* Fixed: Atom link on saved query does not include query_id +* Fixed: Logtime info lost when there's an error updating an issue +* Fixed: TOC does not parse colorization markups +* Fixed: CVS: add support for modules names with spaces +* Fixed: Bad rendering on projects/add +* Fixed: exception when viewing differences on cvs +* Fixed: export issue to pdf will messup when use Chinese language +* Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant +* Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE +* Fixed: Importing from trac : some wiki links are messed +* Fixed: Incorrect weekend definition in Hebrew calendar locale +* Fixed: Atom feeds don't provide author section for repository revisions +* Fixed: In Activity views, changesets titles can be multiline while they should not +* Fixed: Ignore unreadable subversion directories (read disabled using authz) +* Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets +* Fixed: Close statement handler in Redmine.pm + + +== 2008-05-04 v0.7.1 + +* Thai translation added (Gampol Thitinilnithi) +* Translations updates +* Escape HTML comment tags +* Prevent "can't convert nil into String" error when :sort_order param is not present +* Fixed: Updating tickets add a time log with zero hours +* Fixed: private subprojects names are revealed on the project overview +* Fixed: Search for target version of "none" fails with postgres 8.3 +* Fixed: Home, Logout, Login links shouldn't be absolute links +* Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects +* Fixed: error when using upcase language name in coderay +* Fixed: error on Trac import when :due attribute is nil + + == 2008-04-28 v0.7.0 * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present diff --git a/groups/doc/INSTALL b/groups/doc/INSTALL index 7a00b9367..13502f8d0 100644 --- a/groups/doc/INSTALL +++ b/groups/doc/INSTALL @@ -1,25 +1,22 @@ == Redmine installation Redmine - project management software -Copyright (C) 2006-2007 Jean-Philippe Lang +Copyright (C) 2006-2008 Jean-Philippe Lang http://www.redmine.org/ == Requirements -* Ruby on Rails 2.0.2 -* A database (see compatibility below) +* Ruby on Rails 2.1 +* A database: + * MySQL (tested with MySQL 5) + * PostgreSQL (tested with PostgreSQL 8.1) + * SQLite (tested with SQLite 3) Optional: * SVN binaries >= 1.3 (needed for repository browsing, must be available in PATH) * RMagick (gantt export to png) -Supported databases: -* MySQL (tested with MySQL 5) -* PostgreSQL (tested with PostgreSQL 8.1) -* SQLite (tested with SQLite 3) - - == Installation 1. Uncompress the program archive @@ -33,24 +30,33 @@ Supported databases: rake db:migrate RAILS_ENV="production" It will create tables and an administrator account. -5. Test the installation by running WEBrick web server: +5. Setting up permissions + The user who runs Redmine must have write permission on the following + subdirectories: files, log, tmp (create the last one if not present). + + Assuming you run Redmine with a user named redmine: + mkdir tmp + sudo chown -R redmine:redmine files log tmp + sudo chmod -R 755 files log tmp + +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 -6. Use default administrator account to log in: +7. Use default administrator account to log in: login: admin password: admin -7. Go to "Administration" to load the default configuration data (roles, + Go to "Administration" to load the default configuration data (roles, trackers, statuses, workflow) and adjust application settings -== SMTP server Configuration - -In config/environment.rb, you can set parameters for your SMTP server: -config.action_mailer.smtp_settings: SMTP server configuration -config.action_mailer.perform_deliveries: set to false to disable mail delivering +== Email delivery Configuration +Copy config/email.yml.example to config/email.yml and edit this file +to adjust your SMTP settings. Don't forget to restart the application after any change to this file. + +Please do not enter your SMTP settings in environment.rb. diff --git a/groups/doc/README_FOR_APP b/groups/doc/README_FOR_APP new file mode 100644 index 000000000..fb70acaac --- /dev/null +++ b/groups/doc/README_FOR_APP @@ -0,0 +1,5 @@ += Redmine + +Redmine is a flexible project management web application written using Ruby on Rails framework. + +More details can be found at http://www.redmine.org diff --git a/groups/doc/RUNNING_TESTS b/groups/doc/RUNNING_TESTS index 7a5e2b992..6ee977811 100644 --- a/groups/doc/RUNNING_TESTS +++ b/groups/doc/RUNNING_TESTS @@ -24,6 +24,14 @@ Git --- gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test +Darcs (2.0+ required) +--------------------- +gunzip < test/fixtures/repositories/darcs_repository.tar.gz | tar -xv -C tmp/test + +FileSystem +---------- +gunzip < test/fixtures/repositories/filesystem_repository.tar.gz | tar -xv -C tmp/test + Running Tests ============= diff --git a/groups/doc/UPGRADING b/groups/doc/UPGRADING index 2edb2952a..1dd901171 100644 --- a/groups/doc/UPGRADING +++ b/groups/doc/UPGRADING @@ -10,15 +10,13 @@ http://www.redmine.org/ 1. Uncompress the program archive in a new directory 3. Copy your database settings (RAILS_ROOT/config/database.yml) + and SMTP settings (RAILS_ROOT/config/email.yml) into the new config directory -4. Enter your SMTP settings in config/environment.rb - Do not replace this file with the old one - -5. Migrate your database (please make a backup before doing this): +4. Migrate your database (please make a backup before doing this): rake db:migrate RAILS_ENV="production" -6. Copy the RAILS_ROOT/files directory content into your new installation +5. Copy the RAILS_ROOT/files directory content into your new installation This directory contains all the attached files diff --git a/groups/extra/mail_handler/rdm-mailhandler.rb b/groups/extra/mail_handler/rdm-mailhandler.rb new file mode 100644 index 000000000..96e975187 --- /dev/null +++ b/groups/extra/mail_handler/rdm-mailhandler.rb @@ -0,0 +1,125 @@ +#!/usr/bin/ruby + +# rdm-mailhandler +# Reads an email from standard input and forward it to a Redmine server +# Can be used from a remote mail server + +require 'net/http' +require 'net/https' +require 'uri' +require 'getoptlong' + +module Net + class HTTPS < HTTP + def self.post_form(url, params) + request = Post.new(url.path) + request.form_data = params + request.basic_auth url.user, url.password if url.user + http = new(url.host, url.port) + http.use_ssl = (url.scheme == 'https') + http.start {|h| h.request(request) } + end + end +end + +class RedmineMailHandler + VERSION = '0.1' + + attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key + + def initialize + self.issue_attributes = {} + + opts = GetoptLong.new( + [ '--help', '-h', GetoptLong::NO_ARGUMENT ], + [ '--version', '-V', GetoptLong::NO_ARGUMENT ], + [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], + [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ], + [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT], + [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ], + [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT], + [ '--category', GetoptLong::REQUIRED_ARGUMENT], + [ '--priority', GetoptLong::REQUIRED_ARGUMENT], + [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT] + ) + + opts.each do |opt, arg| + case opt + when '--url' + self.url = arg.dup + when '--key' + self.key = arg.dup + when '--help' + usage + when '--verbose' + self.verbose = true + when '--version' + puts VERSION; exit + when '--project', '--tracker', '--category', '--priority' + self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup + when '--allow-override' + self.allow_override = arg.dup + end + end + + usage if url.nil? + end + + def submit(email) + uri = url.gsub(%r{/*$}, '') + '/mail_handler' + + data = { 'key' => key, 'email' => email, 'allow_override' => allow_override } + issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } + + debug "Posting to #{uri}..." + response = Net::HTTPS.post_form(URI.parse(uri), data) + debug "Response received: #{response.code}" + response.code == 201 ? 0 : 1 + end + + private + + def usage + puts <<-USAGE +Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key> +Reads an email from standard input and forward it to a Redmine server + +Required: + -u, --url URL of the Redmine server + -k, --key Redmine API key + +General options: + -h, --help show this help + -v, --verbose show extra information + -V, --version show version information and exit + +Issue attributes control options: + -p, --project=PROJECT identifier of the target project + -t, --tracker=TRACKER name of the target tracker + --category=CATEGORY name of the target category + --priority=PRIORITY name of the target priority + -o, --allow-override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + rdm-mailhandler --url http://redmine.domain.foo --key secret + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + rdm-mailhandler --url https://domain.foo/redmine --key secret \\ + --project foo \\ + --tracker bug \\ + --allow-override tracker,priority +USAGE + exit + end + + def debug(msg) + puts msg if verbose + end +end + +handler = RedmineMailHandler.new +handler.submit(STDIN.read) diff --git a/groups/extra/sample_plugin/app/models/meeting.rb b/groups/extra/sample_plugin/app/models/meeting.rb new file mode 100644 index 000000000..c1bb64a93 --- /dev/null +++ b/groups/extra/sample_plugin/app/models/meeting.rb @@ -0,0 +1,11 @@ +class Meeting < ActiveRecord::Base + belongs_to :project + + acts_as_event :title => Proc.new {|o| "#{o.scheduled_on} Meeting"}, + :datetime => :scheduled_on, + :author => nil, + :url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}} + + acts_as_activity_provider :timestamp => 'scheduled_on', + :find_options => { :include => :project } +end diff --git a/groups/extra/sample_plugin/db/migrate/001_create_meetings.rb b/groups/extra/sample_plugin/db/migrate/001_create_meetings.rb new file mode 100644 index 000000000..fec9c8bd1 --- /dev/null +++ b/groups/extra/sample_plugin/db/migrate/001_create_meetings.rb @@ -0,0 +1,15 @@ +# Sample plugin migration +# Use rake db:migrate_plugins to migrate installed plugins +class CreateMeetings < ActiveRecord::Migration + def self.up + create_table :meetings do |t| + t.column :project_id, :integer, :null => false + t.column :description, :string + t.column :scheduled_on, :datetime + end + end + + def self.down + drop_table :meetings + end +end diff --git a/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb b/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb deleted file mode 100644 index 39d58a649..000000000 --- a/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Sample plugin migration -# Use rake db:migrate_plugins to migrate installed plugins -class CreateSomeModels < ActiveRecord::Migration - def self.up - create_table :example_plugin_model, :force => true do |t| - t.column "example_attribute", :integer - end - end - - def self.down - drop_table :example_plugin_model - end -end diff --git a/groups/extra/sample_plugin/init.rb b/groups/extra/sample_plugin/init.rb index 7389aaa6f..5c543338c 100644 --- a/groups/extra/sample_plugin/init.rb +++ b/groups/extra/sample_plugin/init.rb @@ -18,8 +18,13 @@ Redmine::Plugin.register :sample_plugin do # This permission has to be explicitly given # It will be listed on the permissions screen permission :example_say_goodbye, {:example => [:say_goodbye]} + # This permission can be given to project members only + permission :view_meetings, {:meetings => [:index, :show]}, :require => :member end # A new item is added to the project menu menu :project_menu, :sample_plugin, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample' + + # Meetings are added to the activity view + activity_provider :meetings end diff --git a/groups/extra/sample_plugin/lang/en.yml b/groups/extra/sample_plugin/lang/en.yml index bf62bc344..c4005a764 100644 --- a/groups/extra/sample_plugin/lang/en.yml +++ b/groups/extra/sample_plugin/lang/en.yml @@ -1,4 +1,5 @@ # Sample plugin label_plugin_example: Sample Plugin +label_meeting_plural: Meetings text_say_hello: Plugin say 'Hello' text_say_goodbye: Plugin say 'Good bye' diff --git a/groups/extra/sample_plugin/lang/fr.yml b/groups/extra/sample_plugin/lang/fr.yml index 2c0829c32..135050a5a 100644 --- a/groups/extra/sample_plugin/lang/fr.yml +++ b/groups/extra/sample_plugin/lang/fr.yml @@ -1,4 +1,5 @@ # Sample plugin label_plugin_example: Plugin exemple +label_meeting_plural: Meetings text_say_hello: Plugin dit 'Bonjour' text_say_goodbye: Plugin dit 'Au revoir' diff --git a/groups/extra/svn/Redmine.pm b/groups/extra/svn/Redmine.pm index 6f3ba4385..09a85fb09 100644 --- a/groups/extra/svn/Redmine.pm +++ b/groups/extra/svn/Redmine.pm @@ -36,10 +36,9 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used): =head1 CONFIGURATION - ## if the module isn't in your perl path - PerlRequire /usr/local/apache/Redmine.pm - ## else - # PerlModule Apache::Authn::Redmine + ## This module has to be in your perl path + ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm + PerlLoadModule Apache::Authn::Redmine <Location /svn> DAV svn SVNParentPath "/var/svn" @@ -52,12 +51,17 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used): PerlAuthenHandler Apache::Authn::Redmine::authen_handler ## for mysql - PerlSetVar dsn DBI:mysql:database=databasename;host=my.db.server + RedmineDSN "DBI:mysql:database=databasename;host=my.db.server" ## for postgres - # PerlSetVar dsn DBI:Pg:dbname=databasename;host=my.db.server - - PerlSetVar db_user redmine - PerlSetVar db_pass password + # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server" + + RedmineDbUser "redmine" + RedmineDbPass "password" + ## Optional where clause (fulltext search would be slow and + ## database dependant). + # RedmineDbWhereClause "and members.role_id IN (1,2)" + ## Optional credentials cache size + # RedmineCacheCredsMax 50 </Location> To be able to browse repository inside redmine, you must add something @@ -92,6 +96,7 @@ And you need to upgrade at least reposman.rb (after r860). =cut use strict; +use warnings FATAL => 'all', NONFATAL => 'redefine'; use DBI; use Digest::SHA1; @@ -103,9 +108,87 @@ use Apache2::Access; use Apache2::ServerRec qw(); use Apache2::RequestRec qw(); use Apache2::RequestUtil qw(); -use Apache2::Const qw(:common); +use Apache2::Const qw(:common :override :cmd_how); +use APR::Pool (); +use APR::Table (); + # use Apache2::Directive qw(); +my @directives = ( + { + name => 'RedmineDSN', + req_override => OR_AUTHCFG, + args_how => TAKE1, + errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"', + }, + { + name => 'RedmineDbUser', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'RedmineDbPass', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'RedmineDbWhereClause', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'RedmineCacheCredsMax', + req_override => OR_AUTHCFG, + args_how => TAKE1, + errmsg => 'RedmineCacheCredsMax must be decimal number', + }, +); + +sub RedmineDSN { + my ($self, $parms, $arg) = @_; + $self->{RedmineDSN} = $arg; + my $query = "SELECT + hashed_password, auth_source_id + FROM members, projects, users + WHERE + projects.id=members.project_id + AND users.id=members.user_id + AND users.status=1 + AND login=? + AND identifier=? "; + $self->{RedmineQuery} = trim($query); +} +sub RedmineDbUser { set_val('RedmineDbUser', @_); } +sub RedmineDbPass { set_val('RedmineDbPass', @_); } +sub RedmineDbWhereClause { + my ($self, $parms, $arg) = @_; + $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." "); +} + +sub RedmineCacheCredsMax { + my ($self, $parms, $arg) = @_; + if ($arg) { + $self->{RedmineCachePool} = APR::Pool->new; + $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg); + $self->{RedmineCacheCredsCount} = 0; + $self->{RedmineCacheCredsMax} = $arg; + } +} + +sub trim { + my $string = shift; + $string =~ s/\s{2,}/ /g; + return $string; +} + +sub set_val { + my ($key, $self, $parms, $arg) = @_; + $self->{$key} = $arg; +} + +Apache2::Module::add(__PACKAGE__, \@directives); + + my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; sub access_handler { @@ -117,7 +200,7 @@ sub access_handler { } my $method = $r->method; - return OK unless 1 == $read_only_methods{$method}; + return OK if defined $read_only_methods{$method}; my $project_id = get_project_identifier($r); @@ -152,6 +235,7 @@ sub is_public_project { $sth->execute($project_id); my $ret = $sth->fetchrow_array ? 1 : 0; + $sth->finish(); $dbh->disconnect(); $ret; @@ -182,9 +266,14 @@ sub is_member { my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); - my $sth = $dbh->prepare( - "SELECT hashed_password, auth_source_id FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;" - ); + my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); + my $usrprojpass; + if ($cfg->{RedmineCacheCredsMax}) { + $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id); + return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest)); + } + my $query = $cfg->{RedmineQuery}; + my $sth = $dbh->prepare($query); $sth->execute($redmine_user, $project_id); my $ret; @@ -216,6 +305,20 @@ sub is_member { $sth->finish(); $dbh->disconnect(); + if ($cfg->{RedmineCacheCredsMax} and $ret) { + if (defined $usrprojpass) { + $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest); + } else { + if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) { + $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest); + $cfg->{RedmineCacheCredsCount}++; + } else { + $cfg->{RedmineCacheCreds}->clear(); + $cfg->{RedmineCacheCredsCount} = 0; + } + } + } + $ret; } @@ -229,9 +332,9 @@ sub get_project_identifier { sub connect_database { my $r = shift; - - my ($dsn, $db_user, $db_pass) = map { $r->dir_config($_) } qw/dsn db_user db_pass/; - return DBI->connect($dsn, $db_user, $db_pass); + + my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config); + return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass}); } 1; diff --git a/groups/lang/bg.yml b/groups/lang/bg.yml index b341d989f..1f174e29f 100644 --- a/groups/lang/bg.yml +++ b/groups/lang/bg.yml @@ -48,6 +48,7 @@ general_text_no: 'не' general_text_yes: 'да' general_lang_name: 'Bulgarian' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: UTF-8 general_pdf_encoding: UTF-8 general_day_names: Понеделник,Вторник,СрÑда,Четвъртък,Петък,Събота,ÐÐµÐ´ÐµÐ»Ñ @@ -618,3 +619,20 @@ setting_default_projects_public: Ðовите проекти Ñа публичн error_scm_annotate: "Обектът не ÑъщеÑтвува или не може да бъде анотиран." label_planning: Планиране text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/cs.yml b/groups/lang/cs.yml index 250c602c2..609e95478 100644 --- a/groups/lang/cs.yml +++ b/groups/lang/cs.yml @@ -51,6 +51,7 @@ general_text_no: 'ne' general_text_yes: 'ano' general_lang_name: 'ÄŒeÅ¡tina' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: UTF-8 general_pdf_encoding: UTF-8 general_day_names: PondÄ›lÃ,Úterý,StÅ™eda,ÄŒtvrtek,Pátek,Sobota,NedÄ›le @@ -623,3 +624,20 @@ enumeration_activities: Aktivity (sledovánà Äasu) error_scm_annotate: "Položka neexistuje nebo nemůže být komentována." label_planning: Plánovánà text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/da.yml b/groups/lang/da.yml index ff2ed982d..a76e7ea5c 100644 --- a/groups/lang/da.yml +++ b/groups/lang/da.yml @@ -48,6 +48,7 @@ general_text_no: 'nej' general_text_yes: 'ja' general_lang_name: 'Danish (Dansk)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Mandag,Tirsdag,Onsdag,Torsdag,Fredag,Lørdag,Søndag @@ -620,3 +621,20 @@ setting_default_projects_public: Nye projekter er offentlige som default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planlægning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/de.yml b/groups/lang/de.yml index 77184cf88..e309dfb57 100644 --- a/groups/lang/de.yml +++ b/groups/lang/de.yml @@ -48,6 +48,7 @@ general_text_no: 'nein' general_text_yes: 'ja' general_lang_name: 'Deutsch' general_csv_separator: ';' +general_csv_decimal_separator: ',' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag,Sonntag @@ -619,3 +620,20 @@ enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/en.yml b/groups/lang/en.yml index 320501c2b..7763b44b5 100644 --- a/groups/lang/en.yml +++ b/groups/lang/en.yml @@ -48,6 +48,7 @@ general_text_no: 'no' general_text_yes: 'yes' general_lang_name: 'English' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday @@ -91,6 +92,8 @@ mail_body_account_information_external: You can use your "%s" account to log in. mail_body_account_information: Your account information mail_subject_account_activation_request: %s account activation request mail_body_account_activation_request: 'A new user (%s) has registered. His account is pending your approval:' +mail_subject_reminder: "%d issue(s) due in the next days" +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" gui_validation_error: 1 error gui_validation_error_plural: %d errors @@ -180,6 +183,7 @@ field_searchable: Searchable field_default_value: Default value field_comments_sorting: Display comments field_group: Group +field_parent_title: Parent page setting_app_title: Application title setting_app_subtitle: Application subtitle @@ -206,12 +210,16 @@ setting_time_format: Time format setting_cross_project_issue_relations: Allow cross-project issue relations setting_issue_list_default_columns: Default columns displayed on the issue list setting_repositories_encodings: Repositories encodings +setting_commit_logs_encoding: Commit messages encoding setting_emails_footer: Emails footer setting_protocol: Protocol setting_per_page_options: Objects per page options setting_user_format: Users display format setting_activity_days_default: Days displayed on project activity setting_display_subprojects_issues: Display subprojects issues on main projects by default +setting_enabled_scm: Enabled SCM +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key project_module_issue_tracking: Issue tracking project_module_time_tracking: Time tracking @@ -292,6 +300,7 @@ label_auth_source: Authentication mode label_auth_source_new: New authentication mode label_auth_source_plural: Authentication modes label_subproject_plural: Subprojects +label_and_its_subprojects: %s and its subprojects label_min_max_length: Min - Max length label_list: List label_date: Date @@ -446,6 +455,7 @@ label_relation_new: New relation label_relation_delete: Delete relation label_relates_to: related to label_duplicates: duplicates +label_duplicated_by: duplicated by label_blocks: blocks label_blocked_by: blocked by label_precedes: precedes @@ -514,6 +524,9 @@ label_planning: Planning label_group: Group label_group_plural: Groups label_group_new: New group +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +label_issue_watchers: Watchers button_login: Login button_submit: Submit @@ -552,6 +565,7 @@ button_copy: Copy button_annotate: Annotate button_update: Update button_configure: Configure +button_quote: Quote status_active: active status_registered: registered @@ -570,7 +584,7 @@ text_journal_deleted: deleted text_tip_task_begin_day: task beginning this day text_tip_task_end_day: task ending this day text_tip_task_begin_end_day: task beginning and ending this day -text_project_identifier_info: 'Lower case letters (a-z), numbers and dashes allowed.<br />Once saved, the identifier can not be changed.' +text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.' text_caracters_maximum: %d characters maximum. text_caracters_minimum: Must be at least %d characters long. text_length_between: Length between %d and %d characters. @@ -597,6 +611,10 @@ text_destroy_time_entries_question: %.02f hours were reported on the issues you text_destroy_time_entries: Delete reported hours text_assign_time_entries_to_project: Assign reported hours to the project text_reassign_time_entries: 'Reassign reported hours to this issue:' +text_user_wrote: '%s wrote:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." default_role_manager: Manager default_role_developper: Developer diff --git a/groups/lang/es.yml b/groups/lang/es.yml index c6eef021a..fc9540a02 100644 --- a/groups/lang/es.yml +++ b/groups/lang/es.yml @@ -48,6 +48,7 @@ general_text_no: 'no' general_text_yes: 'sÃ' general_lang_name: 'Español' general_csv_separator: ';' +general_csv_decimal_separator: ',' general_csv_encoding: ISO-8859-15 general_pdf_encoding: ISO-8859-15 general_day_names: Lunes,Martes,Miércoles,Jueves,Viernes,Sábado,Domingo @@ -621,3 +622,20 @@ setting_default_projects_public: Los proyectos nuevos son públicos por defecto error_scm_annotate: "No existe la entrada o no ha podido ser anotada" label_planning: Planificación text_subprojects_destroy_warning: 'Sus subprojectos: %s también se eliminarán' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/fi.yml b/groups/lang/fi.yml index 68b6c20d7..6eb16bfac 100644 --- a/groups/lang/fi.yml +++ b/groups/lang/fi.yml @@ -16,7 +16,7 @@ actionview_datehelper_time_in_words_minute_less_than: vähemmän kuin minuuttia actionview_datehelper_time_in_words_minute_plural: %d minuuttia actionview_datehelper_time_in_words_minute_single: 1 minuutti actionview_datehelper_time_in_words_second_less_than: vähemmän kuin sekuntin -actionview_datehelper_time_in_words_second_less_than_plural: vähemmän kuin %d sekunttia +actionview_datehelper_time_in_words_second_less_than_plural: vähemmän kuin %d sekuntia actionview_instancetag_blank_option: Valitse, ole hyvä activerecord_error_inclusion: ei ole listalla @@ -34,7 +34,7 @@ activerecord_error_not_a_number: ei ole numero activerecord_error_not_a_date: ei ole oikea päivä activerecord_error_greater_than_start_date: tulee olla aloituspäivän jälkeinen activerecord_error_not_same_project: ei kuulu samaan projektiin -activerecord_error_circular_dependency: Tämä suhde loisi kiertävän suhteen. +activerecord_error_circular_dependency: Tämä suhde loisi kehän. general_fmt_age: %d v. general_fmt_age_plural: %d vuotta @@ -48,19 +48,20 @@ general_text_no: 'ei' general_text_yes: 'kyllä' general_lang_name: 'Finnish (Suomi)' general_csv_separator: ',' -general_csv_encoding: ISO-8859-1 -general_pdf_encoding: ISO-8859-1 +general_csv_decimal_separator: '.' +general_csv_encoding: ISO-8859-15 +general_pdf_encoding: ISO-8859-15 general_day_names: Maanantai,Tiistai,Keskiviikko,Torstai,Perjantai,Lauantai,Sunnuntai general_first_day_of_week: '1' notice_account_updated: Tilin päivitys onnistui. -notice_account_invalid_creditentials: Väärä käyttäjä tai salasana +notice_account_invalid_creditentials: Virheellinen käyttäjätunnus tai salasana notice_account_password_updated: Salasanan päivitys onnistui. notice_account_wrong_password: Väärä salasana notice_account_register_done: Tilin luonti onnistui. Aktivoidaksesi tilin seuraa linkkiä joka välitettiin sähköpostiisi. notice_account_unknown_email: Tuntematon käyttäjä. -notice_can_t_change_password: Tämä tili käyttää ulkoista autentikointi järjestelmää. Mahdotonta muuttaa salasanaa. -notice_account_lost_email_sent: Sinulle on lähetetty sähköposti jossa on ohje miten vaihdat salasanasi. +notice_can_t_change_password: Tämä tili käyttää ulkoista tunnistautumisjärjestelmää. Salasanaa ei voi muuttaa. +notice_account_lost_email_sent: Sinulle on lähetetty sähköposti jossa on ohje kuinka vaihdat salasanasi. notice_account_activated: Tilisi on nyt aktivoitu, voit kirjautua sisälle. notice_successful_create: Luonti onnistui. notice_successful_update: Päivitys onnistui. @@ -71,20 +72,20 @@ notice_locking_conflict: Toinen käyttäjä on päivittänyt tiedot. notice_not_authorized: Sinulla ei ole oikeutta näyttää tätä sivua. notice_email_sent: Sähköposti on lähetty osoitteeseen %s notice_email_error: Sähköpostilähetyksessä tapahtui virhe (%s) -notice_feeds_access_key_reseted: RSS pääsy avaimesi on nollaantunut. +notice_feeds_access_key_reseted: RSS salasana on nollaantunut. notice_failed_to_save_issues: "%d Tapahtum(an/ien) tallennus epäonnistui %d valitut: %s." notice_no_issue_selected: "Tapahtumia ei ole valittu! Valitse tapahtumat joita haluat muokata." notice_account_pending: "Tilisi on luotu ja odottaa ylläpitäjän hyväksyntää." -notice_default_data_loaded: Vakio asetusten palautus onnistui. +notice_default_data_loaded: Vakioasetusten palautus onnistui. -error_can_t_load_default_data: "Vakio asetuksia ei voitu ladata: %s" -error_scm_not_found: "Syötettä ja/tai versiota ei löydy säiliöstä." -error_scm_command_failed: "Säiliöön pääsyssä tapahtui virhe: %s" +error_can_t_load_default_data: "Vakioasetuksia ei voitu ladata: %s" +error_scm_not_found: "Syötettä ja/tai versiota ei löydy tietovarastosta." +error_scm_command_failed: "Tietovarastoon pääsyssä tapahtui virhe: %s" mail_subject_lost_password: Sinun %s salasanasi -mail_body_lost_password: 'Vaihtaaksesi salasanasi, paina seuraavaa linkkiä:' +mail_body_lost_password: 'Vaihtaaksesi salasanasi, napsauta seuraavaa linkkiä:' mail_subject_register: %s tilin aktivointi -mail_body_register: 'Aktivoidaksesi tilisi, paina seuraavaa linkkiä:' +mail_body_register: 'Aktivoidaksesi tilisi, napsauta seuraavaa linkkiä:' mail_body_account_information_external: Voit nyt käyttää "%s" tiliäsi kirjautuaksesi järjestelmään. mail_body_account_information: Sinun tilin tiedot mail_subject_account_activation_request: %s tilin aktivointi pyyntö @@ -97,8 +98,8 @@ field_name: Nimi field_description: Kuvaus field_summary: Yhteenveto field_is_required: Vaaditaan -field_firstname: Etu nimi -field_lastname: Suku nimi +field_firstname: Etunimi +field_lastname: Sukunimi field_mail: Sähköposti field_filename: Tiedosto field_filesize: Koko @@ -109,9 +110,9 @@ field_updated_on: Päivitetty field_field_format: Muoto field_is_for_all: Kaikille projekteille field_possible_values: Mahdolliset arvot -field_regexp: Säännönmukainen ilmentymä (reg exp) -field_min_length: Minimi pituus -field_max_length: Maksimi pituus +field_regexp: Säännöllinen lauseke (reg exp) +field_min_length: Minimipituus +field_max_length: Maksimipituus field_value: Arvo field_category: Luokka field_title: Otsikko @@ -120,18 +121,18 @@ field_issue: Tapahtuma field_status: Tila field_notes: Muistiinpanot field_is_closed: Tapahtuma suljettu -field_is_default: Vakio arvo +field_is_default: Vakioarvo field_tracker: Tapahtuma field_subject: Aihe field_due_date: Määräaika field_assigned_to: Nimetty field_priority: Prioriteetti -field_fixed_version: Kohde versio +field_fixed_version: Kohdeversio field_user: Käyttäjä field_role: Rooli field_homepage: Kotisivu field_is_public: Julkinen -field_parent: Alaprojekti +field_parent: Aliprojekti field_is_in_chlog: Tapahtumat näytetään muutoslokissa field_is_in_roadmap: Tapahtumat näytetään roadmap näkymässä field_login: Kirjautuminen @@ -145,23 +146,23 @@ field_new_password: Uusi salasana field_password_confirmation: Vahvistus field_version: Versio field_type: Tyyppi -field_host: Isäntä +field_host: Verkko-osoite field_port: Portti field_account: Tili field_base_dn: Base DN -field_attr_login: Kirjautumis määre -field_attr_firstname: Etuminen määre -field_attr_lastname: Sukunimen määre -field_attr_mail: Sähköpostin määre +field_attr_login: Kirjautumismääre +field_attr_firstname: Etuminenmääre +field_attr_lastname: Sukunimenmääre +field_attr_mail: Sähköpostinmääre field_onthefly: Automaattinen käyttäjien luonti field_start_date: Alku field_done_ratio: %% Tehty -field_auth_source: Autentikointi muoto +field_auth_source: Varmennusmuoto field_hide_mail: Piiloita sähköpostiosoitteeni field_comments: Kommentti field_url: URL -field_start_page: Aloitus sivu -field_subproject: Alaprojekti +field_start_page: Aloitussivu +field_subproject: Aliprojekti field_hours: Tuntia field_activity: Historia field_spent_on: Päivä @@ -175,32 +176,32 @@ field_estimated_hours: Arvioitu aika field_column_names: Saraketta field_time_zone: Aikavyöhyke field_searchable: Haettava -field_default_value: Vakio arvo +field_default_value: Vakioarvo setting_app_title: Ohjelman otsikko setting_app_subtitle: Ohjelman alaotsikko -setting_welcome_text: Tervetulo teksti -setting_default_language: Vakio kieli -setting_login_required: Pakollinen autentikointi -setting_self_registration: Tee-Se-Itse rekisteröinti -setting_attachment_max_size: Liitteen maksimi koko -setting_issues_export_limit: Tapahtumien vienti rajoite +setting_welcome_text: Tervehdysteksti +setting_default_language: Vakiokieli +setting_login_required: Pakollinen kirjautuminen +setting_self_registration: Itserekisteröinti +setting_attachment_max_size: Liitteen maksimikoko +setting_issues_export_limit: Tapahtumien vientirajoite setting_mail_from: Lähettäjän sähköpostiosoite -setting_bcc_recipients: Blind carbon copy vastaanottajat (bcc) -setting_host_name: Isännän nimi +setting_bcc_recipients: Vastaanottajat piilokopiona (bcc) +setting_host_name: Verkko-osoite setting_text_formatting: Tekstin muotoilu setting_wiki_compression: Wiki historian pakkaus setting_feeds_limit: Syötteen sisällön raja -setting_autofetch_changesets: Automaatisen haun souritukset -setting_sys_api_enabled: Salli WS säiliön hallintaan +setting_autofetch_changesets: Automaattisten muutosjoukkojen haku +setting_sys_api_enabled: Salli WS tietovaraston hallintaan setting_commit_ref_keywords: Viittaavat hakusanat setting_commit_fix_keywords: Korjaavat hakusanat setting_autologin: Automaatinen kirjautuminen setting_date_format: Päivän muoto setting_time_format: Ajan muoto setting_cross_project_issue_relations: Salli projektien väliset tapahtuminen suhteet -setting_issue_list_default_columns: Vakio sarakkeiden näyttö tapahtuma listauksessa -setting_repositories_encodings: Säiliön koodaus +setting_issue_list_default_columns: Vakiosarakkeiden näyttö tapahtumalistauksessa +setting_repositories_encodings: Tietovaraston koodaus setting_emails_footer: Sähköpostin alatunniste setting_protocol: Protokolla setting_per_page_options: Sivun objektien määrän asetukset @@ -235,8 +236,8 @@ label_workflow: Työnkulku label_issue_status: Tapahtuman tila label_issue_status_plural: Tapahtumien tilat label_issue_status_new: Uusi tila -label_issue_category: Tapahtuma luokka -label_issue_category_plural: Tapahtuma luokat +label_issue_category: Tapahtumaluokka +label_issue_category_plural: Tapahtumaluokat label_issue_category_new: Uusi luokka label_custom_field: Räätälöity kenttä label_custom_field_plural: Räätälöidyt kentät @@ -249,9 +250,9 @@ label_please_login: Kirjaudu ole hyvä label_register: Rekisteröidy label_password_lost: Hukattu salasana label_home: Koti -label_my_page: Minun sivu -label_my_account: Minun tili -label_my_projects: Minun projektit +label_my_page: Omasivu +label_my_account: Oma tili +label_my_projects: Omat projektit label_administration: Ylläpito label_login: Kirjaudu sisään label_logout: Kirjaudu ulos @@ -266,11 +267,11 @@ label_activity: Historia label_new: Uusi label_logged_as: Kirjauduttu nimellä label_environment: Ympäristö -label_authentication: Autentikointi -label_auth_source: Autentikointi tapa -label_auth_source_new: Uusi autentikointi tapa -label_auth_source_plural: Autentikointi tavat -label_subproject_plural: Alaprojektit +label_authentication: Varmennus +label_auth_source: Varmennustapa +label_auth_source_new: Uusi varmennustapa +label_auth_source_plural: Varmennustavat +label_subproject_plural: Aliprojektit label_min_max_length: Min - Max pituudet label_list: Lista label_date: Päivä @@ -307,8 +308,8 @@ label_confirmation: Vahvistus label_export_to: Vie label_read: Lukee... label_public_projects: Julkiset projektit -label_open_issues: avoin -label_open_issues_plural: avointa +label_open_issues: avoin, yhteensä +label_open_issues_plural: avointa, yhteensä label_closed_issues: suljettu label_closed_issues_plural: suljettua label_total: Yhteensä @@ -341,8 +342,8 @@ label_query_plural: Räätälöidyt haut label_query_new: Uusi haku label_filter_add: Lisää suodatin label_filter_plural: Suodattimet -label_equals: yhtä kuin -label_not_equals: epäsuuri kuin +label_equals: sama kuin +label_not_equals: eri kuin label_in_less_than: pienempi kuin label_in_more_than: suurempi kuin label_today: tänään @@ -353,8 +354,8 @@ label_ago: päiviä sitten label_contains: sisältää label_not_contains: ei sisällä label_day_plural: päivää -label_repository: Säiliö -label_repository_plural: Säiliöt +label_repository: Tietovarasto +label_repository_plural: Tietovarastot label_browse: Selaus label_modification: %d muutos label_modification_plural: %d muutettu @@ -366,7 +367,7 @@ label_deleted: poistettu label_latest_revision: Viimeisin versio label_latest_revision_plural: Viimeisimmät versiot label_view_revisions: Näytä versiot -label_max_size: Maksimi koko +label_max_size: Suurin koko label_sort_highest: Siirrä ylimmäiseksi label_sort_higher: Siirrä ylös label_sort_lower: Siirrä alas @@ -411,15 +412,15 @@ label_loading: Lataa... label_relation_new: Uusi suhde label_relation_delete: Poista suhde label_relates_to: liittyy -label_duplicates: kaksoiskappale +label_duplicates: kopio label_blocks: estää label_blocked_by: estetty label_precedes: edeltää label_follows: seuraa -label_end_to_start: loppu alkuun -label_end_to_end: loppu loppuun -label_start_to_start: alku alkuun -label_start_to_end: alku loppuun +label_end_to_start: lopusta alkuun +label_end_to_end: lopusta loppuun +label_start_to_start: alusta alkuun +label_start_to_end: alusta loppuun label_stay_logged_in: Pysy kirjautuneena label_disabled: poistettu käytöstä label_show_completed_versions: Näytä valmiit versiot @@ -439,14 +440,14 @@ label_week: Viikko label_language_based: Pohjautuen käyttäjän kieleen label_sort_by: Lajittele %s label_send_test_email: Lähetä testi sähköposti -label_feeds_access_key_created_on: RSS pääsy avain luotiin %s sitten +label_feeds_access_key_created_on: RSS salasana luotiin %s sitten label_module_plural: Moduulit label_added_time_by: Lisännyt %s %s sitten label_updated_time: Päivitetty %s sitten label_jump_to_a_project: Siirry projektiin... label_file_plural: Tiedostot label_changeset_plural: Muutosryhmät -label_default_columns: Vakio sarakkeet +label_default_columns: Vakiosarakkeet label_no_change_option: (Ei muutosta) label_bulk_edit_selected_issues: Perusmuotoile valitut tapahtumat label_theme: Teema @@ -457,8 +458,8 @@ label_user_mail_option_selected: "Kaikista tapahtumista vain valitsemistani proj label_user_mail_option_none: "Vain tapahtumista joita valvon tai olen mukana" label_user_mail_no_self_notified: "En halua muistutusta muutoksista joita itse teen" label_registration_activation_by_email: tilin aktivointi sähköpostitse -label_registration_manual_activation: manuaalinen tilin aktivointi -label_registration_automatic_activation: automaattinen tilin aktivointi +label_registration_manual_activation: tilin aktivointi käsin +label_registration_automatic_activation: tilin aktivointi automaattisesti label_display_per_page: 'Per sivu: %s' label_age: Ikä label_change_properties: Vaihda asetuksia @@ -507,7 +508,7 @@ status_locked: lukittu text_select_mail_notifications: Valitse tapahtumat joista tulisi lähettää sähköpostimuistutus. text_regexp_info: esim. ^[A-Z0-9]+$ -text_min_max_length_info: 0 tarkoitta, ei rajoitusta +text_min_max_length_info: 0 tarkoittaa, ei rajoitusta text_project_destroy_confirmation: Oletko varma että haluat poistaa tämän projektin ja kaikki siihen kuuluvat tiedot? text_workflow_edit: Valitse rooli ja tapahtuma muokataksesi työnkulkua text_are_you_sure: Oletko varma? @@ -521,7 +522,7 @@ text_project_identifier_info: 'Pienet kirjaimet (a-z), numerot ja viivat ovat sa text_caracters_maximum: %d merkkiä enintään. text_caracters_minimum: Täytyy olla vähintään %d merkkiä pitkä. text_length_between: Pituus välillä %d ja %d merkkiä. -text_tracker_no_workflow: Ei työnkulkua määritelty tälle tapahtumalle +text_tracker_no_workflow: Työnkulkua ei määritelty tälle tapahtumalle text_unallowed_characters: Kiellettyjä merkkejä text_comma_separated: Useat arvot sallittu (pilkku eroteltuna). text_issues_ref_in_commit_messages: Liitän ja korjaan ongelmia syötetyssä viestissä @@ -531,8 +532,8 @@ text_wiki_destroy_confirmation: Oletko varma että haluat poistaa tämän wiki:n text_issue_category_destroy_question: Jotkut tapahtumat (%d) ovat nimetty tälle luokalle. Mitä haluat tehdä? text_issue_category_destroy_assignments: Poista luokan tehtävät text_issue_category_reassign_to: Vaihda tapahtuma tähän luokkaan -text_user_mail_option: "Valitesemattomille projekteille, saat vain muistutuksen asioista joita seuraat tai olet mukana (esim. tapahtumat joissa olet tekijä tai nimettynä)." -text_no_configuration_data: "Rooleja, tikettejä, tapahtumien tiloja ja työnkulkua ei vielä olla määritelty.\nOn erittäin suotavaa ladata vakioasetukset. Voit muuttaa sitä latauksen jälkeen." +text_user_mail_option: "Valitsemattomille projekteille, saat vain muistutuksen asioista joita seuraat tai olet mukana (esim. tapahtumat joissa olet tekijä tai nimettynä)." +text_no_configuration_data: "Rooleja, tapahtumien tiloja ja työnkulkua ei vielä olla määritelty.\nOn erittäin suotavaa ladata vakioasetukset. Voit muuttaa sitä latauksen jälkeen." text_load_default_configuration: Lataa vakioasetukset default_role_manager: Päälikkö @@ -557,7 +558,7 @@ default_priority_immediate: Valitön default_activity_design: Suunnittelu default_activity_development: Kehitys -enumeration_issue_priorities: Tapahtuman prioriteetit +enumeration_issue_priorities: Tapahtuman tärkeysjärjestys enumeration_doc_categories: Dokumentin luokat enumeration_activities: Historia (ajan seuranta) label_associated_revisions: Liittyvät versiot @@ -578,15 +579,15 @@ project_module_issue_tracking: Tapahtuman seuranta project_module_wiki: Wiki project_module_files: Tiedostot project_module_documents: Dokumentit -project_module_repository: Säiliö +project_module_repository: Tietovarasto project_module_news: Uutiset project_module_time_tracking: Ajan seuranta -text_file_repository_writable: Kirjoitettava tiedosto säiliö +text_file_repository_writable: Kirjoitettava tiedostovarasto text_default_administrator_account_changed: Vakio hallinoijan tunnus muutettu text_rmagick_available: RMagick saatavilla (valinnainen) button_configure: Asetukset label_plugins: Lisäosat -label_ldap_authentication: LDAP autentikointi +label_ldap_authentication: LDAP tunnistautuminen label_downloads_abbr: D/L label_add_another_file: Lisää uusi tiedosto label_this_month: tässä kuussa @@ -609,7 +610,7 @@ label_date_to: '' setting_activity_days_default: Päivien esittäminen projektien historiassa label_date_from: '' label_in: '' -setting_display_subprojects_issues: Näytä alaprojektien tapahtumat pääprojektissa oletusarvoisesti +setting_display_subprojects_issues: Näytä aliprojektien tapahtumat pääprojektissa oletusarvoisesti field_comments_sorting: Näytä kommentit label_reverse_chronological_order: Käänteisessä aikajärjestyksessä label_preferences: Asetukset @@ -617,4 +618,21 @@ setting_default_projects_public: Uudet projektit ovat oletuksena julkisia label_overall_activity: Kokonaishistoria error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä." label_planning: Suunnittelu -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +text_subprojects_destroy_warning: 'Tämän aliprojekti(t): %s tullaan myös poistamaan.' +label_and_its_subprojects: %s ja aliprojektit +mail_body_reminder: "%d sinulle nimettyä tapahtuma(a) erääntyy %d päivä sisään:" +mail_subject_reminder: "%d tapahtuma(a) erääntyy lähipäivinä" +text_user_wrote: '%s kirjoitti:' +label_duplicated_by: kopioinut +setting_enabled_scm: Versionhallinta käytettävissä +text_enumeration_category_reassign_to: 'Siirrä täksi arvoksi:' +text_enumeration_destroy_question: '%d kohdetta on sijoitettu tälle arvolle.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/fr.yml b/groups/lang/fr.yml index cbdda4f3d..81e44949f 100644 --- a/groups/lang/fr.yml +++ b/groups/lang/fr.yml @@ -48,6 +48,7 @@ general_text_no: 'non' general_text_yes: 'oui' general_lang_name: 'Français' general_csv_separator: ';' +general_csv_decimal_separator: ',' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche @@ -91,6 +92,8 @@ mail_body_account_information_external: Vous pouvez utiliser votre compte "%s" p mail_body_account_information: Paramètres de connexion de votre compte mail_subject_account_activation_request: "Demande d'activation d'un compte %s" mail_body_account_activation_request: "Un nouvel utilisateur (%s) s'est inscrit. Son compte nécessite votre approbation:" +mail_subject_reminder: "%d demande(s) arrivent à échéance" +mail_body_reminder: "%d demande(s) qui vous sont assignées arrivent à échéance dans les %d prochains jours:" gui_validation_error: 1 erreur gui_validation_error_plural: %d erreurs @@ -180,6 +183,7 @@ field_time_zone: Fuseau horaire field_searchable: Utilisé pour les recherches field_default_value: Valeur par défaut field_comments_sorting: Afficher les commentaires +field_parent_title: Page parent setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application @@ -206,12 +210,16 @@ setting_time_format: Format d'heure setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes setting_repositories_encodings: Encodages des dépôts +setting_commit_logs_encoding: Encodage des messages de commit setting_emails_footer: Pied-de-page des emails setting_protocol: Protocole setting_per_page_options: Options d'objets affichés par page setting_user_format: Format d'affichage des utilisateurs setting_activity_days_default: Nombre de jours affichés sur l'activité des projets setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux +setting_enabled_scm: SCM activés +setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails" +setting_mail_handler_api_key: Clé de protection de l'API project_module_issue_tracking: Suivi des demandes project_module_time_tracking: Suivi du temps passé @@ -291,6 +299,7 @@ label_auth_source: Mode d'authentification label_auth_source_new: Nouveau mode d'authentification label_auth_source_plural: Modes d'authentification label_subproject_plural: Sous-projets +label_and_its_subprojects: %s et ses sous-projets label_min_max_length: Longueurs mini - maxi label_list: Liste label_date: Date @@ -444,7 +453,8 @@ label_loading: Chargement... label_relation_new: Nouvelle relation label_relation_delete: Supprimer la relation label_relates_to: lié à -label_duplicates: doublon de +label_duplicates: duplique +label_duplicated_by: dupliqué par label_blocks: bloque label_blocked_by: bloqué par label_precedes: précède @@ -510,6 +520,9 @@ label_preferences: Préférences label_chronological_order: Dans l'ordre chronologique label_reverse_chronological_order: Dans l'ordre chronologique inverse label_planning: Planning +label_incoming_emails: Emails entrants +label_generate_key: Générer une clé +label_issue_watchers: Utilisateurs surveillant cette demande button_login: Connexion button_submit: Soumettre @@ -548,6 +561,7 @@ button_copy: Copier button_annotate: Annoter button_update: Mettre à jour button_configure: Configurer +button_quote: Citer status_active: actif status_registered: enregistré @@ -566,7 +580,7 @@ text_journal_deleted: supprimé text_tip_task_begin_day: tâche commençant ce jour text_tip_task_end_day: tâche finissant ce jour text_tip_task_begin_end_day: tâche commençant et finissant ce jour -text_project_identifier_info: 'Lettres minuscules (a-z), chiffres et tirets autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.' +text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres et tirets sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.' text_caracters_maximum: %d caractères maximum. text_caracters_minimum: %d caractères minimum. text_length_between: Longueur comprise entre %d et %d caractères. @@ -593,6 +607,10 @@ text_destroy_time_entries_question: %.02f heures ont été enregistrées sur les text_destroy_time_entries: Supprimer les heures text_assign_time_entries_to_project: Reporter les heures sur le projet text_reassign_time_entries: 'Reporter les heures sur cette demande:' +text_user_wrote: '%s a écrit:' +text_enumeration_destroy_question: 'Cette valeur est affectée à %d objets.' +text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:' +text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/email.yml et redémarrez l'application pour les activer." default_role_manager: Manager default_role_developper: Développeur diff --git a/groups/lang/he.yml b/groups/lang/he.yml index a611c8c39..77fe32e53 100644 --- a/groups/lang/he.yml +++ b/groups/lang/he.yml @@ -48,6 +48,7 @@ general_text_no: 'ל×' general_text_yes: 'כן' general_lang_name: 'Hebrew (עברית)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-8-I general_pdf_encoding: ISO-8859-8-I general_day_names: ×©× ×™,שלישי,רביעי,חמישי,שישי,שבת,ר×שון @@ -618,3 +619,20 @@ setting_default_projects_public: ×¤×¨×•×™×§×˜×™× ×—×“×©×™× ×”×™× × ×¤×•×ž×‘×™ error_scm_annotate: "×”×›× ×™×¡×” ×œ× ×§×™×™×ž×ª ×ו ×©×œ× × ×™×ª×Ÿ לת×ר ×ותה." label_planning: ×ª×›× ×•×Ÿ text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/hu.yml b/groups/lang/hu.yml new file mode 100644 index 000000000..208b6fe1e --- /dev/null +++ b/groups/lang/hu.yml @@ -0,0 +1,639 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: Január,Február,Március,Ãprilis,Május,Június,Július,Augusztus,Szeptember,Október,November,December +actionview_datehelper_select_month_names_abbr: Jan,Feb,Már,Ãpr,Máj,Jún,Júl,Aug,Szept,Okt,Nov,Dec +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 nap +actionview_datehelper_time_in_words_day_plural: %d nap +actionview_datehelper_time_in_words_hour_about: kb. 1 óra +actionview_datehelper_time_in_words_hour_about_plural: kb. %d óra +actionview_datehelper_time_in_words_hour_about_single: kb. 1 óra +actionview_datehelper_time_in_words_minute: 1 perc +actionview_datehelper_time_in_words_minute_half: fél perc +actionview_datehelper_time_in_words_minute_less_than: kevesebb, mint 1 perc +actionview_datehelper_time_in_words_minute_plural: %d perc +actionview_datehelper_time_in_words_minute_single: 1 perc +actionview_datehelper_time_in_words_second_less_than: kevesebb, mint 1 másodperc +actionview_datehelper_time_in_words_second_less_than_plural: kevesebb, mint %d másodperc +actionview_instancetag_blank_option: Kérem válasszon + +activerecord_error_inclusion: nem található a listában +activerecord_error_exclusion: foglalt +activerecord_error_invalid: érvénytelen +activerecord_error_confirmation: jóváhagyás szükséges +activerecord_error_accepted: ell kell fogadni +activerecord_error_empty: nem lehet üres +activerecord_error_blank: nem lehet üres +activerecord_error_too_long: túl hosszú +activerecord_error_too_short: túl rövid +activerecord_error_wrong_length: hibás a hossza +activerecord_error_taken: már foglalt +activerecord_error_not_a_number: nem egy szám +activerecord_error_not_a_date: nem érvényes dátum +activerecord_error_greater_than_start_date: nagyobbnak kell lennie, mint az indÃtás dátuma +activerecord_error_not_same_project: nem azonos projekthez tartozik +activerecord_error_circular_dependency: Ez a kapcsolat egy körkörös függÅ‘séget eredményez + +general_fmt_age: %d év +general_fmt_age_plural: %d év +general_fmt_date: %%Y.%%m.%%d +general_fmt_datetime: %%Y.%%m.%%d %%H:%%M:%%S +general_fmt_datetime_short: %%b %%d, %%H:%%M:%%S +general_fmt_time: %%H:%%M:%%S +general_text_No: 'Nem' +general_text_Yes: 'Igen' +general_text_no: 'nem' +general_text_yes: 'igen' +general_lang_name: 'Magyar' +general_csv_separator: ',' +general_csv_decimal_separator: '.' +general_csv_encoding: ISO-8859-2 +general_pdf_encoding: ISO-8859-2 +general_day_names: HétfÅ‘,Kedd,Szerda,Csütörtök,Péntek,Szombat,Vasárnap +general_first_day_of_week: '1' + +notice_account_updated: A fiók adatai sikeresen frissÃtve. +notice_account_invalid_creditentials: Hibás felhasználói név, vagy jelszó +notice_account_password_updated: A jelszó módosÃtása megtörtént. +notice_account_wrong_password: Hibás jelszó +notice_account_register_done: A fiók sikeresen létrehozva. Aktiválásához kattints az e-mailben kapott linkre +notice_account_unknown_email: Ismeretlen felhasználó. +notice_can_t_change_password: A fiók külsÅ‘ azonosÃtási forrást használ. A jelszó megváltoztatása nem lehetséges. +notice_account_lost_email_sent: Egy e-mail üzenetben postáztunk Önnek egy leÃrást az új jelszó beállÃtásáról. +notice_account_activated: Fiókját aktiváltuk. Most már be tud jelentkezni a rendszerbe. +notice_successful_create: Sikeres létrehozás. +notice_successful_update: Sikeres módosÃtás. +notice_successful_delete: Sikeres törlés. +notice_successful_connection: Sikeres bejelentkezés. +notice_file_not_found: Az oldal, amit meg szeretne nézni nem található, vagy átkerült egy másik helyre. +notice_locking_conflict: Az adatot egy másik felhasználó idÅ‘ közben módosÃtotta. +notice_not_authorized: Nincs hozzáférési engedélye ehhez az oldalhoz. +notice_email_sent: Egy e-mail üzenetet küldtünk a következÅ‘ cÃmre %s +notice_email_error: Hiba történt a levél küldése közben (%s) +notice_feeds_access_key_reseted: Az RSS hozzáférési kulcsát újra generáltuk. +notice_failed_to_save_issues: "Nem sikerült a %d feladat(ok) mentése a %d -ban kiválasztva: %s." +notice_no_issue_selected: "Nincs feladat kiválasztva! Kérem jelölje meg melyik feladatot szeretné szerkeszteni!" +notice_account_pending: "A fiókja létrejött, és adminisztrátori jóváhagyásra vár." +notice_default_data_loaded: Az alapértelmezett konfiguráció betöltése sikeresen megtörtént. + +error_can_t_load_default_data: "Az alapértelmezett konfiguráció betöltése nem lehetséges: %s" +error_scm_not_found: "A bejegyzés, vagy revÃzió nem található a tárolóban." +error_scm_command_failed: "A tároló elérése közben hiba lépett fel: %s" +error_scm_annotate: "A bejegyzés nem létezik, vagy nics jegyzetekkel ellátva." +error_issue_not_found_in_project: 'A feladat nem található, vagy nem ehhez a projekthez tartozik' + +mail_subject_lost_password: Az Ön Redmine jelszava +mail_body_lost_password: 'A Redmine jelszó megváltoztatásához, kattintson a következÅ‘ linkre:' +mail_subject_register: Redmine azonosÃtó aktiválása +mail_body_register: 'A Redmine azonosÃtója aktiválásához, kattintson a következÅ‘ linkre:' +mail_body_account_information_external: A "%s" azonosÃtó használatával bejelentkezhet a Redmineba. +mail_body_account_information: Az Ön Redmine azonosÃtójának információi +mail_subject_account_activation_request: Redmine azonosÃtó aktiválási kérelem +mail_body_account_activation_request: 'Egy új felhasználó (%s) regisztrált, azonosÃtója jóváhasgyásra várakozik:' + +gui_validation_error: 1 hiba +gui_validation_error_plural: %d hiba + +field_name: Név +field_description: LeÃrás +field_summary: Összegzés +field_is_required: KötelezÅ‘ +field_firstname: Keresztnév +field_lastname: Vezetéknév +field_mail: E-mail +field_filename: Fájl +field_filesize: Méret +field_downloads: Letöltések +field_author: SzerzÅ‘ +field_created_on: Létrehozva +field_updated_on: MódosÃtva +field_field_format: Formátum +field_is_for_all: Minden projekthez +field_possible_values: Lehetséges értékek +field_regexp: Reguláris kifejezés +field_min_length: Minimum hossz +field_max_length: Maximum hossz +field_value: Érték +field_category: Kategória +field_title: CÃm +field_project: Projekt +field_issue: Feladat +field_status: Státusz +field_notes: Feljegyzések +field_is_closed: Feladat lezárva +field_is_default: Alapértelmezett érték +field_tracker: TÃpus +field_subject: Tárgy +field_due_date: Befejezés dátuma +field_assigned_to: FelelÅ‘s +field_priority: Prioritás +field_fixed_version: Cél verzió +field_user: Felhasználó +field_role: Szerepkör +field_homepage: Weboldal +field_is_public: Nyilvános +field_parent: SzülÅ‘ projekt +field_is_in_chlog: Feladatok látszanak a változás naplóban +field_is_in_roadmap: Feladatok látszanak az életútban +field_login: AzonosÃtó +field_mail_notification: E-mail értesÃtések +field_admin: Adminisztrátor +field_last_login_on: Utolsó bejelentkezés +field_language: Nyelv +field_effective_date: Dátum +field_password: Jelszó +field_new_password: Új jelszó +field_password_confirmation: MegerÅ‘sÃtés +field_version: Verzió +field_type: TÃpus +field_host: Kiszolgáló +field_port: Port +field_account: Felhasználói fiók +field_base_dn: Base DN +field_attr_login: Bejelentkezési tulajdonság +field_attr_firstname: Családnév +field_attr_lastname: Utónév +field_attr_mail: E-mail +field_onthefly: On-the-fly felhasználó létrehozás +field_start_date: Kezdés dátuma +field_done_ratio: Elkészült (%%) +field_auth_source: AzonosÃtási mód +field_hide_mail: Rejtse el az e-mail cÃmem +field_comments: Megjegyzés +field_url: URL +field_start_page: KezdÅ‘lap +field_subproject: Alprojekt +field_hours: Óra +field_activity: Aktivitás +field_spent_on: Dátum +field_identifier: AzonosÃtó +field_is_filter: SzűrÅ‘ként használható +field_issue_to_id: Kapcsolódó feladat +field_delay: Késés +field_assignable: Feladat rendelhetÅ‘ ehhez a szerepkörhöz +field_redirect_existing_links: LétezÅ‘ linkek átirányÃtása +field_estimated_hours: Becsült idÅ‘ +field_column_names: Oszlopok +field_time_zone: IdÅ‘zóna +field_searchable: KereshetÅ‘ +field_default_value: Alapértelmezett érték +field_comments_sorting: Feljegyzések megjelenÃtése + +setting_app_title: Alkalmazás cÃme +setting_app_subtitle: Alkalmazás alcÃme +setting_welcome_text: ÜdvözlÅ‘ üzenet +setting_default_language: Alapértelmezett nyelv +setting_login_required: AzonosÃtás szükséges +setting_self_registration: Regisztráció +setting_attachment_max_size: Melléklet max. mérete +setting_issues_export_limit: Feladatok exportálásának korlátja +setting_mail_from: Kibocsátó e-mail cÃme +setting_bcc_recipients: Titkos másolat cÃmzet (bcc) +setting_host_name: Kiszolgáló neve +setting_text_formatting: Szöveg formázás +setting_wiki_compression: Wiki történet tömörÃtés +setting_feeds_limit: RSS tartalom korlát +setting_default_projects_public: Az új projektek alapértelmezés szerint nyilvánosak +setting_autofetch_changesets: Commitok automatikus lehúzása +setting_sys_api_enabled: WS engedélyezése a tárolók kezeléséhez +setting_commit_ref_keywords: Hivatkozó kulcsszavak +setting_commit_fix_keywords: JavÃtások kulcsszavai +setting_autologin: Automatikus bejelentkezés +setting_date_format: Dátum formátum +setting_time_format: IdÅ‘ formátum +setting_cross_project_issue_relations: Kereszt-projekt feladat hivatkozások engedélyezése +setting_issue_list_default_columns: Az alapértelmezésként megjelenÃtett oszlopok a feladat listában +setting_repositories_encodings: Tárolók kódolása +setting_emails_footer: E-mail lábléc +setting_protocol: Protokol +setting_per_page_options: Objektum / oldal opciók +setting_user_format: Felhasználók megjelenÃtésének formája +setting_activity_days_default: Napok megjelenÃtése a project aktivitásnál +setting_display_subprojects_issues: Alapértelmezettként mutassa az alprojektek feladatait is a projekteken + +project_module_issue_tracking: Feladat követés +project_module_time_tracking: IdÅ‘ rögzÃtés +project_module_news: HÃrek +project_module_documents: Dokumentumok +project_module_files: Fájlok +project_module_wiki: Wiki +project_module_repository: Tároló +project_module_boards: Fórumok + +label_user: Felhasználó +label_user_plural: Felhasználók +label_user_new: Új felhasználó +label_project: Projekt +label_project_new: Új projekt +label_project_plural: Projektek +label_project_all: Az összes projekt +label_project_latest: Legutóbbi projektek +label_issue: Feladat +label_issue_new: Új feladat +label_issue_plural: Feladatok +label_issue_view_all: Minden feladat megtekintése +label_issues_by: %s feladatai +label_issue_added: Feladat hozzáadva +label_issue_updated: Feladat frissÃtve +label_document: Dokumentum +label_document_new: Új dokumentum +label_document_plural: Dokumentumok +label_document_added: Dokumentum hozzáadva +label_role: Szerepkör +label_role_plural: Szerepkörök +label_role_new: Új szerepkör +label_role_and_permissions: Szerepkörök, és jogosultságok +label_member: RésztvevÅ‘ +label_member_new: Új résztvevÅ‘ +label_member_plural: RésztvevÅ‘k +label_tracker: Feladat tÃpus +label_tracker_plural: Feladat tÃpusok +label_tracker_new: Új feladat tÃpus +label_workflow: Workflow +label_issue_status: Feladat státusz +label_issue_status_plural: Feladat státuszok +label_issue_status_new: Új státusz +label_issue_category: Feladat kategória +label_issue_category_plural: Feladat kategóriák +label_issue_category_new: Új kategória +label_custom_field: Egyéni mezÅ‘ +label_custom_field_plural: Egyéni mezÅ‘k +label_custom_field_new: Új egyéni mezÅ‘ +label_enumerations: Felsorolások +label_enumeration_new: Új érték +label_information: Információ +label_information_plural: Információk +label_please_login: Jelentkezzen be +label_register: Regisztráljon +label_password_lost: Elfelejtett jelszó +label_home: KezdÅ‘lap +label_my_page: Saját kezdÅ‘lapom +label_my_account: Fiókom adatai +label_my_projects: Saját projektem +label_administration: Adminisztráció +label_login: Bejelentkezés +label_logout: Kijelentkezés +label_help: Súgó +label_reported_issues: Bejelentett feladatok +label_assigned_to_me_issues: A nekem kiosztott feladatok +label_last_login: Utolsó bejelentkezés +label_last_updates: Utoljára frissÃtve +label_last_updates_plural: Utoljára módosÃtva %d +label_registered_on: Regisztrált +label_activity: Tevékenységek +label_overall_activity: Teljes aktivitás +label_new: Új +label_logged_as: Bejelentkezve, mint +label_environment: Környezet +label_authentication: AzonosÃtás +label_auth_source: AzonosÃtás módja +label_auth_source_new: Új azonosÃtási mód +label_auth_source_plural: AzonosÃtási módok +label_subproject_plural: Alprojektek +label_and_its_subprojects: %s és alprojektjei +label_min_max_length: Min - Max hossz +label_list: Lista +label_date: Dátum +label_integer: Egész +label_float: LebegÅ‘pontos +label_boolean: Logikai +label_string: Szöveg +label_text: Hosszú szöveg +label_attribute: Tulajdonság +label_attribute_plural: Tulajdonságok +label_download: %d Letöltés +label_download_plural: %d Letöltések +label_no_data: Nincs megjelenÃthetÅ‘ adat +label_change_status: Státusz módosÃtása +label_history: Történet +label_attachment: Fájl +label_attachment_new: Új fájl +label_attachment_delete: Fájl törlése +label_attachment_plural: Fájlok +label_file_added: Fájl hozzáadva +label_report: Jelentés +label_report_plural: Jelentések +label_news: HÃrek +label_news_new: HÃr hozzáadása +label_news_plural: HÃrek +label_news_latest: Legutóbbi hÃrek +label_news_view_all: Minden hÃr megtekintése +label_news_added: HÃr hozzáadva +label_change_log: Változás napló +label_settings: BeállÃtások +label_overview: Ãttekintés +label_version: Verzió +label_version_new: Új verzió +label_version_plural: Verziók +label_confirmation: Jóváhagyás +label_export_to: Exportálás +label_read: Olvas... +label_public_projects: Nyilvános projektek +label_open_issues: nyitott +label_open_issues_plural: nyitott +label_closed_issues: lezárt +label_closed_issues_plural: lezárt +label_total: Összesen +label_permissions: Jogosultságok +label_current_status: Jelenlegi státusz +label_new_statuses_allowed: Státusz változtatások engedélyei +label_all: mind +label_none: nincs +label_nobody: senki +label_next: KövetkezÅ‘ +label_previous: ElÅ‘zÅ‘ +label_used_by: Használja +label_details: Részletek +label_add_note: Jegyzet hozzáadása +label_per_page: Oldalanként +label_calendar: Naptár +label_months_from: hónap, kezdve +label_gantt: Gantt +label_internal: BelsÅ‘ +label_last_changes: utolsó %d változás +label_change_view_all: Minden változás megtekintése +label_personalize_page: Az oldal testreszabása +label_comment: Megjegyzés +label_comment_plural: Megjegyzés +label_comment_add: Megjegyzés hozzáadása +label_comment_added: Megjegyzés hozzáadva +label_comment_delete: Megjegyzések törlése +label_query: Egyéni lekérdezés +label_query_plural: Egyéni lekérdezések +label_query_new: Új lekérdezés +label_filter_add: SzűrÅ‘ hozzáadása +label_filter_plural: SzűrÅ‘k +label_equals: egyenlÅ‘ +label_not_equals: nem egyenlÅ‘ +label_in_less_than: kevesebb, mint +label_in_more_than: több, mint +label_in: in +label_today: ma +label_all_time: mindenkor +label_yesterday: tegnap +label_this_week: aktuális hét +label_last_week: múlt hét +label_last_n_days: az elmúlt %d nap +label_this_month: aktuális hónap +label_last_month: múlt hónap +label_this_year: aktuális év +label_date_range: Dátum intervallum +label_less_than_ago: kevesebb, mint nappal ezelÅ‘tt +label_more_than_ago: több, mint nappal ezelÅ‘tt +label_ago: nappal ezelÅ‘tt +label_contains: tartalmazza +label_not_contains: nem tartalmazza +label_day_plural: nap +label_repository: Tároló +label_repository_plural: Tárolók +label_browse: Tallóz +label_modification: %d változás +label_modification_plural: %d változások +label_revision: RevÃzió +label_revision_plural: RevÃziók +label_associated_revisions: Kapcsolt revÃziók +label_added: hozzáadva +label_modified: módosÃtva +label_deleted: törölve +label_latest_revision: Legutolsó revÃzió +label_latest_revision_plural: Legutolsó revÃziók +label_view_revisions: RevÃziók megtekintése +label_max_size: Maximális méret +label_on: 'összesen' +label_sort_highest: Az elejére +label_sort_higher: Eggyel feljebb +label_sort_lower: Eggyel lejjebb +label_sort_lowest: Az aljára +label_roadmap: Életút +label_roadmap_due_in: Elkészültéig várhatóan még +label_roadmap_overdue: %s késésben +label_roadmap_no_issues: Nincsenek feladatok ehhez a verzióhoz +label_search: Keresés +label_result_plural: Találatok +label_all_words: Minden szó +label_wiki: Wiki +label_wiki_edit: Wiki szerkesztés +label_wiki_edit_plural: Wiki szerkesztések +label_wiki_page: Wiki oldal +label_wiki_page_plural: Wiki oldalak +label_index_by_title: CÃm szerint indexelve +label_index_by_date: Dátum szerint indexelve +label_current_version: Jelenlegi verzió +label_preview: ElÅ‘nézet +label_feed_plural: Visszajelzések +label_changes_details: Változások részletei +label_issue_tracking: Feladat követés +label_spent_time: RáfordÃtott idÅ‘ +label_f_hour: %.2f óra +label_f_hour_plural: %.2f óra +label_time_tracking: IdÅ‘ követés +label_change_plural: Változások +label_statistics: Statisztikák +label_commits_per_month: Commits havonta +label_commits_per_author: Commits szerzÅ‘nként +label_view_diff: Különbségek megtekintése +label_diff_inline: inline +label_diff_side_by_side: side by side +label_options: Opciók +label_copy_workflow_from: Workflow másolása innen +label_permissions_report: Jogosultsági riport +label_watched_issues: Megfigyelt feladatok +label_related_issues: Kapcsolódó feladatok +label_applied_status: Alkalmazandó státusz +label_loading: Betöltés... +label_relation_new: Új kapcsolat +label_relation_delete: Kapcsolat törlése +label_relates_to: kapcsolódik +label_duplicates: duplikálja +label_blocks: zárolja +label_blocked_by: zárolta +label_precedes: megelÅ‘zi +label_follows: követi +label_end_to_start: végétÅ‘l indulásig +label_end_to_end: végétÅ‘l végéig +label_start_to_start: indulástól indulásig +label_start_to_end: indulástól végéig +label_stay_logged_in: Emlékezzen rám +label_disabled: kikapcsolva +label_show_completed_versions: A kész verziók mutatása +label_me: én +label_board: Fórum +label_board_new: Új fórum +label_board_plural: Fórumok +label_topic_plural: Témák +label_message_plural: Üzenetek +label_message_last: Utolsó üzenet +label_message_new: Új üzenet +label_message_posted: Üzenet hozzáadva +label_reply_plural: Válaszok +label_send_information: Fiók infomációk küldése a felhasználónak +label_year: Év +label_month: Hónap +label_week: Hét +label_date_from: 'Kezdet:' +label_date_to: 'Vége:' +label_language_based: A felhasználó nyelve alapján +label_sort_by: %s szerint rendezve +label_send_test_email: Teszt e-mail küldése +label_feeds_access_key_created_on: 'RSS hozzáférési kulcs létrehozva ennyivel ezelÅ‘tt: %s' +label_module_plural: Modulok +label_added_time_by: '%s adta hozzá ennyivel ezelÅ‘tt: %s' +label_updated_time: 'Utolsó módosÃtás ennyivel ezelÅ‘tt: %s' +label_jump_to_a_project: Ugrás projekthez... +label_file_plural: Fájlok +label_changeset_plural: Changesets +label_default_columns: Alapértelmezett oszlopok +label_no_change_option: (Nincs változás) +label_bulk_edit_selected_issues: A kiválasztott feladatok kötegelt szerkesztése +label_theme: Téma +label_default: Alapértelmezett +label_search_titles_only: Keresés csak a cÃmekben +label_user_mail_option_all: "Minden eseményrÅ‘l minden saját projektemben" +label_user_mail_option_selected: "Minden eseményrÅ‘l a kiválasztott projektekben..." +label_user_mail_option_none: "Csak a megfigyelt dolgokról, vagy, amiben részt veszek" +label_user_mail_no_self_notified: "Nem kérek értesÃtést az általam végzett módosÃtásokról" +label_registration_activation_by_email: Fiók aktiválása e-mailben +label_registration_manual_activation: Manuális fiók aktiválás +label_registration_automatic_activation: Automatikus fiók aktiválás +label_display_per_page: 'Oldalanként: %s' +label_age: Kor +label_change_properties: Tulajdonságok változtatása +label_general: Ãltalános +label_more: továbbiak +label_scm: SCM +label_plugins: Pluginek +label_ldap_authentication: LDAP azonosÃtás +label_downloads_abbr: D/L +label_optional_description: Opcionális leÃrás +label_add_another_file: Újabb fájl hozzáadása +label_preferences: Tulajdonságok +label_chronological_order: IdÅ‘rendben +label_reverse_chronological_order: FordÃtott idÅ‘rendben +label_planning: Tervezés + +button_login: Bejelentkezés +button_submit: Elfogad +button_save: Mentés +button_check_all: Mindent kijelöl +button_uncheck_all: Kijelölés törlése +button_delete: Töröl +button_create: Létrehoz +button_test: Teszt +button_edit: Szerkeszt +button_add: Hozzáad +button_change: Változtat +button_apply: Alkalmaz +button_clear: Töröl +button_lock: Zárol +button_unlock: Felold +button_download: Letöltés +button_list: Lista +button_view: Megnéz +button_move: Mozgat +button_back: Vissza +button_cancel: Mégse +button_activate: Aktivál +button_sort: Rendezés +button_log_time: IdÅ‘ rögzÃtés +button_rollback: Visszaáll erre a verzióra +button_watch: Megfigyel +button_unwatch: Megfigyelés törlése +button_reply: Válasz +button_archive: Archivál +button_unarchive: Dearchivál +button_reset: Reset +button_rename: Ãtnevez +button_change_password: Jelszó megváltoztatása +button_copy: Másol +button_annotate: Jegyzetel +button_update: MódosÃt +button_configure: Konfigurál + +status_active: aktÃv +status_registered: regisztrált +status_locked: zárolt + +text_select_mail_notifications: Válasszon eseményeket, amelyekrÅ‘l e-mail értesÃtést kell küldeni. +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 = nincs korlátozás +text_project_destroy_confirmation: Biztosan törölni szeretné a projektet és vele együtt minden kapcsolódó adatot ? +text_subprojects_destroy_warning: 'Az alprojekt(ek): %s szintén törlésre kerülnek.' +text_workflow_edit: Válasszon egy szerepkört, és egy trackert a workflow szerkesztéséhez +text_are_you_sure: Biztos benne ? +text_journal_changed: "változás: %s volt, %s lett" +text_journal_set_to: "beállÃtva: %s" +text_journal_deleted: törölve +text_tip_task_begin_day: a feladat ezen a napon kezdÅ‘dik +text_tip_task_end_day: a feladat ezen a napon ér véget +text_tip_task_begin_end_day: a feladat ezen a napon kezdÅ‘dik és ér véget +text_project_identifier_info: 'Kis betűk (a-z), számok és kötÅ‘jel megengedett.<br />Mentés után az azonosÃtót megváltoztatni nem lehet.' +text_caracters_maximum: maximum %d karakter. +text_caracters_minimum: Legkevesebb %d karakter hosszúnek kell lennie. +text_length_between: Legalább %d és legfeljebb %d hosszú karakter. +text_tracker_no_workflow: Nincs workflow definiálva ehhez a tracker-hez +text_unallowed_characters: Tiltott karakterek +text_comma_separated: Több érték megengedett (vesszÅ‘vel elválasztva) +text_issues_ref_in_commit_messages: Hivatkozás feladatokra, feladatok javÃtása a commit üzenetekben +text_issue_added: %s feladat bejelentve. +text_issue_updated: %s feladat frissÃtve. +text_wiki_destroy_confirmation: Biztosan törölni szeretné ezt a wiki-t minden tartalmával együtt ? +text_issue_category_destroy_question: Néhány feladat (%d) hozzá van rendelve ehhez a kategóriához. Mit szeretne tenni ? +text_issue_category_destroy_assignments: Kategória hozzárendelés megszűntetése +text_issue_category_reassign_to: Feladatok újra hozzárendelése a kategóriához +text_user_mail_option: "A nem kiválasztott projektekrÅ‘l csak akkor kap értesÃtést, ha figyelést kér rá, vagy részt vesz benne (pl. Ön a létrehozó, vagy a hozzárendelÅ‘)" +text_no_configuration_data: "Szerepkörök, trackerek, feladat státuszok, és workflow adatok még nincsenek konfigurálva.\nErÅ‘sen ajánlott, az alapértelmezett konfiguráció betöltése, és utána módosÃthatja azt." +text_load_default_configuration: Alapértelmezett konfiguráció betöltése +text_status_changed_by_changeset: Applied in changeset %s. +text_issues_destroy_confirmation: 'Biztos benne, hogy törölni szeretné a kijelölt feladato(ka)t ?' +text_select_project_modules: 'Válassza ki az engedélyezett modulokat ehhez a projekthez:' +text_default_administrator_account_changed: Alapértelmezett adminisztrátor fiók megváltoztatva +text_file_repository_writable: Fájl tároló Ãrható +text_rmagick_available: RMagick elérhetÅ‘ (opcionális) +text_destroy_time_entries_question: %.02f órányi munka van rögzÃtve a feladatokon, amiket törölni szeretne. Mit szeretne tenni ? +text_destroy_time_entries: A rögzÃtett órák törlése +text_assign_time_entries_to_project: A rögzÃtett órák hozzárendelése a projekthez +text_reassign_time_entries: 'A rögzÃtett órák újra hozzárendelése ehhez a feladathoz:' + +default_role_manager: VezetÅ‘ +default_role_developper: FejlesztÅ‘ +default_role_reporter: BejelentÅ‘ +default_tracker_bug: Hiba +default_tracker_feature: Fejlesztés +default_tracker_support: Support +default_issue_status_new: Új +default_issue_status_assigned: Kiosztva +default_issue_status_resolved: Megoldva +default_issue_status_feedback: Visszajelzés +default_issue_status_closed: Lezárt +default_issue_status_rejected: ElutasÃtott +default_doc_category_user: Felhasználói dokumentáció +default_doc_category_tech: Technikai dokumentáció +default_priority_low: Alacsony +default_priority_normal: Normál +default_priority_high: Magas +default_priority_urgent: SürgÅ‘s +default_priority_immediate: Azonnal +default_activity_design: Tervezés +default_activity_development: Fejlesztés + +enumeration_issue_priorities: Feladat prioritások +enumeration_doc_categories: Dokumentum kategóriák +enumeration_activities: Tevékenységek (idÅ‘ rögzÃtés) +mail_body_reminder: "%d neked kiosztott feladat határidÅ‘s az elkövetkezÅ‘ %d napban:" +mail_subject_reminder: "%d feladat határidÅ‘s az elkövetkezÅ‘ napokban" +text_user_wrote: '%s Ãrta:' +label_duplicated_by: duplikálta +setting_enabled_scm: ForráskódkezelÅ‘ (SCM) engedélyezése +text_enumeration_category_reassign_to: 'Újra hozzárendelés ehhez:' +text_enumeration_destroy_question: '%d objektum van hozzárendelve ehhez az értékhez.' +label_incoming_emails: Beérkezett levelek +label_generate_key: Kulcs generálása +setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez +setting_mail_handler_api_key: API kulcs +text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesÃtések ki vannak kapcsolva.\nÃllÃtsd be az SMTP szervert a config/email.yml fájlban és indÃtsd újra az alkalmazást, hogy érvénybe lépjen." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/it.yml b/groups/lang/it.yml index 3d1dea09e..d123e913a 100644 --- a/groups/lang/it.yml +++ b/groups/lang/it.yml @@ -20,24 +20,24 @@ actionview_datehelper_time_in_words_second_less_than_plural: meno di %d secondi actionview_instancetag_blank_option: Scegli activerecord_error_inclusion: non è incluso nella lista -activerecord_error_exclusion: e' riservato -activerecord_error_invalid: non e' valido +activerecord_error_exclusion: è riservato +activerecord_error_invalid: non è valido activerecord_error_confirmation: non coincide con la conferma activerecord_error_accepted: deve essere accettato activerecord_error_empty: non puo' essere vuoto activerecord_error_blank: non puo' essere blank -activerecord_error_too_long: e' troppo lungo/a -activerecord_error_too_short: e' troppo corto/a -activerecord_error_wrong_length: e' della lunghezza sbagliata -activerecord_error_taken: e' gia' stato/a preso/a -activerecord_error_not_a_number: non e' un numero -activerecord_error_not_a_date: non e' una data valida +activerecord_error_too_long: è troppo lungo/a +activerecord_error_too_short: è troppo corto/a +activerecord_error_wrong_length: è della lunghezza sbagliata +activerecord_error_taken: è già stato/a preso/a +activerecord_error_not_a_number: non è un numero +activerecord_error_not_a_date: non è una data valida activerecord_error_greater_than_start_date: deve essere maggiore della data di partenza -activerecord_error_not_same_project: doesn't belong to the same project -activerecord_error_circular_dependency: This relation would create a circular dependency +activerecord_error_not_same_project: non appartiene allo stesso progetto +activerecord_error_circular_dependency: Questa relazione creerebbe una dipendenza circolare -general_fmt_age: %d yr -general_fmt_age_plural: %d yrs +general_fmt_age: %d anno +general_fmt_age_plural: %d anni general_fmt_date: %%d/%%m/%%Y general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p general_fmt_datetime_short: %%b %%d, %%I:%%M %%p @@ -48,6 +48,7 @@ general_text_no: 'no' general_text_yes: 'si' general_lang_name: 'Italiano' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Lunedì,Martedì,Mercoledì,Giovedì,Venerdì,Sabato,Domenica @@ -68,13 +69,13 @@ notice_successful_delete: Eliminazione effettuata. notice_successful_connection: Connessione effettuata. notice_file_not_found: La pagina desiderata non esiste o è stata rimossa. notice_locking_conflict: Le informazioni sono state modificate da un altro utente. -notice_not_authorized: You are not authorized to access this page. -notice_email_sent: An email was sent to %s -notice_email_error: An error occurred while sending mail (%s) -notice_feeds_access_key_reseted: Your RSS access key was reseted. +notice_not_authorized: Non sei autorizzato ad accedere a questa pagina. +notice_email_sent: Una e-mail è stata spedita a %s +notice_email_error: Si è verificato un errore durante l'invio di una e-mail (%s) +notice_feeds_access_key_reseted: La tua chiave di accesso RSS è stata reimpostata. error_scm_not_found: "La risorsa e/o la versione non esistono nel repository." -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_scm_command_failed: "Si è verificato un errore durante l'accesso al repository: %s" mail_subject_lost_password: Password %s mail_body_lost_password: 'Per cambiare la password, usate il seguente collegamento:' @@ -110,21 +111,21 @@ field_project: Progetto field_issue: Issue field_status: Stato field_notes: Note -field_is_closed: Chiude il contesto +field_is_closed: Chiude la segnalazione field_is_default: Stato predefinito field_tracker: Tracker field_subject: Oggetto field_due_date: Data ultima field_assigned_to: Assegnato a field_priority: Priorita' -field_fixed_version: Target version +field_fixed_version: Versione prevista field_user: Utente field_role: Ruolo field_homepage: Homepage field_is_public: Pubblico field_parent: Sottoprogetto di -field_is_in_chlog: Contesti mostrati nel changelog -field_is_in_roadmap: Contesti mostrati nel roadmap +field_is_in_chlog: Segnalazioni mostrate nel changelog +field_is_in_roadmap: Segnalazioni mostrate nel roadmap field_login: Login field_mail_notification: Notifiche via e-mail field_admin: Amministratore @@ -153,16 +154,16 @@ field_comments: Commento field_url: URL field_start_page: Pagina principale field_subproject: Sottoprogetto -field_hours: Hours -field_activity: Activity +field_hours: Ore +field_activity: Attività field_spent_on: Data -field_identifier: Identifier -field_is_filter: Used as a filter -field_issue_to_id: Related issue -field_delay: Delay -field_assignable: Issues can be assigned to this role -field_redirect_existing_links: Redirect existing links -field_estimated_hours: Estimated time +field_identifier: Identificativo +field_is_filter: Usato come filtro +field_issue_to_id: Segnalazioni correlate +field_delay: Ritardo +field_assignable: E' possibile assegnare segnalazioni a questo ruolo +field_redirect_existing_links: Redirige i collegamenti esistenti +field_estimated_hours: Tempo stimato field_default_value: Stato predefinito setting_app_title: Titolo applicazione @@ -172,7 +173,7 @@ setting_default_language: Lingua di default setting_login_required: Autenticazione richiesta setting_self_registration: Auto-registrazione abilitata setting_attachment_max_size: Massima dimensione allegati -setting_issues_export_limit: Limite esportazione contesti +setting_issues_export_limit: Limite esportazione segnalazioni setting_mail_from: Indirizzo sorgente e-mail setting_host_name: Nome host setting_text_formatting: Formattazione testo @@ -182,9 +183,9 @@ setting_autofetch_changesets: Acquisisci automaticamente le commit setting_sys_api_enabled: Abilita WS per la gestione del repository setting_commit_ref_keywords: Referencing keywords setting_commit_fix_keywords: Fixing keywords -setting_autologin: Autologin -setting_date_format: Date format -setting_cross_project_issue_relations: Allow cross-project issue relations +setting_autologin: Login automatico +setting_date_format: Formato data +setting_cross_project_issue_relations: Consenti la creazione di relazioni tra segnalazioni in progetti differenti label_user: Utente label_user_plural: Utenti @@ -192,12 +193,12 @@ label_user_new: Nuovo utente label_project: Progetto label_project_new: Nuovo progetto label_project_plural: Progetti -label_project_all: All Projects +label_project_all: Tutti i progetti label_project_latest: Ultimi progetti registrati -label_issue: Contesto -label_issue_new: Nuovo contesto -label_issue_plural: Contesti -label_issue_view_all: Mostra tutti i contesti +label_issue: Segnalazione +label_issue_new: Nuova segnalazione +label_issue_plural: Segnalazioni +label_issue_view_all: Mostra tutte le segnalazioni label_document: Documento label_document_new: Nuovo documento label_document_plural: Documenti @@ -212,11 +213,11 @@ label_tracker: Tracker label_tracker_plural: Tracker label_tracker_new: Nuovo tracker label_workflow: Workflow -label_issue_status: Stato contesti -label_issue_status_plural: Stati contesto +label_issue_status: Stato segnalazioni +label_issue_status_plural: Stati segnalazione label_issue_status_new: Nuovo stato -label_issue_category: Categorie contesti -label_issue_category_plural: Categorie contesto +label_issue_category: Categorie segnalazioni +label_issue_category_plural: Categorie segnalazioni label_issue_category_new: Nuova categoria label_custom_field: Campo personalizzato label_custom_field_plural: Campi personalizzati @@ -236,8 +237,8 @@ label_administration: Amministrazione label_login: Login label_logout: Logout label_help: Aiuto -label_reported_issues: Contesti segnalati -label_assigned_to_me_issues: I miei contesti +label_reported_issues: Segnalazioni +label_assigned_to_me_issues: Le mie segnalazioni label_last_login: Ultimo collegamento label_last_updates: Ultimo aggiornamento label_last_updates_plural: %d ultimo aggiornamento @@ -276,7 +277,7 @@ label_news_new: Aggiungi notizia label_news_plural: Notizie label_news_latest: Utime notizie label_news_view_all: Tutte le notizie -label_change_log: Change log +label_change_log: Elenco modifiche label_settings: Impostazioni label_overview: Panoramica label_version: Versione @@ -314,7 +315,7 @@ label_comment_plural: Commenti label_comment_add: Aggiungi un commento label_comment_added: Commento aggiunto label_comment_delete: Elimina commenti -label_query: Custom query +label_query: Query personalizzata label_query_plural: Query personalizzate label_query_new: Nuova query label_filter_add: Aggiungi filtro @@ -325,7 +326,7 @@ label_in_less_than: è minore di label_in_more_than: è maggiore di label_in: in label_today: oggi -label_this_week: this week +label_this_week: questa settimana label_less_than_ago: meno di giorni fa label_more_than_ago: più di giorni fa label_ago: giorni fa @@ -333,7 +334,7 @@ label_contains: contiene label_not_contains: non contiene label_day_plural: giorni label_repository: Repository -label_browse: Browse +label_browse: Sfoglia label_modification: %d modifica label_modification_plural: %d modifiche label_revision: Versione @@ -353,7 +354,7 @@ label_sort_lowest: Sposta in fondo label_roadmap: Roadmap label_roadmap_due_in: Da ultimare in label_roadmap_overdue: %s late -label_roadmap_no_issues: Nessun contesto per questa versione +label_roadmap_no_issues: Nessuna segnalazione per questa versione label_search: Ricerca label_result_plural: Risultati label_all_words: Tutte le parole @@ -368,7 +369,7 @@ label_current_version: Versione corrente label_preview: Anteprima label_feed_plural: Feed label_changes_details: Particolari di tutti i cambiamenti -label_issue_tracking: tracking dei contesti +label_issue_tracking: tracking delle segnalazioni label_spent_time: Tempo impiegato label_f_hour: %.2f ora label_f_hour_plural: %.2f ore @@ -378,53 +379,53 @@ label_statistics: Statistiche label_commits_per_month: Commit per mese label_commits_per_author: Commit per autore label_view_diff: mostra differenze -label_diff_inline: inline -label_diff_side_by_side: side by side +label_diff_inline: in linea +label_diff_side_by_side: fianco a fianco label_options: Opzioni label_copy_workflow_from: Copia workflow da label_permissions_report: Report permessi -label_watched_issues: Watched issues -label_related_issues: Related issues -label_applied_status: Applied status -label_loading: Loading... -label_relation_new: New relation -label_relation_delete: Delete relation -label_relates_to: related to -label_duplicates: duplicates -label_blocks: blocks -label_blocked_by: blocked by -label_precedes: precedes -label_follows: follows +label_watched_issues: Segnalazioni osservate +label_related_issues: Segnalazioni correlate +label_applied_status: Stato applicato +label_loading: Caricamento... +label_relation_new: Nuova relazione +label_relation_delete: Elimina relazione +label_relates_to: correlato a +label_duplicates: duplicati +label_blocks: blocchi +label_blocked_by: bloccato da +label_precedes: precede +label_follows: segue label_end_to_start: end to start label_end_to_end: end to end label_start_to_start: start to start label_start_to_end: start to end -label_stay_logged_in: Stay logged in -label_disabled: disabled -label_show_completed_versions: Show completed versions -label_me: me +label_stay_logged_in: Rimani collegato +label_disabled: disabilitato +label_show_completed_versions: Mostra versioni completate +label_me: io label_board: Forum -label_board_new: New forum -label_board_plural: Forums -label_topic_plural: Topics -label_message_plural: Messages -label_message_last: Last message -label_message_new: New message -label_reply_plural: Replies -label_send_information: Send account information to the user -label_year: Year -label_month: Month -label_week: Week -label_date_from: From -label_date_to: To -label_language_based: Language based -label_sort_by: Sort by %s -label_send_test_email: Send a test email -label_feeds_access_key_created_on: RSS access key created %s ago -label_module_plural: Modules -label_added_time_by: Added by %s %s ago -label_updated_time: Updated %s ago -label_jump_to_a_project: Jump to a project... +label_board_new: Nuovo forum +label_board_plural: Forum +label_topic_plural: Argomenti +label_message_plural: Messaggi +label_message_last: Ultimo messaggio +label_message_new: Nuovo messaggio +label_reply_plural: Risposte +label_send_information: Invia all'utente le informazioni relative all'account +label_year: Anno +label_month: Mese +label_week: Settimana +label_date_from: Da +label_date_to: A +label_language_based: Basato sul linguaggio +label_sort_by: Ordina per %s +label_send_test_email: Invia una e-mail di test +label_feeds_access_key_created_on: chiave di accesso RSS creata %s fa +label_module_plural: Moduli +label_added_time_by: Aggiunto da %s %s fa +label_updated_time: Aggiornato %s fa +label_jump_to_a_project: Vai al progetto... button_login: Login button_submit: Invia @@ -451,13 +452,13 @@ button_activate: Attiva button_sort: Ordina button_log_time: Registra tempo button_rollback: Ripristina questa versione -button_watch: Watch -button_unwatch: Unwatch -button_reply: Reply -button_archive: Archive -button_unarchive: Unarchive +button_watch: Osserva +button_unwatch: Dimentica +button_reply: Rispondi +button_archive: Archivia +button_unarchive: Ripristina button_reset: Reset -button_rename: Rename +button_rename: Rinomina status_active: attivo status_registered: registrato @@ -475,32 +476,32 @@ text_journal_deleted: cancellato text_tip_task_begin_day: attività che iniziano in questa giornata text_tip_task_end_day: attività che terminano in questa giornata text_tip_task_begin_end_day: attività che iniziano e terminano in questa giornata -text_project_identifier_info: 'Lower case letters (a-z), numbers and dashes allowed.<br />Once saved, the identifier can not be changed.' +text_project_identifier_info: "Lettere minuscole (a-z), numeri e trattini permessi.<br />Una volta salvato, l'identificativo non può essere modificato." text_caracters_maximum: massimo %d caratteri. text_length_between: Lunghezza compresa tra %d e %d caratteri. text_tracker_no_workflow: Nessun workflow definito per questo tracker -text_unallowed_characters: Unallowed characters -text_comma_separated: Multiple values allowed (comma separated). +text_unallowed_characters: Caratteri non permessi +text_comma_separated: Valori multipli permessi (separati da virgola). text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages text_issue_added: "E' stata segnalata l'anomalia %s da %s." text_issue_updated: "L'anomalia %s e' stata aggiornata da %s." text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? -text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? -text_issue_category_destroy_assignments: Remove category assignments -text_issue_category_reassign_to: Reassing issues to this category +text_issue_category_destroy_question: Alcune segnalazioni (%d) risultano assegnate a questa categoria. Cosa vuoi fare ? +text_issue_category_destroy_assignments: Rimuovi gli assegnamenti a questa categoria +text_issue_category_reassign_to: Riassegna segnalazioni a questa categoria default_role_manager: Manager default_role_developper: Sviluppatore default_role_reporter: Reporter -default_tracker_bug: Contesto +default_tracker_bug: Segnalazione default_tracker_feature: Funzione default_tracker_support: Supporto -default_issue_status_new: Nuovo/a -default_issue_status_assigned: Assegnato/a -default_issue_status_resolved: Risolto/a +default_issue_status_new: Nuovo +default_issue_status_assigned: Assegnato +default_issue_status_resolved: Risolto default_issue_status_feedback: Feedback -default_issue_status_closed: Chiuso/a -default_issue_status_rejected: Rifiutato/a +default_issue_status_closed: Chiuso +default_issue_status_rejected: Rifiutato default_doc_category_user: Documentazione utente default_doc_category_tech: Documentazione tecnica default_priority_low: Bassa @@ -508,113 +509,130 @@ default_priority_normal: Normale default_priority_high: Alta default_priority_urgent: Urgente default_priority_immediate: Immediata -default_activity_design: Design -default_activity_development: Development +default_activity_design: Progettazione +default_activity_development: Sviluppo -enumeration_issue_priorities: Priorità contesti +enumeration_issue_priorities: Priorità segnalazioni enumeration_doc_categories: Categorie di documenti enumeration_activities: Attività (time tracking) -label_file_plural: Files -label_changeset_plural: Changesets -field_column_names: Columns -label_default_columns: Default columns -setting_issue_list_default_columns: Default columns displayed on the issue list -setting_repositories_encodings: Repositories encodings -notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." -label_bulk_edit_selected_issues: Bulk edit selected issues -label_no_change_option: (No change) -notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." -label_theme: Theme -label_default: Default -label_search_titles_only: Search titles only -label_nobody: nobody -button_change_password: Change password +label_file_plural: File +label_changeset_plural: Changeset +field_column_names: Colonne +label_default_columns: Colonne predefinite +setting_issue_list_default_columns: Colonne predefinite mostrate nell'elenco segnalazioni +setting_repositories_encodings: Codifiche dei repository +notice_no_issue_selected: "Nessuna segnalazione selezionata! Seleziona le segnalazioni che intendi modificare." +label_bulk_edit_selected_issues: Modifica massiva delle segnalazioni selezionate +label_no_change_option: (Nessuna modifica) +notice_failed_to_save_issues: "Impossibile salvare %d segnalazioni su %d selezionate: %s." +label_theme: Tema +label_default: Predefinito +label_search_titles_only: Cerca solo nei titoli +label_nobody: nessuno +button_change_password: Modifica password text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." -label_user_mail_option_selected: "For any event on the selected projects only..." -label_user_mail_option_all: "For any event on all my projects" -label_user_mail_option_none: "Only for things I watch or I'm involved in" -setting_emails_footer: Emails footer +label_user_mail_option_selected: "Solo per gli eventi relativi ai progetti selezionati..." +label_user_mail_option_all: "Per ogni evento relativo ad uno dei miei progetti" +label_user_mail_option_none: "Solo per argomenti che osservo o che mi riguardano" +setting_emails_footer: Piè di pagina e-mail label_float: Float -button_copy: Copy -mail_body_account_information_external: You can use your "%s" account to log in. -mail_body_account_information: Your account information -setting_protocol: Protocol -label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" -setting_time_format: Time format -label_registration_activation_by_email: account activation by email -mail_subject_account_activation_request: %s account activation request -mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' -label_registration_automatic_activation: automatic account activation -label_registration_manual_activation: manual account activation -notice_account_pending: "Your account was created and is now pending administrator approval." +button_copy: Copia +mail_body_account_information_external: Puoi utilizzare il tuo account "%s" per accedere al sistema. +mail_body_account_information: Le informazioni riguardanti il tuo account +setting_protocol: Protocollo +label_user_mail_no_self_notified: "Non voglio notifiche riguardanti modifiche da me apportate" +setting_time_format: Formato ora +label_registration_activation_by_email: attivazione account via e-mail +mail_subject_account_activation_request: %s richiesta attivazione account +mail_body_account_activation_request: 'Un nuovo utente (%s) ha effettuato la registrazione. Il suo account è in attesa di abilitazione da parte tua:' +label_registration_automatic_activation: attivazione account automatica +label_registration_manual_activation: attivazione account manuale +notice_account_pending: "Il tuo account è stato creato ed è in attesa di attivazione da parte dell'amministratore." field_time_zone: Time zone -text_caracters_minimum: Must be at least %d characters long. -setting_bcc_recipients: Blind carbon copy recipients (bcc) -button_annotate: Annotate -label_issues_by: Issues by %s -field_searchable: Searchable -label_display_per_page: 'Per page: %s' -setting_per_page_options: Objects per page options -label_age: Age -notice_default_data_loaded: Default configuration successfully loaded. -text_load_default_configuration: Load the default configuration -text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." -error_can_t_load_default_data: "Default configuration could not be loaded: %s" -button_update: Update -label_change_properties: Change properties -label_general: General -label_repository_plural: Repositories -label_associated_revisions: Associated revisions -setting_user_format: Users display format -text_status_changed_by_changeset: Applied in changeset %s. -label_more: More -text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' +text_caracters_minimum: Deve essere lungo almeno %d caratteri. +setting_bcc_recipients: Destinatari in copia nascosta (bcc) +button_annotate: Annota +label_issues_by: Segnalazioni di %s +field_searchable: Ricercabile +label_display_per_page: 'Per pagina: %s' +setting_per_page_options: Opzioni oggetti per pagina +label_age: Età +notice_default_data_loaded: Configurazione di default caricata con successo. +text_load_default_configuration: Carica la configurazione di default +text_no_configuration_data: "Ruoli, tracker, stati delle segnalazioni e workflow non sono stati ancora configurati.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." +error_can_t_load_default_data: "Non è stato possibile caricare la configurazione di default : %s" +button_update: Aggiorna +label_change_properties: Modifica le proprietà +label_general: Generale +label_repository_plural: Repository +label_associated_revisions: Revisioni associate +setting_user_format: Formato visualizzazione utenti +text_status_changed_by_changeset: Applicata nel changeset %s. +label_more: Altro +text_issues_destroy_confirmation: 'Sei sicuro di voler eliminare le segnalazioni selezionate?' label_scm: SCM -text_select_project_modules: 'Select modules to enable for this project:' -label_issue_added: Issue added -label_issue_updated: Issue updated -label_document_added: Document added -label_message_posted: Message added -label_file_added: File added -label_news_added: News added +text_select_project_modules: 'Seleziona i moduli abilitati per questo progetto:' +label_issue_added: Segnalazioni aggiunte +label_issue_updated: Segnalazioni aggiornate +label_document_added: Documenti aggiunti +label_message_posted: Messaggi aggiunti +label_file_added: File aggiunti +label_news_added: Notizie aggiunte project_module_boards: Boards -project_module_issue_tracking: Issue tracking +project_module_issue_tracking: Tracking delle segnalazioni project_module_wiki: Wiki -project_module_files: Files -project_module_documents: Documents +project_module_files: File +project_module_documents: Documenti project_module_repository: Repository -project_module_news: News +project_module_news: Notizie project_module_time_tracking: Time tracking -text_file_repository_writable: File repository writable -text_default_administrator_account_changed: Default administrator account changed -text_rmagick_available: RMagick available (optional) -button_configure: Configure -label_plugins: Plugins -label_ldap_authentication: LDAP authentication +text_file_repository_writable: Repository dei file scrivibile +text_default_administrator_account_changed: L'account amministrativo di default è stato modificato +text_rmagick_available: RMagick disponibile (opzionale) +button_configure: Configura +label_plugins: Plugin +label_ldap_authentication: Autenticazione LDAP label_downloads_abbr: D/L -label_this_month: this month -label_last_n_days: last %d days -label_all_time: all time -label_this_year: this year -label_date_range: Date range -label_last_week: last week -label_yesterday: yesterday -label_last_month: last month -label_add_another_file: Add another file -label_optional_description: Optional description -text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ? -error_issue_not_found_in_project: 'The issue was not found or does not belong to this project' -text_assign_time_entries_to_project: Assign reported hours to the project -text_destroy_time_entries: Delete reported hours -text_reassign_time_entries: 'Reassign reported hours to this issue:' -setting_activity_days_default: Days displayed on project activity -label_chronological_order: In chronological order -field_comments_sorting: Display comments -label_reverse_chronological_order: In reverse chronological order -label_preferences: Preferences -setting_display_subprojects_issues: Display subprojects issues on main projects by default -label_overall_activity: Overall activity -setting_default_projects_public: New projects are public by default -error_scm_annotate: "The entry does not exist or can not be annotated." -label_planning: Planning -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_this_month: questo mese +label_last_n_days: ultimi %d giorni +label_all_time: sempre +label_this_year: quest'anno +label_date_range: Intervallo di date +label_last_week: ultima settimana +label_yesterday: ieri +label_last_month: ultimo mese +label_add_another_file: Aggiungi un altro file +label_optional_description: Descrizione opzionale +text_destroy_time_entries_question: %.02f ore risultano spese sulle segnalazioni che stai per cancellare. Cosa vuoi fare ? +error_issue_not_found_in_project: 'La segnalazione non è stata trovata o non appartiene al progetto' +text_assign_time_entries_to_project: Assegna le ore segnalate al progetto +text_destroy_time_entries: Elimina le ore segnalate +text_reassign_time_entries: 'Riassegna le ore a questa segnalazione:' +setting_activity_days_default: Giorni mostrati sulle attività di progetto +label_chronological_order: In ordine cronologico +field_comments_sorting: Mostra commenti +label_reverse_chronological_order: In ordine cronologico inverso +label_preferences: Preferenze +setting_display_subprojects_issues: Mostra le segnalazioni dei sottoprogetti nel progetto principale per default +label_overall_activity: Attività generale +setting_default_projects_public: I nuovi progetti sono pubblici per default +error_scm_annotate: "L'oggetto non esiste o non può essere annotato." +label_planning: Pianificazione +text_subprojects_destroy_warning: 'Anche i suoi sottoprogetti: %s verranno eliminati.' +label_and_its_subprojects: %s ed i suoi sottoprogetti +mail_body_reminder: "%d segnalazioni che ti sono state assegnate scadranno nei prossimi %d giorni:" +mail_subject_reminder: "%d segnalazioni in scadenza nei prossimi giorni" +text_user_wrote: '%s ha scritto:' +label_duplicated_by: duplicato da +setting_enabled_scm: SCM abilitato +text_enumeration_category_reassign_to: 'Riassegnale a questo valore:' +text_enumeration_destroy_question: '%d oggetti hanno un assegnamento su questo valore.' +label_incoming_emails: E-mail in arrivo +label_generate_key: Genera una chiave +setting_mail_handler_api_enabled: Abilita WS per le e-mail in arrivo +setting_mail_handler_api_key: chiave API +text_email_delivery_not_configured: "La consegna via e-mail non è configurata e le notifiche sono disabilitate.\nConfigura il tuo server SMTP in config/email.yml e riavvia l'applicazione per abilitarle." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/ja.yml b/groups/lang/ja.yml index 680d29836..5a728fb02 100644 --- a/groups/lang/ja.yml +++ b/groups/lang/ja.yml @@ -49,6 +49,7 @@ general_text_no: 'ã„ã„ãˆ' general_text_yes: 'ã¯ã„' general_lang_name: 'Japanese (日本語)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: SJIS general_pdf_encoding: UTF-8 general_day_names: 月曜日,ç«æ›œæ—¥,水曜日,木曜日,金曜日,土曜日,日曜日 @@ -619,3 +620,20 @@ setting_default_projects_public: ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§æ–°ã—ã„プãƒã‚¸ã‚§ã‚¯ãƒˆã error_scm_annotate: "エントリãŒå˜åœ¨ã—ãªã„ã€ã‚‚ã—ãã¯ã‚¢ãƒŽãƒ†ãƒ¼ãƒˆã§ãã¾ã›ã‚“。" label_planning: 計画 text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/ko.yml b/groups/lang/ko.yml index 4281f3881..16bd65364 100644 --- a/groups/lang/ko.yml +++ b/groups/lang/ko.yml @@ -48,6 +48,7 @@ general_text_no: '아니오' general_text_yes: '예' general_lang_name: 'Korean (한êµì–´)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: CP949 general_pdf_encoding: CP949 general_day_names: 월요ì¼,화요ì¼,수요ì¼,목요ì¼,금요ì¼,í† ìš”ì¼,ì¼ìš”ì¼ @@ -618,3 +619,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/lt.yml b/groups/lang/lt.yml index df7cd960b..2a75a95ea 100644 --- a/groups/lang/lt.yml +++ b/groups/lang/lt.yml @@ -48,6 +48,7 @@ general_text_no: 'ne' general_text_yes: 'taip' general_lang_name: 'Lithuanian (lietuvių)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: UTF-8 general_pdf_encoding: UTF-8 general_day_names: pirmadienis,antradienis,treÄiadienis,ketvirtadienis,penktadienis,Å¡eÅ¡tadienis,sekmadienis @@ -554,7 +555,7 @@ enumeration_issue_priorities: Darbo prioritetai enumeration_doc_categories: Dokumento kategorijos enumeration_activities: Veiklos (laiko sekimas) label_display_per_page: '%s įrašų puslapyje' -setting_per_page_options: Objects per page options +setting_per_page_options: Ä®rašų puslapyje nustatimas notice_default_data_loaded: Numatytoji konfiguracija sÄ—kmingai užkrauta. label_age: Amžius label_general: Bendri @@ -578,44 +579,63 @@ label_document_added: Dokumentas pridÄ—tas label_message_posted: PraneÅ¡imas pridÄ—tas label_file_added: Byla pridÄ—ta label_news_added: Naujiena pridÄ—ta -project_module_boards: Boards -project_module_issue_tracking: Issue tracking +project_module_boards: Forumai +project_module_issue_tracking: Darbu pÄ—dsekys project_module_wiki: Wiki -project_module_files: Files -project_module_documents: Documents -project_module_repository: Repository -project_module_news: News -project_module_time_tracking: Time tracking -text_file_repository_writable: File repository writable -text_default_administrator_account_changed: Default administrator account changed -text_rmagick_available: RMagick available (optional) -button_configure: Configure +project_module_files: Rinkmenos +project_module_documents: Dokumentai +project_module_repository: Saugykla +project_module_news: Žinios +project_module_time_tracking: Laiko pÄ—dsekys +text_file_repository_writable: Ä® rinkmenu saugyklÄ… galima saugoti (RW) +text_default_administrator_account_changed: Administratoriaus numatyta paskyra pakeista +text_rmagick_available: RMagick pasiekiamas (pasirinktinai) +button_configure: Konfiguruoti label_plugins: Plugins -label_ldap_authentication: LDAP authentication -label_downloads_abbr: D/L -label_this_month: this month -label_last_n_days: last %d days -label_all_time: all time -label_this_year: this year -label_date_range: Date range -label_last_week: last week -label_yesterday: yesterday -label_last_month: last month -label_add_another_file: Add another file -label_optional_description: Optional description -text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ? -error_issue_not_found_in_project: 'The issue was not found or does not belong to this project' -text_assign_time_entries_to_project: Assign reported hours to the project -text_destroy_time_entries: Delete reported hours -text_reassign_time_entries: 'Reassign reported hours to this issue:' -setting_activity_days_default: Days displayed on project activity -label_chronological_order: In chronological order -field_comments_sorting: Display comments -label_reverse_chronological_order: In reverse chronological order -label_preferences: Preferences -setting_display_subprojects_issues: Display subprojects issues on main projects by default -label_overall_activity: Overall activity -setting_default_projects_public: New projects are public by default -error_scm_annotate: "The entry does not exist or can not be annotated." -label_planning: Planning -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_ldap_authentication: LDAP autentifikacija +label_downloads_abbr: siunt. +label_this_month: Å¡is menuo +label_last_n_days: paskutinių %d dienų +label_all_time: visas laikas +label_this_year: Å¡iemet +label_date_range: Dienų diapazonas +label_last_week: paskutinÄ— savaitÄ— +label_yesterday: vakar +label_last_month: paskutinis menuo +label_add_another_file: PridÄ—ti kitÄ… bylÄ… +label_optional_description: ApibÅ«dinimas (laisvai pasirenkamas) +text_destroy_time_entries_question: Naikinamam darbui paskelbta %.02f valandų. KÄ… jÅ«s noryte su jomis daryti? +error_issue_not_found_in_project: 'Darbas nerastas arba nesuriÅ¡tas su Å¡iuo projektu' +text_assign_time_entries_to_project: Priskirti valandas prie projekto +text_destroy_time_entries: IÅ¡trinti paskelbtas valandas +text_reassign_time_entries: 'Priskirti paskelbtas valandas Å¡iam darbui:' +setting_activity_days_default: Atvaizduojamos dienos projekto veikloje +label_chronological_order: Chronologine tvarka +field_comments_sorting: rodyti komentarus +label_reverse_chronological_order: Atbuline chronologine tvarka +label_preferences: SavybÄ—s +setting_display_subprojects_issues: Pagal nutylÄ—jimÄ… rodyti subprojektų darbus pagrindiniame projekte +label_overall_activity: Visa veikla +setting_default_projects_public: Naujas projektas vieÅ¡as pagal nutylÄ—jimÄ… +error_scm_annotate: "Ä®raÅ¡as neegzituoja arba negalima jo atvaizduoti." +label_planning: Planavimas +text_subprojects_destroy_warning: 'Å is(ie) subprojektas(ai): %s taip pat bus iÅ¡trintas(i).' +label_and_its_subprojects: %s projektas ir jo subprojektai + +mail_body_reminder: "%d darbas(ai), kurie yra jums priskirti, baigiasi po %d dienų(os):" +mail_subject_reminder: "%d darbas(ai) po kelių dienų" +text_user_wrote: '%s parašė:' +label_duplicated_by: susiejo +setting_enabled_scm: Ä®galintas SCM +text_enumeration_category_reassign_to: 'Priskirti juos Å¡iai reikÅ¡mei:' +text_enumeration_destroy_question: '%d objektai priskirti Å¡iai reikÅ¡mei.' +label_incoming_emails: Ä®einantys laiÅ¡kai +label_generate_key: Generuoti raktÄ… +setting_mail_handler_api_enabled: Ä®galinti WS įeinantiems laiÅ¡kams +setting_mail_handler_api_key: API raktas + +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/nl.yml b/groups/lang/nl.yml index e487a7a6d..f79e78994 100644 --- a/groups/lang/nl.yml +++ b/groups/lang/nl.yml @@ -48,6 +48,7 @@ general_text_no: 'nee' general_text_yes: 'ja' general_lang_name: 'Nederlands' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Maandag, Dinsdag, Woensdag, Donderdag, Vrijdag, Zaterdag, Zondag @@ -619,3 +620,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/no.yml b/groups/lang/no.yml index 22b8c10af..6643f9c86 100644 --- a/groups/lang/no.yml +++ b/groups/lang/no.yml @@ -48,6 +48,7 @@ general_text_no: 'nei' general_text_yes: 'ja' general_lang_name: 'Norwegian (Norsk bokmÃ¥l)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Mandag,Tirsdag,Onsdag,Torsdag,Fredag,Lørdag,Søndag @@ -91,6 +92,8 @@ mail_body_account_information_external: Du kan bruke din "%s"-konto for Ã¥ logge mail_body_account_information: Informasjon om din konto mail_subject_account_activation_request: %s kontoaktivering mail_body_account_activation_request: 'En ny bruker (%s) er registrert, og avventer din godkjenning:' +mail_subject_reminder: "%d sak(er) har frist de kommende dagene" +mail_body_reminder: "%d sak(er) som er tildelt deg har frist de kommende %d dager:" gui_validation_error: 1 feil gui_validation_error_plural: %d feil @@ -211,6 +214,7 @@ setting_per_page_options: Alternativer, objekter pr. side setting_user_format: Visningsformat, brukere setting_activity_days_default: Dager vist pÃ¥ prosjektaktivitet setting_display_subprojects_issues: Vis saker fra underprosjekter pÃ¥ hovedprosjekt som standard +setting_enabled_scm: Aktiviserte SCM project_module_issue_tracking: Sakssporing project_module_time_tracking: Tidssporing @@ -291,6 +295,7 @@ label_auth_source: Autentifikasjonsmodus label_auth_source_new: Ny autentifikasjonmodus label_auth_source_plural: Autentifikasjonsmoduser label_subproject_plural: Underprosjekter +label_and_its_subprojects: %s og dets underprosjekter label_min_max_length: Min.-maks. lengde label_list: Liste label_date: Dato @@ -444,7 +449,8 @@ label_loading: Laster... label_relation_new: Ny relasjon label_relation_delete: Slett relasjon label_relates_to: relatert til -label_duplicates: duplikater +label_duplicates: dupliserer +label_duplicated_by: duplisert av label_blocks: blokkerer label_blocked_by: blokkert av label_precedes: kommer før @@ -557,7 +563,7 @@ text_select_mail_notifications: Velg hendelser som skal varsles med e-post. text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 betyr ingen begrensning text_project_destroy_confirmation: Er du sikker pÃ¥ at du vil slette dette prosjekter og alle relatert data ? -text_subprojects_destroy_warning: 'Underprojekt(ene): %s vil ogsÃ¥ bli slettet.' +text_subprojects_destroy_warning: 'Underprojekt(ene): %s vil ogsÃ¥ bli slettet.' text_workflow_edit: Velg en rolle og en sakstype for Ã¥ endre arbeidsflyten text_are_you_sure: Er du sikker ? text_journal_changed: endret fra %s til %s @@ -593,6 +599,7 @@ text_destroy_time_entries_question: %.02f timer er ført pÃ¥ sakene du er i ferd text_destroy_time_entries: Slett førte timer text_assign_time_entries_to_project: Overfør førte timer til prosjektet text_reassign_time_entries: 'Overfør førte timer til denne saken:' +text_user_wrote: '%s skrev:' default_role_manager: Leder default_role_developper: Utvikler @@ -619,3 +626,14 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/pl.yml b/groups/lang/pl.yml index 81f03a62f..2df921b71 100644 --- a/groups/lang/pl.yml +++ b/groups/lang/pl.yml @@ -48,6 +48,7 @@ general_text_no: 'nie' general_text_yes: 'tak' general_lang_name: 'Polski' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-2 general_pdf_encoding: ISO-8859-2 general_day_names: PoniedziaÅ‚ek,Wtorek,Åšroda,Czwartek,PiÄ…tek,Sobota,Niedziela @@ -70,7 +71,7 @@ notice_file_not_found: Strona do której próbujesz siÄ™ dostać nie istnieje lu notice_locking_conflict: Dane poprawione przez innego użytkownika. notice_not_authorized: Nie jesteÅ› autoryzowany by zobaczyć stronÄ™. -error_scm_not_found: "WejÅ›cie i/lub zmiana nie istnieje w repozytorium." +error_scm_not_found: "Obiekt lub wersja nie zostaÅ‚y znalezione w repozytorium." error_scm_command_failed: "An error occurred when trying to access the repository: %s" mail_subject_lost_password: Twoje hasÅ‚o do %s @@ -114,12 +115,12 @@ field_subject: Temat field_due_date: Data oddania field_assigned_to: Przydzielony do field_priority: Priorytet -field_fixed_version: Target version +field_fixed_version: Wersja docelowa field_user: Użytkownik field_role: Rola field_homepage: Strona www field_is_public: Publiczny -field_parent: Podprojekt +field_parent: Nadprojekt field_is_in_chlog: Zagadnienie pokazywane w zapisie zmian field_is_in_roadmap: Zagadnienie pokazywane na mapie field_login: Login @@ -172,10 +173,10 @@ setting_host_name: Nazwa hosta setting_text_formatting: Formatowanie tekstu setting_wiki_compression: Kompresja historii Wiki setting_feeds_limit: Limit danych RSS -setting_autofetch_changesets: Auto-odÅ›wieżanie CVS +setting_autofetch_changesets: Automatyczne pobieranie zmian setting_sys_api_enabled: Włączenie WS do zarzÄ…dzania repozytorium -setting_commit_ref_keywords: Terminy odnoszÄ…ce (CVS) -setting_commit_fix_keywords: Terminy ustalajÄ…ce (CVS) +setting_commit_ref_keywords: SÅ‚owa tworzÄ…ce powiÄ…zania +setting_commit_fix_keywords: SÅ‚owa zmieniajÄ…ce status setting_autologin: Auto logowanie setting_date_format: Format daty @@ -328,14 +329,14 @@ label_repository: Repozytorium label_browse: PrzeglÄ…d label_modification: %d modyfikacja label_modification_plural: %d modyfikacja -label_revision: Zmiana -label_revision_plural: Zmiany +label_revision: Rewizja +label_revision_plural: Rewizje label_added: dodane -label_modified: zmodufikowane +label_modified: zmodyfikowane label_deleted: usuniÄ™te -label_latest_revision: Ostatnia zmiana -label_latest_revision_plural: Ostatnie zmiany -label_view_revisions: Pokaż zmiany +label_latest_revision: Najnowsza rewizja +label_latest_revision_plural: Najnowsze rewizje +label_view_revisions: Pokaż rewizje label_max_size: Maksymalny rozmiar label_on: 'z' label_sort_highest: PrzesuÅ„ na górÄ™ @@ -366,8 +367,8 @@ label_f_hour_plural: %.2f godzin label_time_tracking: Åšledzenie czasu label_change_plural: Zmiany label_statistics: Statystyki -label_commits_per_month: Wrzutek CVS w miesiÄ…cu -label_commits_per_author: Wrzutek CVS przez autora +label_commits_per_month: Zatwierdzenia wedÅ‚ug miesiÄ™cy +label_commits_per_author: Zatwierdzenia wedÅ‚ug autorów label_view_diff: Pokaż różnice label_diff_inline: w linii label_diff_side_by_side: obok siebie @@ -463,13 +464,13 @@ text_length_between: DÅ‚ugość pomiÄ™dzy %d i %d znaków. text_tracker_no_workflow: Brak przepÅ‚ywu zefiniowanego dla tego typu zagadnienia text_unallowed_characters: Niedozwolone znaki text_comma_separated: Wielokrotne wartoÅ›ci dozwolone (rozdzielone przecinkami). -text_issues_ref_in_commit_messages: Zagadnienia odnoszÄ…ce i ustalajÄ…ce we wrzutkach CVS +text_issues_ref_in_commit_messages: OdwoÅ‚ania do zagadnieÅ„ w komentarzach zatwierdzeÅ„ default_role_manager: Kierownik default_role_developper: Programista default_role_reporter: Wprowadzajacy default_tracker_bug: Błąd -default_tracker_feature: Cecha +default_tracker_feature: Zadanie default_tracker_support: Wsparcie default_issue_status_new: Nowy default_issue_status_assigned: Przypisany @@ -483,7 +484,7 @@ default_priority_low: Niski default_priority_normal: Normalny default_priority_high: Wysoki default_priority_urgent: Pilny -default_priority_immediate: Natyczmiastowy +default_priority_immediate: Natychmiastowy default_activity_design: Projektowanie default_activity_development: Rozwój @@ -618,3 +619,20 @@ setting_default_projects_public: Nowe projekty sÄ… domyÅ›lnie publiczne error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/pt-br.yml b/groups/lang/pt-br.yml index 9facd8d19..8cd171b72 100644 --- a/groups/lang/pt-br.yml +++ b/groups/lang/pt-br.yml @@ -1,140 +1,141 @@ _gloc_rule_default: '|n| n==1 ? "" : "_plural" '
actionview_datehelper_select_day_prefix:
-actionview_datehelper_select_month_names: Janeiro,Fevereiro,Marco,Abrill,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro
+actionview_datehelper_select_month_names: Janeiro,Fevereiro,Março,Abrill,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro
actionview_datehelper_select_month_names_abbr: Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez
actionview_datehelper_select_month_prefix:
actionview_datehelper_select_year_prefix:
actionview_datehelper_time_in_words_day: 1 dia
actionview_datehelper_time_in_words_day_plural: %d dias
-actionview_datehelper_time_in_words_hour_about: sobre uma hora
-actionview_datehelper_time_in_words_hour_about_plural: sobra %d horas
-actionview_datehelper_time_in_words_hour_about_single: sobre uma hora
+actionview_datehelper_time_in_words_hour_about: aproximadamente uma hora
+actionview_datehelper_time_in_words_hour_about_plural: aproximadamente %d horas
+actionview_datehelper_time_in_words_hour_about_single: aproximadamente uma hora
actionview_datehelper_time_in_words_minute: 1 minuto
actionview_datehelper_time_in_words_minute_half: meio minuto
-actionview_datehelper_time_in_words_minute_less_than: menos que um minuto
+actionview_datehelper_time_in_words_minute_less_than: menos de um minuto
actionview_datehelper_time_in_words_minute_plural: %d minutos
actionview_datehelper_time_in_words_minute_single: 1 minuto
-actionview_datehelper_time_in_words_second_less_than: menos que um segundo
-actionview_datehelper_time_in_words_second_less_than_plural: menos que %d segundos
+actionview_datehelper_time_in_words_second_less_than: menos de um segundo
+actionview_datehelper_time_in_words_second_less_than_plural: menos de %d segundos
actionview_instancetag_blank_option: Selecione
-activerecord_error_inclusion: nao esta incluido na lista
-activerecord_error_exclusion: esta reservado
-activerecord_error_invalid: e invalido
-activerecord_error_confirmation: confirmacao nao confere
+activerecord_error_inclusion: não está incluso na lista
+activerecord_error_exclusion: está reservado
+activerecord_error_invalid: é inválido
+activerecord_error_confirmation: confirmação não confere
activerecord_error_accepted: deve ser aceito
-activerecord_error_empty: nao pode ser vazio
-activerecord_error_blank: nao pode estar em branco
-activerecord_error_too_long: e muito longo
-activerecord_error_too_short: e muito comprido
-activerecord_error_wrong_length: esta com o comprimento errado
-activerecord_error_taken: ja esta examinado
-activerecord_error_not_a_number: nao e um numero
-activerecord_error_not_a_date: nao e uma data valida
+activerecord_error_empty: não pode ser vazio
+activerecord_error_blank: não pode estar em branco
+activerecord_error_too_long: é muito longo
+activerecord_error_too_short: é muito curto
+activerecord_error_wrong_length: esta com o tamanho errado
+activerecord_error_taken: já foi obtido
+activerecord_error_not_a_number: não é um numero
+activerecord_error_not_a_date: não é uma data valida
activerecord_error_greater_than_start_date: deve ser maior que a data inicial
-activerecord_error_not_same_project: doesn't belong to the same project
-activerecord_error_circular_dependency: This relation would create a circular dependency
+activerecord_error_not_same_project: não pode pertencer ao mesmo projeto
+activerecord_error_circular_dependency: Esta relação geraria uma dependência circular
-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_age: %d ano
+general_fmt_age_plural: %d anos
+general_fmt_date: %%d/%%m/%%Y
+general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p
general_fmt_datetime_short: %%b %%d, %%I:%%M %%p
general_fmt_time: %%I:%%M %%p
-general_text_No: 'Nao'
+general_text_No: 'Não'
general_text_Yes: 'Sim'
-general_text_no: 'nao'
+general_text_no: 'não'
general_text_yes: 'sim'
-general_lang_name: 'Portugues Brasileiro'
+general_lang_name: 'Português(Brasil)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
-general_day_names: Segunda,Terca,Quarta,Quinta,Sexta,Sabado,Domingo
+general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sabado,Domingo
general_first_day_of_week: '1'
notice_account_updated: Conta foi alterada com sucesso.
-notice_account_invalid_creditentials: Usuario ou senha invalido.
-notice_account_password_updated: Senha foi alterada com sucesso.
-notice_account_wrong_password: Senha errada.
-notice_account_register_done: Conta foi criada com sucesso.
-notice_account_unknown_email: Usuario desconhecido.
-notice_can_t_change_password: Esta conta usa autenticacao externa. E impossivel trocar a senha.
-notice_account_lost_email_sent: Um email com instrucoes para escolher uma nova senha foi enviado para voce.
-notice_account_activated: Sua conta foi ativada. Voce pode logar agora
+notice_account_invalid_creditentials: Usuário ou senha inválido.
+notice_account_password_updated: Senha alterada com sucesso.
+notice_account_wrong_password: Senha inválida.
+notice_account_register_done: Conta criada com sucesso.
+notice_account_unknown_email: Usuário desconhecido.
+notice_can_t_change_password: Esta conta usa autenticação externa. E impossÃvel alterar a senha.
+notice_account_lost_email_sent: Um email com instruções para escolher uma nova senha foi enviado para você.
+notice_account_activated: Sua conta foi ativada. Você pode acessá-la agora.
notice_successful_create: Criado com sucesso.
notice_successful_update: Alterado com sucesso.
-notice_successful_delete: Apagado com sucesso.
+notice_successful_delete: ExcluÃdo com sucesso.
notice_successful_connection: Conectado com sucesso.
-notice_file_not_found: A pagina que voce esta tentando acessar nao existe ou foi excluida.
-notice_locking_conflict: Os dados foram atualizados por um outro usuario.
-notice_not_authorized: You are not authorized to access this page.
-notice_email_sent: An email was sent to %s
-notice_email_error: An error occurred while sending mail (%s)
-notice_feeds_access_key_reseted: Your RSS access key was reseted.
+notice_file_not_found: A página que você está tentando acessar não existe ou foi excluÃda.
+notice_locking_conflict: Os dados foram atualizados por outro usuário.
+notice_not_authorized: Você não está autorizado a acessar esta página.
+notice_email_sent: Um email foi enviado para %s
+notice_email_error: Um erro ocorreu ao enviar o email (%s)
+notice_feeds_access_key_reseted: Sua chave RSS foi reconfigurada.
-error_scm_not_found: "A entrada e/ou a revisao nao existem no repositorio."
-error_scm_command_failed: "An error occurred when trying to access the repository: %s"
+error_scm_not_found: "A entrada e/ou a revisão não existe no repositório."
+error_scm_command_failed: "Ocorreu um erro ao tentar acessar o repositório: %s"
mail_subject_lost_password: Sua senha do %s.
mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:'
-mail_subject_register: Ativacao de conta do %s.
+mail_subject_register: Ativação de conta do %s.
mail_body_register: 'Para ativar sua conta, clique no link abaixo:'
gui_validation_error: 1 erro
gui_validation_error_plural: %d erros
field_name: Nome
-field_description: Descricao
-field_summary: Sumario
-field_is_required: Obrigatorio
+field_description: Descrição
+field_summary: Resumo
+field_is_required: Obrigatório
field_firstname: Primeiro nome
-field_lastname: Ultimo nome
+field_lastname: Último nome
field_mail: Email
field_filename: Arquivo
field_filesize: Tamanho
field_downloads: Downloads
field_author: Autor
-field_created_on: Criado
-field_updated_on: Alterado
+field_created_on: Criado em
+field_updated_on: Alterado em
field_field_format: Formato
field_is_for_all: Para todos os projetos
-field_possible_values: Possiveis valores
-field_regexp: Expressao regular
-field_min_length: Tamanho minimo
-field_max_length: Tamanho maximo
+field_possible_values: PossÃveis valores
+field_regexp: Expressão regular
+field_min_length: Tamanho mÃnimo
+field_max_length: Tamanho máximo
field_value: Valor
field_category: Categoria
-field_title: Titulo
+field_title: TÃtulo
field_project: Projeto
-field_issue: Tarefa
+field_issue: Ticket
field_status: Status
field_notes: Notas
-field_is_closed: Tarefa fechada
-field_is_default: Status padrao
+field_is_closed: Ticket fechado
+field_is_default: Status padrão
field_tracker: Tipo
-field_subject: Titulo
-field_due_date: Data devida
-field_assigned_to: Atribuido para
+field_subject: TÃtulo
+field_due_date: Data prevista
+field_assigned_to: AtribuÃdo para
field_priority: Prioridade
-field_fixed_version: Target version
-field_user: Usuario
-field_role: Regra
-field_homepage: Pagina inicial
-field_is_public: Publico
+field_fixed_version: Versão
+field_user: Usuário
+field_role: Papel
+field_homepage: Página inicial
+field_is_public: Público
field_parent: Sub-projeto de
-field_is_in_chlog: Tarefas mostradas no changelog
-field_is_in_roadmap: Tarefas mostradas no roadmap
+field_is_in_chlog: Tarefas exibidas no registro de alterações
+field_is_in_roadmap: Tarefas exibidas no planejamento
field_login: Login
-field_mail_notification: Notificacoes por email
+field_mail_notification: Notificações por email
field_admin: Administrador
-field_last_login_on: Ultima conexao
-field_language: Lingua
+field_last_login_on: Última conexão
+field_language: Idioma
field_effective_date: Data
field_password: Senha
field_new_password: Nova senha
-field_password_confirmation: Confirmacao
-field_version: Versao
+field_password_confirmation: Confirmação
+field_version: Versão
field_type: Tipo
field_host: Servidor
field_port: Porta
@@ -142,116 +143,116 @@ field_account: Conta field_base_dn: Base DN
field_attr_login: Atributo login
field_attr_firstname: Atributo primeiro nome
-field_attr_lastname: Atributo ultimo nome
+field_attr_lastname: Atributo último nome
field_attr_mail: Atributo email
-field_onthefly: Criacao de usuario on-the-fly
-field_start_date: Inicio
+field_onthefly: Criação automática de usuário
+field_start_date: InÃcio
field_done_ratio: %% Terminado
-field_auth_source: Modo de autenticacao
-field_hide_mail: Esconder meu email
-field_comments: Comentario
+field_auth_source: Modo de autenticação
+field_hide_mail: Ocultar meu email
+field_comments: Comentário
field_url: URL
-field_start_page: Pagina inicial
+field_start_page: Página inicial
field_subproject: Sub-projeto
field_hours: Horas
field_activity: Atividade
field_spent_on: Data
field_identifier: Identificador
-field_is_filter: Used as a filter
-field_issue_to_id: Related issue
-field_delay: Delay
-field_assignable: Issues can be assigned to this role
-field_redirect_existing_links: Redirect existing links
-field_estimated_hours: Estimated time
-field_default_value: Padrao
+field_is_filter: É um filtro
+field_issue_to_id: Ticket relacionado
+field_delay: Espera
+field_assignable: Tickets podem ser atribuÃdos para este papel
+field_redirect_existing_links: Redirecionar links existentes
+field_estimated_hours: Tempo estimado
+field_default_value: Padrão
-setting_app_title: Titulo da aplicacao
-setting_app_subtitle: Sub-titulo da aplicacao
-setting_welcome_text: Texto de boa-vinda
-setting_default_language: Lingua padrao
-setting_login_required: Autenticacao obrigatoria
-setting_self_registration: Registro de si mesmo permitido
-setting_attachment_max_size: Tamanho maximo do anexo
-setting_issues_export_limit: Limite de exportacao das tarefas
+setting_app_title: TÃtulo da aplicação
+setting_app_subtitle: Sub-tÃtulo da aplicação
+setting_welcome_text: Texto de boas-vindas
+setting_default_language: Idioma padrão
+setting_login_required: Autenticação obrigatória
+setting_self_registration: Permitido Auto-registro
+setting_attachment_max_size: Tamanho máximo do anexo
+setting_issues_export_limit: Limite de exportação das tarefas
setting_mail_from: Email enviado de
setting_host_name: Servidor
setting_text_formatting: Formato do texto
-setting_wiki_compression: Compactacao do historio do Wiki
+setting_wiki_compression: Compactação de histórico do Wiki
setting_feeds_limit: Limite do Feed
-setting_autofetch_changesets: Autofetch commits
-setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio
-setting_commit_ref_keywords: Referencing keywords
-setting_commit_fix_keywords: Fixing keywords
-setting_autologin: Autologin
-setting_date_format: Date format
-setting_cross_project_issue_relations: Allow cross-project issue relations
+setting_autofetch_changesets: Auto-obter commits
+setting_sys_api_enabled: Ativa WS para gerenciamento do repositório
+setting_commit_ref_keywords: Palavras de referência
+setting_commit_fix_keywords: Palavras de fechamento
+setting_autologin: Auto-login
+setting_date_format: Formato da data
+setting_cross_project_issue_relations: Permitir relacionar tickets entre projetos
-label_user: Usuario
-label_user_plural: Usuarios
-label_user_new: Novo usuario
+label_user: Usuário
+label_user_plural: Usuários
+label_user_new: Novo usuário
label_project: Projeto
label_project_new: Novo projeto
label_project_plural: Projetos
-label_project_all: All Projects
-label_project_latest: Ultimos projetos
-label_issue: Tarefa
-label_issue_new: Nova tarefa
-label_issue_plural: Tarefas
-label_issue_view_all: Ver todas as tarefas
+label_project_all: Todos os projetos
+label_project_latest: Últimos projetos
+label_issue: Ticket
+label_issue_new: Novo ticket
+label_issue_plural: Tickets
+label_issue_view_all: Ver todos os tickets
label_document: Documento
label_document_new: Novo documento
label_document_plural: Documentos
-label_role: Regra
-label_role_plural: Regras
-label_role_new: Nova regra
-label_role_and_permissions: Regras e permissoes
+label_role: Papel
+label_role_plural: Papéis
+label_role_new: Novo papel
+label_role_and_permissions: Papéis e permissões
label_member: Membro
label_member_new: Novo membro
label_member_plural: Membros
-label_tracker: Tipo
-label_tracker_plural: Tipos
+label_tracker: Tipo de ticket
+label_tracker_plural: Tipos de ticket
label_tracker_new: Novo tipo
label_workflow: Workflow
-label_issue_status: Status da tarefa
-label_issue_status_plural: Status das tarefas
+label_issue_status: Status do ticket
+label_issue_status_plural: Status dos tickets
label_issue_status_new: Novo status
-label_issue_category: Categoria de tarefa
-label_issue_category_plural: Categorias de tarefa
+label_issue_category: Categoria de ticket
+label_issue_category_plural: Categorias de tickets
label_issue_category_new: Nova categoria
label_custom_field: Campo personalizado
-label_custom_field_plural: Campos personalizado
+label_custom_field_plural: Campos personalizados
label_custom_field_new: Novo campo personalizado
-label_enumerations: Enumeracao
-label_enumeration_new: Novo valor
-label_information: Informacao
-label_information_plural: Informacoes
-label_please_login: Efetue login
+label_enumerations: 'Tipos & Categorias'
+label_enumeration_new: Novo
+label_information: Informação
+label_information_plural: Informações
+label_please_login: Efetue o login
label_register: Registre-se
-label_password_lost: Perdi a senha
-label_home: Pagina inicial
-label_my_page: Minha pagina
+label_password_lost: Perdi minha senha
+label_home: Página inicial
+label_my_page: Minha página
label_my_account: Minha conta
label_my_projects: Meus projetos
-label_administration: Administracao
-label_login: Login
-label_logout: Logout
+label_administration: Administração
+label_login: Entrar
+label_logout: Sair
label_help: Ajuda
-label_reported_issues: Tarefas reportadas
-label_assigned_to_me_issues: Tarefas atribuidas a mim
-label_last_login: Utima conexao
-label_last_updates: Ultima alteracao
-label_last_updates_plural: %d Ultimas alteracoes
+label_reported_issues: Tickets reportados
+label_assigned_to_me_issues: Meus tickets
+label_last_login: Última conexao
+label_last_updates: Última alteração
+label_last_updates_plural: %d Últimas alterações
label_registered_on: Registrado em
label_activity: Atividade
label_new: Novo
-label_logged_as: Logado como
+label_logged_as: "Acessando como:"
label_environment: Ambiente
-label_authentication: Autenticacao
-label_auth_source: Modo de autenticacao
-label_auth_source_new: Novo modo de autenticacao
-label_auth_source_plural: Modos de autenticacao
+label_authentication: Autenticação
+label_auth_source: Modo de autenticação
+label_auth_source_new: Novo modo de autenticação
+label_auth_source_plural: Modos de autenticação
label_subproject_plural: Sub-projetos
-label_min_max_length: Tamanho min-max
+label_min_max_length: Tamanho mÃn-máx
label_list: Lista
label_date: Data
label_integer: Inteiro
@@ -262,169 +263,169 @@ label_attribute: Atributo label_attribute_plural: Atributos
label_download: %d Download
label_download_plural: %d Downloads
-label_no_data: Sem dados para mostrar
-label_change_status: Mudar status
-label_history: Historico
+label_no_data: Nenhuma informação disponÃvel
+label_change_status: Alterar status
+label_history: Histórico
label_attachment: Arquivo
label_attachment_new: Novo arquivo
label_attachment_delete: Apagar arquivo
label_attachment_plural: Arquivos
-label_report: Relatorio
-label_report_plural: Relatorio
-label_news: Noticias
-label_news_new: Adicionar noticias
-label_news_plural: Noticias
-label_news_latest: Ultimas noticias
-label_news_view_all: Ver todas as noticias
-label_change_log: Change log
-label_settings: Ajustes
-label_overview: Visao geral
-label_version: Versao
-label_version_new: Nova versao
-label_version_plural: Versoes
-label_confirmation: Confirmacao
+label_report: Relatório
+label_report_plural: Relatório
+label_news: NotÃcia
+label_news_new: Adicionar notÃcias
+label_news_plural: NotÃcias
+label_news_latest: Últimas notÃcias
+label_news_view_all: Ver todas as notÃcias
+label_change_log: Registro de alterações
+label_settings: Configurações
+label_overview: Visão geral
+label_version: Versão
+label_version_new: Nova versão
+label_version_plural: Versões
+label_confirmation: Confirmação
label_export_to: Exportar para
label_read: Ler...
-label_public_projects: Projetos publicos
+label_public_projects: Projetos públicos
label_open_issues: Aberto
-label_open_issues_plural: Abertos
+label_open_issues_plural: Abertos
label_closed_issues: Fechado
label_closed_issues_plural: Fechados
label_total: Total
-label_permissions: Permissoes
+label_permissions: Permissões
label_current_status: Status atual
label_new_statuses_allowed: Novo status permitido
label_all: todos
label_none: nenhum
-label_next: Proximo
+label_next: Próximo
label_previous: Anterior
label_used_by: Usado por
label_details: Detalhes
label_add_note: Adicionar nota
-label_per_page: Por pagina
-label_calendar: Calendario
-label_months_from: Meses de
+label_per_page: Por página
+label_calendar: Calendário
+label_months_from: meses a partir de
label_gantt: Gantt
label_internal: Interno
-label_last_changes: utlimas %d mudancas
-label_change_view_all: Mostrar todas as mudancas
-label_personalize_page: Personalizar esta pagina
-label_comment: Comentario
-label_comment_plural: Comentarios
-label_comment_add: Adicionar comentario
-label_comment_added: Comentario adicionado
-label_comment_delete: Apagar comentario
+label_last_changes: últimas %d alteraçoes
+label_change_view_all: Mostrar todas as alteraçoes
+label_personalize_page: Personalizar esta página
+label_comment: Comentário
+label_comment_plural: Comentários
+label_comment_add: Adicionar comentário
+label_comment_added: Comentário adicionado
+label_comment_delete: Apagar comentário
label_query: Consulta personalizada
label_query_plural: Consultas personalizadas
label_query_new: Nova consulta
label_filter_add: Adicionar filtro
label_filter_plural: Filtros
-label_equals: e
-label_not_equals: nao e
-label_in_less_than: e maior que
-label_in_more_than: e menor que
+label_equals: é
+label_not_equals: não é
+label_in_less_than: é maior que
+label_in_more_than: é menor que
label_in: em
label_today: hoje
-label_this_week: this week
+label_this_week: esta semana
label_less_than_ago: faz menos de
label_more_than_ago: faz mais de
-label_ago: dias atras
-label_contains: contem
-label_not_contains: nao contem
+label_ago: dias atrás
+label_contains: contém
+label_not_contains: não contem
label_day_plural: dias
-label_repository: Repository
-label_browse: Browse
-label_modification: %d change
-label_modification_plural: %d changes
-label_revision: Revision
-label_revision_plural: Revisions
-label_added: added
-label_modified: modified
-label_deleted: deleted
-label_latest_revision: Latest revision
-label_latest_revision_plural: Latest revisions
-label_view_revisions: View revisions
-label_max_size: Maximum size
+label_repository: Repositório
+label_browse: Procurar
+label_modification: %d alteração
+label_modification_plural: %d alterações
+label_revision: Revisão
+label_revision_plural: Revisões
+label_added: adicionado
+label_modified: modificado
+label_deleted: excluÃdo
+label_latest_revision: Última revisão
+label_latest_revision_plural: Últimas revisões
+label_view_revisions: Visualizar revisões
+label_max_size: Tamanho máximo
label_on: 'em'
-label_sort_highest: Mover para o inicio
+label_sort_highest: Mover para o inÃcio
label_sort_higher: Mover para cima
label_sort_lower: Mover para baixo
label_sort_lowest: Mover para o fim
-label_roadmap: Roadmap
-label_roadmap_due_in: Due in
-label_roadmap_overdue: %s late
-label_roadmap_no_issues: Sem tarefas para essa versao
+label_roadmap: Planejamento
+label_roadmap_due_in: Previsão em
+label_roadmap_overdue: %s atrasado
+label_roadmap_no_issues: Sem tickets para esta versão
label_search: Busca
label_result_plural: Resultados
label_all_words: Todas as palavras
label_wiki: Wiki
-label_wiki_edit: Wiki edit
-label_wiki_edit_plural: Wiki edits
-label_wiki_page: Wiki page
-label_wiki_page_plural: Wiki pages
-label_index_by_title: Index by title
-label_index_by_date: Index by date
-label_current_version: Versao atual
-label_preview: Previa
+label_wiki_edit: Editar Wiki
+label_wiki_edit_plural: Edições Wiki
+label_wiki_page: Página Wiki
+label_wiki_page_plural: Páginas Wiki
+label_index_by_title: Ãndice por tÃtulo
+label_index_by_date: Ãndice por data
+label_current_version: Versão atual
+label_preview: Pré-visualizar
label_feed_plural: Feeds
-label_changes_details: Detalhes de todas as mudancas
-label_issue_tracking: Tarefas
+label_changes_details: Detalhes de todas as alterações
+label_issue_tracking: Tickets
label_spent_time: Tempo gasto
label_f_hour: %.2f hora
label_f_hour_plural: %.2f horas
label_time_tracking: Tempo trabalhado
-label_change_plural: Mudancas
-label_statistics: Estatisticas
-label_commits_per_month: Commits por mes
+label_change_plural: Mudanças
+label_statistics: EstatÃsticas
+label_commits_per_month: Commits por mês
label_commits_per_author: Commits por autor
-label_view_diff: Ver diferencas
+label_view_diff: Ver diferenças
label_diff_inline: inline
-label_diff_side_by_side: side by side
-label_options: Opcoes
+label_diff_side_by_side: lado a lado
+label_options: Opções
label_copy_workflow_from: Copiar workflow de
-label_permissions_report: Relatorio de permissoes
-label_watched_issues: Watched issues
-label_related_issues: Related issues
-label_applied_status: Applied status
-label_loading: Loading...
-label_relation_new: New relation
-label_relation_delete: Delete relation
-label_relates_to: related to
-label_duplicates: duplicates
-label_blocks: blocks
-label_blocked_by: blocked by
-label_precedes: precedes
-label_follows: follows
-label_end_to_start: end to start
-label_end_to_end: end to end
-label_start_to_start: start to start
-label_start_to_end: start to end
-label_stay_logged_in: Stay logged in
-label_disabled: disabled
-label_show_completed_versions: Show completed versions
-label_me: me
-label_board: Forum
-label_board_new: New forum
-label_board_plural: Forums
-label_topic_plural: Topics
-label_message_plural: Messages
-label_message_last: Last message
-label_message_new: New message
-label_reply_plural: Replies
-label_send_information: Send account information to the user
-label_year: Year
-label_month: Month
-label_week: Week
-label_date_from: From
-label_date_to: To
-label_language_based: Language based
-label_sort_by: Sort by %s
-label_send_test_email: Send a test email
-label_feeds_access_key_created_on: RSS access key created %s ago
-label_module_plural: Modules
-label_added_time_by: Added by %s %s ago
-label_updated_time: Updated %s ago
-label_jump_to_a_project: Jump to a project...
+label_permissions_report: Relatório de permissões
+label_watched_issues: Tickes acompanhados
+label_related_issues: Tickets relacionados
+label_applied_status: Status aplicado
+label_loading: Carregando...
+label_relation_new: Nova relação
+label_relation_delete: Excluir relação
+label_relates_to: relacionado a
+label_duplicates: duplicado de
+label_blocks: bloqueia
+label_blocked_by: bloqueado por
+label_precedes: precede
+label_follows: segue
+label_end_to_start: fim para o inÃcio
+label_end_to_end: fim para fim
+label_start_to_start: inÃcio para inÃcio
+label_start_to_end: inÃcio para fim
+label_stay_logged_in: Permanecer logado
+label_disabled: desabilitado
+label_show_completed_versions: Exibir versões completas
+label_me: eu
+label_board: Fórum
+label_board_new: Novo fórum
+label_board_plural: Fóruns
+label_topic_plural: Tópicos
+label_message_plural: Mensagens
+label_message_last: Última mensagem
+label_message_new: Nova mensagem
+label_reply_plural: Respostas
+label_send_information: Enviar informação de conta para o usuário
+label_year: Ano
+label_month: Mês
+label_week: Semana
+label_date_from: De
+label_date_to: Para
+label_language_based: Com base no idioma
+label_sort_by: Ordenar por %s
+label_send_test_email: Enviar um email de teste
+label_feeds_access_key_created_on: chave de acesso RSS criada %s atrás
+label_module_plural: Módulos
+label_added_time_by: Adicionado por %s %s atrás
+label_updated_time: Atualizado %s atrás
+label_jump_to_a_project: Ir para o projeto...
button_login: Login
button_submit: Enviar
@@ -436,7 +437,7 @@ button_create: Criar button_test: Testar
button_edit: Editar
button_add: Adicionar
-button_change: Mudar
+button_change: Alterar
button_apply: Aplicar
button_clear: Limpar
button_lock: Bloquear
@@ -450,59 +451,59 @@ button_cancel: Cancelar button_activate: Ativar
button_sort: Ordenar
button_log_time: Tempo de trabalho
-button_rollback: Voltar para esta versao
-button_watch: Watch
-button_unwatch: Unwatch
-button_reply: Reply
-button_archive: Archive
-button_unarchive: Unarchive
-button_reset: Reset
-button_rename: Rename
+button_rollback: Voltar para esta versão
+button_watch: Acompanhar
+button_unwatch: Não Acompanhar
+button_reply: Responder
+button_archive: Arquivar
+button_unarchive: Desarquivar
+button_reset: Redefinir
+button_rename: Renomear
status_active: ativo
status_registered: registrado
status_locked: bloqueado
-text_select_mail_notifications: Selecionar acoes para ser enviado uma notificacao por email
-text_regexp_info: eg. ^[A-Z0-9]+$
-text_min_max_length_info: 0 siginifica sem restricao
-text_project_destroy_confirmation: Voce tem certeza que deseja deletar este projeto e todas os dados relacionados?
+text_select_mail_notifications: Selecionar ações para ser enviado uma notificação por email
+text_regexp_info: ex. ^[A-Z0-9]+$
+text_min_max_length_info: 0 siginifica sem restrição
+text_project_destroy_confirmation: Você tem certeza que deseja excluir este projeto e todos os dados relacionados?
text_workflow_edit: Selecione uma regra e um tipo de tarefa para editar o workflow
-text_are_you_sure: Voce tem certeza ?
+text_are_you_sure: Você tem certeza?
text_journal_changed: alterado de %s para %s
text_journal_set_to: setar para %s
text_journal_deleted: apagado
-text_tip_task_begin_day: tarefa comeca neste dia
+text_tip_task_begin_day: tarefa inicia neste dia
text_tip_task_end_day: tarefa termina neste dia
-text_tip_task_begin_end_day: tarefa comeca e termina neste dia
-text_project_identifier_info: 'Letras minusculas (a-z), numeros e tracos permitido.<br />Uma vez salvo, o identificador nao pode ser mudado.'
-text_caracters_maximum: %d maximo de caracteres
+text_tip_task_begin_end_day: tarefa inicia e termina neste dia
+text_project_identifier_info: 'Letras minúsculas (a-z), números e traços permitidos.<br />Uma vez salvo, o identificador não pode ser alterado.'
+text_caracters_maximum: máximo %d caracteres
text_length_between: Tamanho entre %d e %d caracteres.
text_tracker_no_workflow: Sem workflow definido para este tipo.
-text_unallowed_characters: Unallowed characters
-text_comma_separated: Multiple values allowed (comma separated).
-text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
-text_issue_added: Tarefa %s foi incluÃda (by %s).
-text_issue_updated: Tarefa %s foi alterada (by %s).
-text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
-text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ?
-text_issue_category_destroy_assignments: Remove category assignments
-text_issue_category_reassign_to: Reassing issues to this category
+text_unallowed_characters: Caracteres não permitidos
+text_comma_separated: Múltiplos valores são permitidos (separados por vÃrgula).
+text_issues_ref_in_commit_messages: Referenciando e fixando tickets nas mensagens de commit
+text_issue_added: Tarefa %s foi incluÃda (por %s).
+text_issue_updated: Tarefa %s foi alterada (por %s).
+text_wiki_destroy_confirmation: Você tem certeza que deseja excluir este wiki e todo o seu conteúdo?
+text_issue_category_destroy_question: Alguns tickets (%d) estão atribuÃdos a esta categoria. O que você deseja fazer?
+text_issue_category_destroy_assignments: Remover atribuições da categoria
+text_issue_category_reassign_to: Redefinir tickets para esta categoria
-default_role_manager: Analista de Negocio ou Gerente de Projeto
+default_role_manager: Gerente
default_role_developper: Desenvolvedor
-default_role_reporter: Analista de Suporte
-default_tracker_bug: Bug
-default_tracker_feature: Implementacao
+default_role_reporter: Informante
+default_tracker_bug: Problema
+default_tracker_feature: Implementação
default_tracker_support: Suporte
default_issue_status_new: Novo
-default_issue_status_assigned: Atribuido
+default_issue_status_assigned: AtribuÃdo
default_issue_status_resolved: Resolvido
default_issue_status_feedback: Feedback
default_issue_status_closed: Fechado
default_issue_status_rejected: Rejeitado
-default_doc_category_user: Documentacao do usuario
-default_doc_category_tech: Documentacao do tecnica
+default_doc_category_user: Documentação do usuário
+default_doc_category_tech: Documentação técnica
default_priority_low: Baixo
default_priority_normal: Normal
default_priority_high: Alto
@@ -514,107 +515,124 @@ default_activity_development: Desenvolvimento enumeration_issue_priorities: Prioridade das tarefas
enumeration_doc_categories: Categorias de documento
enumeration_activities: Atividades (time tracking)
-label_file_plural: Files
+label_file_plural: Arquivos
label_changeset_plural: Changesets
-field_column_names: Columns
-label_default_columns: Default columns
-setting_issue_list_default_columns: Default columns displayed on the issue list
-setting_repositories_encodings: Repositories encodings
-notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
-label_bulk_edit_selected_issues: Bulk edit selected issues
-label_no_change_option: (No change)
-notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
-label_theme: Theme
-label_default: Default
-label_search_titles_only: Search titles only
-label_nobody: nobody
-button_change_password: Change password
-text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
-label_user_mail_option_selected: "For any event on the selected projects only..."
-label_user_mail_option_all: "For any event on all my projects"
-label_user_mail_option_none: "Only for things I watch or I'm involved in"
-setting_emails_footer: Emails footer
-label_float: Float
-button_copy: Copy
-mail_body_account_information_external: You can use your "%s" account to log in.
-mail_body_account_information: Your account information
-setting_protocol: Protocol
-label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
-setting_time_format: Time format
-label_registration_activation_by_email: account activation by email
-mail_subject_account_activation_request: %s account activation request
-mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:'
-label_registration_automatic_activation: automatic account activation
-label_registration_manual_activation: manual account activation
-notice_account_pending: "Your account was created and is now pending administrator approval."
-field_time_zone: Time zone
-text_caracters_minimum: Must be at least %d characters long.
-setting_bcc_recipients: Blind carbon copy recipients (bcc)
-button_annotate: Annotate
-label_issues_by: Issues by %s
-field_searchable: Searchable
-label_display_per_page: 'Per page: %s'
-setting_per_page_options: Objects per page options
-label_age: Age
-notice_default_data_loaded: Default configuration successfully loaded.
-text_load_default_configuration: Load the default configuration
-text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
-error_can_t_load_default_data: "Default configuration could not be loaded: %s"
-button_update: Update
-label_change_properties: Change properties
-label_general: General
-label_repository_plural: Repositories
-label_associated_revisions: Associated revisions
-setting_user_format: Users display format
-text_status_changed_by_changeset: Applied in changeset %s.
-label_more: More
-text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+field_column_names: Colunas
+label_default_columns: Colunas padrão
+setting_issue_list_default_columns: Colunas padrão visÃveis na lista de tickets
+setting_repositories_encodings: Codificação dos repositórios
+notice_no_issue_selected: "Nenhum ticket está selecionado! Por favor, marque os tickets que você deseja alterar."
+label_bulk_edit_selected_issues: Edição em massa dos tickets selecionados.
+label_no_change_option: (Sem alteração)
+notice_failed_to_save_issues: "Problema ao salvar %d ticket(s) no %d selecionado: %s."
+label_theme: Tema
+label_default: Padrão
+label_search_titles_only: Pesquisar somente tÃtulos
+label_nobody: ninguém
+button_change_password: Alterar senha
+text_user_mail_option: "Para projetos não selecionados, você somente receberá notificações sobre o que você acompanha ou está envolvido (ex. tickets que você é autor ou está atribuÃdo)"
+label_user_mail_option_selected: "Para qualquer evento somente no(s) projeto(s) selecionado(s)..."
+label_user_mail_option_all: "Para qualquer evento em todos os meus projetos"
+label_user_mail_option_none: "Somente eventos que eu acompanho ou estou envolvido"
+setting_emails_footer: Rodapé dos emails
+label_float: Flutuante
+button_copy: Copiar
+mail_body_account_information_external: Você pode usar sua conta "%s" para entrar.
+mail_body_account_information: Informações de sua conta
+setting_protocol: Protocolo
+label_user_mail_no_self_notified: "Eu não desejo ser notificado de minhas próprias modificações"
+setting_time_format: Formato de data
+label_registration_activation_by_email: ativação de conta por email
+mail_subject_account_activation_request: %s requisição de ativação de conta
+mail_body_account_activation_request: 'Um novo usuário (%s) se registrou. A conta está aguardando sua aprovação:'
+label_registration_automatic_activation: ativação automática de conta
+label_registration_manual_activation: ativação manual de conta
+notice_account_pending: "Sua conta foi criada e está aguardando aprovação do administrador."
+field_time_zone: Fuso-horário
+text_caracters_minimum: Precisa ter ao menos %d caracteres.
+setting_bcc_recipients: Destinatários com cópia oculta (cco)
+button_annotate: Anotar
+label_issues_by: Tickets por %s
+field_searchable: Pesquisável
+label_display_per_page: 'Por página: %s'
+setting_per_page_options: Opções de itens por página
+notice_default_data_loaded: Configuração padrão carregada com sucesso.
+text_load_default_configuration: Carregar a configuração padrão
+text_no_configuration_data: "Os Papéis, tipos de tickets, status de tickets e workflows não foram configurados ainda.\nÉ altamente recomendado carregar as configurações padrão. Você poderá modificar estas configurações assim que carregadas."
+error_can_t_load_default_data: "Configuração padrão não pôde ser carregada: %s"
+button_update: Atualizar
+label_change_properties: Alterar propriedades
+label_general: Geral
+label_repository_plural: Repositórios
+label_associated_revisions: Revisões associadas
+setting_user_format: Formato de visualização dos usuários
+text_status_changed_by_changeset: Aplicado no changeset %s.
+label_more: Mais
+text_issues_destroy_confirmation: 'Você tem certeza que deseja excluir o(s) ticket(s) selecionado(s)?'
label_scm: SCM
-text_select_project_modules: 'Select modules to enable for this project:'
-label_issue_added: Issue added
-label_issue_updated: Issue updated
-label_document_added: Document added
-label_message_posted: Message added
-label_file_added: File added
-label_news_added: News added
-project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+text_select_project_modules: 'Selecione módulos para habilitar para este projeto:'
+label_issue_added: Ticket adicionado
+label_issue_updated: Ticket atualizado
+label_document_added: Documento adicionado
+label_message_posted: Mensagem enviada
+label_file_added: Arquivo adicionado
+label_news_added: NotÃcia adicionada
+project_module_boards: Fóruns
+project_module_issue_tracking: Gerenciamento de Tickets
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
-project_module_repository: Repository
-project_module_news: News
-project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
-text_rmagick_available: RMagick available (optional)
-button_configure: Configure
+project_module_files: Arquivos
+project_module_documents: Documentos
+project_module_repository: Repositório
+project_module_news: NotÃcias
+project_module_time_tracking: Gerenciamento de tempo
+text_file_repository_writable: Repositório de arquivos gravável
+text_default_administrator_account_changed: Conta de administrador padrão modificada
+text_rmagick_available: RMagick disponÃvel (opcional)
+button_configure: Configuração
label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
+label_ldap_authentication: autenticação LDAP
label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_this_month: este mês
+label_last_n_days: últimos %d dias
+label_all_time: todo o tempo
+label_this_year: este ano
+label_date_range: Intervalo de datas
+label_last_week: última semana
+label_yesterday: ontem
+label_last_month: último mês
+label_add_another_file: Adicionar outro arquivo
+label_optional_description: Descrição opcional
+text_destroy_time_entries_question: %.02f horas foram reportadas neste ticket que você está excluindo. O que você deseja fazer?
+error_issue_not_found_in_project: 'O ticket não foi encontrado ou não pertence a este projeto'
+text_assign_time_entries_to_project: Atribuir horas reportadas para o projeto
+text_destroy_time_entries: Excluir horas reportadas
+text_reassign_time_entries: 'Redefinir horas reportadas para este ticket:'
+setting_activity_days_default: Dias visualizados na atividade do projeto
+label_chronological_order: Em ordem cronológica
+field_comments_sorting: Visualizar comentários
+label_reverse_chronological_order: Em order cronológica reversa
+label_preferences: Preferências
+setting_display_subprojects_issues: Visualizar tickets dos subprojetos nos projetos principais por padrão
+label_overall_activity: Atividade geral
+setting_default_projects_public: Novos projetos são públicos por padrão
+error_scm_annotate: "Esta entrada não existe ou não pode ser anotada."
+label_planning: Planejamento
+text_subprojects_destroy_warning: 'Seu(s) subprojeto(s): %s também serão excluÃdos.'
+label_age: Age
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/pt.yml b/groups/lang/pt.yml index 6f51c8ed2..5562ca4ac 100644 --- a/groups/lang/pt.yml +++ b/groups/lang/pt.yml @@ -48,6 +48,7 @@ general_text_no: 'não' general_text_yes: 'sim' general_lang_name: 'Português' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sábado,Domingo @@ -489,7 +490,7 @@ text_issue_category_destroy_question: Some issues (%d) are assigned to this cate text_issue_category_destroy_assignments: Remove category assignments text_issue_category_reassign_to: Reassing issues to this category -default_role_manager: Analista de Negócio ou Gerente de Projeto +default_role_manager: Gerente de Projeto default_role_developper: Desenvolvedor default_role_reporter: Analista de Suporte default_tracker_bug: Bug @@ -618,3 +619,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/ro.yml b/groups/lang/ro.yml index 59edfeb70..5bb49ecec 100644 --- a/groups/lang/ro.yml +++ b/groups/lang/ro.yml @@ -48,6 +48,7 @@ general_text_no: 'nu' general_text_yes: 'da' general_lang_name: 'Română' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Luni,Marti,Miercuri,Joi,Vineri,Sambata,Duminica @@ -497,7 +498,7 @@ default_issue_status_new: Nou default_issue_status_assigned: Atribuit default_issue_status_resolved: Rezolvat default_issue_status_feedback: Feedback -default_issue_status_closed: Rezolvat +default_issue_status_closed: Closed default_issue_status_rejected: Respins default_doc_category_user: Documentatie default_doc_category_tech: Documentatie tehnica @@ -618,3 +619,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/ru.yml b/groups/lang/ru.yml index f69009847..01cdcd478 100644 --- a/groups/lang/ru.yml +++ b/groups/lang/ru.yml @@ -48,6 +48,7 @@ general_text_no: 'Ðет' general_text_yes: 'Да' general_lang_name: 'Russian (РуÑÑкий)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: UTF-8 general_pdf_encoding: UTF-8 general_day_names: Понедельник,Вторник,Среда,Четверг,ПÑтница,Суббота,ВоÑкреÑенье @@ -108,7 +109,7 @@ field_author: Ðвтор field_created_on: Создано field_updated_on: Обновлено field_field_format: Формат -field_is_for_all: Ð”Ð»Ñ Ð²Ñех форматов +field_is_for_all: Ð”Ð»Ñ Ð²Ñех проектов field_possible_values: Возможные Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ field_regexp: РегулÑрное выражение field_min_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° @@ -163,7 +164,7 @@ field_comments: Комментарий field_url: URL field_start_page: Ð¡Ñ‚Ð°Ñ€Ñ‚Ð¾Ð²Ð°Ñ Ñтраница field_subproject: Подпроект -field_hours: ЧаÑ(а)(ов) +field_hours: ЧаÑ(а,ов) field_activity: ДеÑтельноÑть field_spent_on: Дата field_identifier: Ун. идентификатор @@ -239,9 +240,9 @@ label_issue_status_new: Ðовый ÑÑ‚Ð°Ñ‚ÑƒÑ label_issue_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ label_issue_category_plural: Категории задачи label_issue_category_new: ÐÐ¾Ð²Ð°Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ -label_custom_field: Поле клиента -label_custom_field_plural: ÐŸÐ¾Ð»Ñ ÐºÐ»Ð¸ÐµÐ½Ñ‚Ð° -label_custom_field_new: Ðовое поле клиента +label_custom_field: ÐаÑтраиваемое поле +label_custom_field_plural: ÐаÑтраиваемые Ð¿Ð¾Ð»Ñ +label_custom_field_new: Ðовое наÑтраиваемое поле label_enumerations: Справочники label_enumeration_new: Ðовое значение label_information: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ @@ -276,7 +277,7 @@ label_min_max_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ - МакÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½ label_list: СпиÑок label_date: Дата label_integer: Целый -label_float: Свободный +label_float: С плавающей точкой label_boolean: ЛогичеÑкий label_string: ТекÑÑ‚ label_text: Длинный текÑÑ‚ @@ -332,13 +333,13 @@ label_internal: Внутренний label_last_changes: менее %d изменений label_change_view_all: ПроÑмотреть вÑе Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ label_personalize_page: ПерÑонализировать данную Ñтраницу -label_comment: Комментировать +label_comment: комментарий label_comment_plural: Комментарии label_comment_add: ОÑтавить комментарий label_comment_added: Добавленный комментарий label_comment_delete: Удалить комментарии -label_query: Ð—Ð°Ð¿Ñ€Ð¾Ñ ÐºÐ»Ð¸ÐµÐ½Ñ‚Ð° -label_query_plural: ЗапроÑÑ‹ клиентов +label_query: Сохраненный Ð·Ð°Ð¿Ñ€Ð¾Ñ +label_query_plural: Сохраненные запроÑÑ‹ label_query_new: Ðовый Ð·Ð°Ð¿Ñ€Ð¾Ñ label_filter_add: Добавить фильтр label_filter_plural: Фильтры @@ -406,7 +407,7 @@ label_diff_side_by_side: Ñ€Ñдом label_options: Опции label_copy_workflow_from: Скопировать поÑледовательноÑть дейÑтвий из label_permissions_report: Отчет о правах доÑтупа -label_watched_issues: ПроÑмотренные задачи +label_watched_issues: ПроÑматриваемые задачи label_related_issues: СвÑзанные задачи label_applied_status: Применимый ÑÑ‚Ð°Ñ‚ÑƒÑ label_loading: Загрузка... @@ -461,7 +462,7 @@ label_user_mail_option_selected: "Ð”Ð»Ñ Ð²Ñех Ñобытий только в label_user_mail_option_none: "Только Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, что Ñ Ð¿Ñ€Ð¾Ñматриваю или в чем Ñ ÑƒÑ‡Ð°Ñтвую" label_user_mail_no_self_notified: "Ðе извещать об изменениÑÑ… которые Ñ Ñделал Ñам" label_registration_activation_by_email: Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ñ‹Ñ… запиÑей по email -label_registration_automatic_activation: автоматичеÑÐºÐ°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡Ñ‚ÐµÐ½Ñ‹Ñ… запиÑей +label_registration_automatic_activation: автоматичеÑÐºÐ°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ñ‹Ñ… запиÑей label_registration_manual_activation: активировать учетные запиÑи вручную label_age: ВозраÑÑ‚ label_change_properties: Изменить ÑвойÑтва @@ -604,7 +605,7 @@ label_date_range: временной интервал label_last_week: поÑледнÑÑ Ð½ÐµÐ´ÐµÐ»ÑŽ label_yesterday: вчера label_last_month: поÑледний меÑÑц -label_add_another_file: Добавить ещё один файл +label_add_another_file: Добавить ещё один файл label_optional_description: ОпиÑание (выборочно) text_destroy_time_entries_question: Ð’Ñ‹ ÑобираетеÑÑŒ удалить %.02f чаÑа(ов) прикрепленных за Ñтой задачей. error_issue_not_found_in_project: Задача не была найдена или не прикреплена к Ñтому проекту @@ -621,4 +622,21 @@ label_overall_activity: Ð¡Ð²Ð¾Ð´Ð½Ð°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð½Ð¾Ñть setting_default_projects_public: Ðовые проекты ÑвлÑÑŽÑ‚ÑÑ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼Ð¸ error_scm_annotate: "Данные отÑутÑтвуют или не могут быть подпиÑаны." label_planning: Планирование -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +text_subprojects_destroy_warning: 'Подпроекты: %s также будут удалены.' +label_and_its_subprojects: %s и вÑе подпроекты +mail_body_reminder: "%d назначенных на Ð²Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡ на Ñледующие %d дней:" +mail_subject_reminder: "%d назначенных на Ð²Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡ в ближайшие дни" +text_user_wrote: '%s напиÑал:' +label_duplicated_by: дублируетÑÑ +setting_enabled_scm: ЗадейÑтвовать SCM +text_enumeration_category_reassign_to: 'Ðазначить им Ñледующее значение:' +text_enumeration_destroy_question: '%d объект(а,ов) ÑвÑзаны Ñ Ñтим значением.' +label_incoming_emails: Приём Ñообщений +label_generate_key: Сгенерировать ключ +setting_mail_handler_api_enabled: Включить веб-ÑÐµÑ€Ð²Ð¸Ñ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñщих Ñообщений +setting_mail_handler_api_key: API ключ +text_email_delivery_not_configured: "Параметры работы Ñ Ð¿Ð¾Ñ‡Ñ‚Ð¾Ð²Ñ‹Ð¼ Ñервером не наÑтроены и Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ email не активна.\nÐаÑтроить параметры Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ SMTP Ñервера вы можете в файле config/email.yml. Ð”Ð»Ñ Ð¿Ñ€Ð¸Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹ перезапуÑтите приложение." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/sr.yml b/groups/lang/sr.yml index d9869c362..566a46d9f 100644 --- a/groups/lang/sr.yml +++ b/groups/lang/sr.yml @@ -48,6 +48,7 @@ general_text_no: 'ne' general_text_yes: 'da' general_lang_name: 'Srpski' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Ponedeljak, Utorak, Sreda, Äetvrtak, Petak, Subota, Nedelja @@ -619,3 +620,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/sv.yml b/groups/lang/sv.yml index c0f691230..4cb1f073b 100644 --- a/groups/lang/sv.yml +++ b/groups/lang/sv.yml @@ -48,6 +48,7 @@ general_text_no: 'nej' general_text_yes: 'ja' general_lang_name: 'Svenska' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: MÃ¥ndag,Tisdag,Onsdag,Torsdag,Fredag,Lördag,Söndag @@ -619,3 +620,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/th.yml b/groups/lang/th.yml new file mode 100644 index 000000000..2c3977de2 --- /dev/null +++ b/groups/lang/th.yml @@ -0,0 +1,641 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: มà¸à¸£à¸²à¸„ม,à¸à¸¸à¸¡à¸ าพันธ์,มีนาคม,เมษายน,พฤษภาคม,มิถุนายน,à¸à¸£à¸à¸Žà¸²à¸„ม,สิงหาคม,à¸à¸±à¸™à¸¢à¸²à¸¢à¸™,ตุลาคม,พฤศจิà¸à¸²à¸¢à¸™,ธันวาคม +actionview_datehelper_select_month_names_abbr: ม.ค.,à¸.พ.,มี.ค.,เม.ย.,พ.ค.,มิ.ย.,à¸.ค.,ส.ค.,à¸.ย.,ต.ค.,พ.ย.,ธ.ค. +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 วัน +actionview_datehelper_time_in_words_day_plural: %d วัน +actionview_datehelper_time_in_words_hour_about: ประมาณ 1 ชั่วโมง +actionview_datehelper_time_in_words_hour_about_plural: ประมาณ %d ชั่วโมง +actionview_datehelper_time_in_words_hour_about_single: ประมาณ 1 ชั่วโมง +actionview_datehelper_time_in_words_minute: 1 นาที +actionview_datehelper_time_in_words_minute_half: ครึ่งนาที +actionview_datehelper_time_in_words_minute_less_than: ไม่ถึงนาที +actionview_datehelper_time_in_words_minute_plural: %d นาที +actionview_datehelper_time_in_words_minute_single: 1 นาที +actionview_datehelper_time_in_words_second_less_than: ไม่ถึงวินาที +actionview_datehelper_time_in_words_second_less_than_plural: ไม่ถึง %d วินาที +actionview_instancetag_blank_option: à¸à¸£à¸¸à¸“าเลืà¸à¸ + +activerecord_error_inclusion: ไม่à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£ +activerecord_error_exclusion: ถูà¸à¸ªà¸‡à¸§à¸™à¹„ว้ +activerecord_error_invalid: ไม่ถูà¸à¸•้à¸à¸‡ +activerecord_error_confirmation: พิมพ์ไม่เหมืà¸à¸™à¹€à¸”ิม +activerecord_error_accepted: ต้à¸à¸‡à¸¢à¸à¸¡à¸£à¸±à¸š +activerecord_error_empty: ต้à¸à¸‡à¹€à¸•ิม +activerecord_error_blank: ต้à¸à¸‡à¹€à¸•ิม +activerecord_error_too_long: ยาวเà¸à¸´à¸™à¹„ป +activerecord_error_too_short: สั้นเà¸à¸´à¸™à¹„ป +activerecord_error_wrong_length: ความยาวไม่ถูà¸à¸•้à¸à¸‡ +activerecord_error_taken: ถูà¸à¹ƒà¸Šà¹‰à¹„ปà¹à¸¥à¹‰à¸§ +activerecord_error_not_a_number: ไม่ใช่ตัวเลข +activerecord_error_not_a_date: ไม่ใช่วันที่ ที่ถูà¸à¸•้à¸à¸‡ +activerecord_error_greater_than_start_date: ต้à¸à¸‡à¸¡à¸²à¸à¸à¸§à¹ˆà¸²à¸§à¸±à¸™à¹€à¸£à¸´à¹ˆà¸¡ +activerecord_error_not_same_project: ไม่ได้à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¹‚ครงà¸à¸²à¸£à¹€à¸”ียวà¸à¸±à¸™ +activerecord_error_circular_dependency: ความสัมพันธ์à¸à¹‰à¸²à¸‡à¸à¸´à¸‡à¹€à¸›à¹‡à¸™à¸§à¸‡à¸à¸¥à¸¡ + +general_fmt_age: %d ปี +general_fmt_age_plural: %d ปี +general_fmt_date: %%d/%%B/%%Y +general_fmt_datetime: %%d/%%B/%%Y %%H:%%M +general_fmt_datetime_short: %%d %%b, %%H:%%M +general_fmt_time: %%H:%%M +general_text_No: 'ไม่' +general_text_Yes: 'ใช่' +general_text_no: 'ไม่' +general_text_yes: 'ใช่' +general_lang_name: 'Thai (ไทย)' +general_csv_separator: ',' +general_csv_decimal_separator: '.' +general_csv_encoding: Windows-874 +general_pdf_encoding: cp874 +general_day_names: จันทร์,à¸à¸±à¸‡à¸„าร,พุธ,พฤหัสบดี,ศุà¸à¸£à¹Œ,เสาร์,à¸à¸²à¸—ิตย์ +general_first_day_of_week: '1' + +notice_account_updated: บัà¸à¸Šà¸µà¹„ด้ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹à¸¥à¹‰à¸§. +notice_account_invalid_creditentials: ชื้ผู้ใช้หรืà¸à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™à¹„ม่ถูà¸à¸•้à¸à¸‡ +notice_account_password_updated: รหัสได้ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹à¸¥à¹‰à¸§. +notice_account_wrong_password: รหัสผ่านไม่ถูà¸à¸•้à¸à¸‡ +notice_account_register_done: บัà¸à¸Šà¸µà¸–ูà¸à¸ªà¸£à¹‰à¸²à¸‡à¹à¸¥à¹‰à¸§. à¸à¸£à¸¸à¸“าเช็คเมล์ à¹à¸¥à¹‰à¸§à¸„ลิ๊à¸à¸—ี่ลิงค์ในà¸à¸µà¹€à¸¡à¸¥à¹Œà¹€à¸žà¸·à¹ˆà¸à¹€à¸›à¸´à¸”ใช้บัà¸à¸Šà¸µ +notice_account_unknown_email: ไม่มีผู้ใช้ที่ใช้à¸à¸µà¹€à¸¡à¸¥à¹Œà¸™à¸µà¹‰. +notice_can_t_change_password: บัà¸à¸Šà¸µà¸™à¸µà¹‰à¹ƒà¸Šà¹‰à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนจาà¸à¹à¸«à¸¥à¹ˆà¸‡à¸ ายนà¸à¸. ไม่สามารถปลี่ยนรหัสผ่านได้. +notice_account_lost_email_sent: เราได้ส่งà¸à¸µà¹€à¸¡à¸¥à¹Œà¸žà¸£à¹‰à¸à¸¡à¸§à¸´à¸˜à¸µà¸à¸²à¸£à¸ªà¸£à¹‰à¸²à¸‡à¸£à¸«à¸±à¸µà¸ªà¸œà¹ˆà¸²à¸™à¹ƒà¸«à¸¡à¹ˆà¹ƒà¸«à¹‰à¸„ุณà¹à¸¥à¹‰à¸§ à¸à¸£à¸¸à¸“าเช็คเมล์. +notice_account_activated: บัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณได้เปิดใช้à¹à¸¥à¹‰à¸§. ตà¸à¸™à¸™à¸µà¹‰à¸„ุณสามารถเข้าสู่ระบบได้à¹à¸¥à¹‰à¸§. +notice_successful_create: สร้างเสร็จà¹à¸¥à¹‰à¸§. +notice_successful_update: ปรับปรุงเสร็จà¹à¸¥à¹‰à¸§. +notice_successful_delete: ลบเสร็จà¹à¸¥à¹‰à¸§. +notice_successful_connection: ติดต่à¸à¸ªà¸³à¹€à¸£à¹‡à¸ˆà¹à¸¥à¹‰à¸§. +notice_file_not_found: หน้าที่คุณต้à¸à¸‡à¸à¸²à¸£à¸”ูไม่มีà¸à¸¢à¸¹à¹ˆà¸ˆà¸£à¸´à¸‡ หรืà¸à¸–ูà¸à¸¥à¸šà¹„ปà¹à¸¥à¹‰à¸§. +notice_locking_conflict: ข้à¸à¸¡à¸¹à¸¥à¸–ูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹‚ดยผู้ใช้คนà¸à¸·à¹ˆà¸™. +notice_not_authorized: คุณไม่มีสิทธิเข้าถึงหน้านี้. +notice_email_sent: à¸à¸µà¹€à¸¡à¸¥à¹Œà¹„ด้ถูà¸à¸ªà¹ˆà¸‡à¸–ึง %s +notice_email_error: เà¸à¸´à¸”ความผิดพลาดขณะà¸à¸³à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥à¹Œ (%s) +notice_feeds_access_key_reseted: RSS access key ขà¸à¸‡à¸„ุณถูภreset à¹à¸¥à¹‰à¸§. +notice_failed_to_save_issues: "%d ปัà¸à¸«à¸²à¸ˆà¸²à¸ %d ปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸à¸à¹„ม่สามารถจัดเà¸à¹‡à¸š: %s." +notice_no_issue_selected: "ไม่มีปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸à¸! à¸à¸£à¸¸à¸“าเลืà¸à¸à¸›à¸±à¸à¸«à¸²à¸—ี่คุณต้à¸à¸‡à¸à¸²à¸£à¹à¸à¹‰à¹„ข." +notice_account_pending: "บัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณสร้างเสร็จà¹à¸¥à¹‰à¸§ ขณะนี้รà¸à¸à¸²à¸£à¸à¸™à¸¸à¸¡à¸±à¸•ิจาà¸à¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£." +notice_default_data_loaded: ค่าเริ่มต้นโหลดเสร็จà¹à¸¥à¹‰à¸§. + +error_can_t_load_default_data: "ค่าเริ่มต้นโหลดไม่สำเร็จ: %s" +error_scm_not_found: "ไม่พบรุ่นที่ต้à¸à¸‡à¸à¸²à¸£à¹ƒà¸™à¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ." +error_scm_command_failed: "เà¸à¸´à¸”ความผิดพลาดในà¸à¸²à¸£à¹€à¸‚้าถึงà¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ: %s" +error_scm_annotate: "entry ไม่มีà¸à¸¢à¸¹à¹ˆà¸ˆà¸£à¸´à¸‡ หรืà¸à¹„ม่สามารถเขียนหมายเหตุประà¸à¸à¸š." +error_issue_not_found_in_project: 'ไม่พบปัà¸à¸«à¸²à¸™à¸µà¹‰ หรืà¸à¸›à¸±à¸à¸«à¸²à¹„ม่ได้à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¹‚ครงà¸à¸²à¸£à¸™à¸µà¹‰' + +mail_subject_lost_password: รหัสผ่าน %s ขà¸à¸‡à¸„ุณ +mail_body_lost_password: 'คลิ๊à¸à¸—ี่ลิงค์ต่à¸à¹„ปนี้เพื่à¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™:' +mail_subject_register: เปิดบัà¸à¸Šà¸µ %s ขà¸à¸‡à¸„ุณ +mail_body_register: 'คลิ๊à¸à¸—ี่ลิงค์ต่à¸à¹„ปนี้เพื่à¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™:' +mail_body_account_information_external: คุณสามารถใช้บัà¸à¸Šà¸µ "%s" เพื่à¸à¹€à¸‚้าสู่ระบบ. +mail_body_account_information: ข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณ +mail_subject_account_activation_request: à¸à¸£à¸¸à¸“าเปิดบัà¸à¸Šà¸µ %s +mail_body_account_activation_request: 'ผู้ใช้ใหม่ (%s) ได้ลงทะเบียน. บัà¸à¸Šà¸µà¸‚à¸à¸‡à¹€à¸‚าà¸à¸³à¸¥à¸±à¸‡à¸£à¸à¸à¸™à¸¸à¸¡à¸±à¸•ิ:' + +gui_validation_error: 1 ข้à¸à¸œà¸´à¸”พลาด +gui_validation_error_plural: %d ข้à¸à¸œà¸´à¸”พลาด + +field_name: ชื่ภ+field_description: รายละเà¸à¸µà¸¢à¸” +field_summary: สรุปย่ภ+field_is_required: ต้à¸à¸‡à¹ƒà¸ªà¹ˆ +field_firstname: ชื่ภ+field_lastname: นามสà¸à¸¸à¸¥ +field_mail: à¸à¸µà¹€à¸¡à¸¥à¹Œ +field_filename: à¹à¸Ÿà¹‰à¸¡ +field_filesize: ขนาด +field_downloads: ดาวน์โหลด +field_author: ผู้à¹à¸•่ง +field_created_on: สร้าง +field_updated_on: ปรับปรุง +field_field_format: รูปà¹à¸šà¸š +field_is_for_all: สำหรับทุà¸à¹‚ครงà¸à¸²à¸£ +field_possible_values: ค่าที่เป็นไปได้ +field_regexp: Regular expression +field_min_length: สั้นสุด +field_max_length: ยาวสุด +field_value: ค่า +field_category: ประเภท +field_title: ชื่à¸à¹€à¸£à¸·à¹ˆà¸à¸‡ +field_project: โครงà¸à¸²à¸£ +field_issue: ปัà¸à¸«à¸² +field_status: สถานะ +field_notes: บันทึภ+field_is_closed: ปัà¸à¸«à¸²à¸ˆà¸š +field_is_default: ค่าเริ่มต้น +field_tracker: à¸à¸²à¸£à¸•ิดตาม +field_subject: เรื่à¸à¸‡ +field_due_date: วันครบà¸à¸³à¸«à¸™à¸” +field_assigned_to: มà¸à¸šà¸«à¸¡à¸²à¸¢à¹ƒà¸«à¹‰ +field_priority: ความสำคัภ+field_fixed_version: รุ่น +field_user: ผู้ใช้ +field_role: บทบาท +field_homepage: หน้าà¹à¸£à¸ +field_is_public: สาธารณะ +field_parent: โครงà¸à¸²à¸£à¸¢à¹ˆà¸à¸¢à¸‚à¸à¸‡ +field_is_in_chlog: ปัà¸à¸«à¸²à¹à¸ªà¸”งใน รายà¸à¸²à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ +field_is_in_roadmap: ปัà¸à¸«à¸²à¹à¸ªà¸”งใน à¹à¸œà¸™à¸‡à¸²à¸™ +field_login: ชื่à¸à¸—ี่ใช้เข้าระบบ +field_mail_notification: à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ืà¸à¸™à¸—างà¸à¸µà¹€à¸¡à¸¥à¹Œ +field_admin: ผู้บริหารจัดà¸à¸²à¸£ +field_last_login_on: เข้าระบบครั้งสุดท้าย +field_language: ภาษา +field_effective_date: วันที่ +field_password: รหัสผ่าน +field_new_password: รหัสผ่านใหม่ +field_password_confirmation: ยืนยันรหัสผ่าน +field_version: รุ่น +field_type: ชนิด +field_host: โฮสต์ +field_port: พà¸à¸£à¹Œà¸• +field_account: บัà¸à¸Šà¸µ +field_base_dn: Base DN +field_attr_login: เข้าระบบ attribute +field_attr_firstname: ชื่ภattribute +field_attr_lastname: นามสà¸à¸¸à¸¥ attribute +field_attr_mail: à¸à¸µà¹€à¸¡à¸¥à¹Œ attribute +field_onthefly: สร้างผู้ใช้ทันที +field_start_date: เริ่ม +field_done_ratio: %% สำเร็จ +field_auth_source: วิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน +field_hide_mail: ซ่à¸à¸™à¸à¸µà¹€à¸¡à¸¥à¹Œà¸‚à¸à¸‡à¸‰à¸±à¸™ +field_comments: ความเห็น +field_url: URL +field_start_page: หน้าเริ่มต้น +field_subproject: โครงà¸à¸²à¸£à¸¢à¹ˆà¸à¸¢ +field_hours: ชั่วโมง +field_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ +field_spent_on: วันที่ +field_identifier: ชื่à¸à¹€à¸‰à¸žà¸²à¸° +field_is_filter: ใช้เป็นตัวà¸à¸£à¸à¸‡ +field_issue_to_id: ปัà¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡ +field_delay: เลื่à¸à¸™ +field_assignable: ปัà¸à¸«à¸²à¸ªà¸²à¸¡à¸²à¸£à¸–มà¸à¸šà¸«à¸¡à¸²à¸¢à¹ƒà¸«à¹‰à¸„นที่ทำบทบาทนี้ +field_redirect_existing_links: ย้ายจุดเชื่à¸à¸¡à¹‚ยงนี้ +field_estimated_hours: เวลาที่ใช้โดยประมาณ +field_column_names: สดมภ์ +field_time_zone: ย่านเวลา +field_searchable: ค้นหาได้ +field_default_value: ค่าเริ่มต้น +field_comments_sorting: à¹à¸ªà¸”งความเห็น + +setting_app_title: ชื่à¸à¹‚ปรà¹à¸à¸£à¸¡ +setting_app_subtitle: ชื่à¸à¹‚ปรà¹à¸à¸£à¸¡à¸£à¸à¸‡ +setting_welcome_text: ข้à¸à¸„วามต้à¸à¸™à¸£à¸±à¸š +setting_default_language: ภาษาเริ่มต้น +setting_login_required: ต้à¸à¸‡à¸›à¹‰à¸à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰-รหัสผ่าน +setting_self_registration: ลงทะเบียนด้วยตนเà¸à¸‡ +setting_attachment_max_size: ขนาดà¹à¸Ÿà¹‰à¸¡à¹à¸™à¸šà¸ªà¸¹à¸‡à¸ªà¸¸à¸” +setting_issues_export_limit: à¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸à¸à¸›à¸±à¸à¸«à¸²à¸ªà¸¹à¸‡à¸ªà¸¸à¸” +setting_mail_from: à¸à¸µà¹€à¸¡à¸¥à¹Œà¸—ี่ใช้ส่ง +setting_bcc_recipients: ไม่ระบุชื่à¸à¸œà¸¹à¹‰à¸£à¸±à¸š (bcc) +setting_host_name: ชื่à¸à¹‚ฮสต์ +setting_text_formatting: à¸à¸²à¸£à¸ˆà¸±à¸”รูปà¹à¸šà¸šà¸‚้à¸à¸„วาม +setting_wiki_compression: บีบà¸à¸±à¸”ประวัติ Wiki +setting_feeds_limit: จำนวน Feed +setting_default_projects_public: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆà¸¡à¸µà¸„่าเริ่มต้นเป็น สาธารณะ +setting_autofetch_changesets: ดึง commits à¸à¸±à¸•โนมัติ +setting_sys_api_enabled: เปิดใช้ WS สำหรับà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸—ี่เà¸à¹‡à¸šà¸•้นฉบับ +setting_commit_ref_keywords: คำสำคัภReferencing +setting_commit_fix_keywords: คำสำคัภFixing +setting_autologin: เข้าระบบà¸à¸±à¸•โนมัติ +setting_date_format: รูปà¹à¸šà¸šà¸§à¸±à¸™à¸—ี่ +setting_time_format: รูปà¹à¸šà¸šà¹€à¸§à¸¥à¸² +setting_cross_project_issue_relations: à¸à¸™à¸¸à¸à¸²à¸•ให้ระบุปัà¸à¸«à¸²à¸‚้ามโครงà¸à¸²à¸£ +setting_issue_list_default_columns: สดมภ์เริ่มต้นà¹à¸ªà¸”งในรายà¸à¸²à¸£à¸›à¸±à¸à¸«à¸² +setting_repositories_encodings: à¸à¸²à¸£à¹€à¸‚้ารหัสที่เà¸à¹‡à¸šà¸•้นฉบับ +setting_emails_footer: คำลงท้ายà¸à¸µà¹€à¸¡à¸¥à¹Œ +setting_protocol: Protocol +setting_per_page_options: ตัวเลืà¸à¸à¸ˆà¸³à¸™à¸§à¸™à¸•่à¸à¸«à¸™à¹‰à¸² +setting_user_format: รูปà¹à¸šà¸šà¸à¸²à¸£à¹à¸ªà¸”งชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰ +setting_activity_days_default: จำนวนวันที่à¹à¸ªà¸”งในà¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸‚à¸à¸‡à¹‚ครงà¸à¸²à¸£ +setting_display_subprojects_issues: à¹à¸ªà¸”งปัà¸à¸«à¸²à¸‚à¸à¸‡à¹‚ครงà¸à¸²à¸£à¸¢à¹ˆà¸à¸¢à¹ƒà¸™à¹‚ครงà¸à¸²à¸£à¸«à¸¥à¸±à¸ + +project_module_issue_tracking: à¸à¸²à¸£à¸•ิดตามปัà¸à¸«à¸² +project_module_time_tracking: à¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸² +project_module_news: ข่าว +project_module_documents: เà¸à¸à¸ªà¸²à¸£ +project_module_files: à¹à¸Ÿà¹‰à¸¡ +project_module_wiki: Wiki +project_module_repository: ที่เà¸à¹‡à¸šà¸•้นฉบับ +project_module_boards: à¸à¸£à¸°à¸”านข้à¸à¸„วาม + +label_user: ผู้ใช้ +label_user_plural: ผู้ใช้ +label_user_new: ผู้ใช้ใหม่ +label_project: โครงà¸à¸²à¸£ +label_project_new: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆ +label_project_plural: โครงà¸à¸²à¸£ +label_project_all: โครงà¸à¸²à¸£à¸—ั้งหมด +label_project_latest: โครงà¸à¸²à¸£à¸¥à¹ˆà¸²à¸ªà¸¸à¸” +label_issue: ปัà¸à¸«à¸² +label_issue_new: ปัà¸à¸«à¸²à¹ƒà¸«à¸¡à¹ˆ +label_issue_plural: ปัà¸à¸«à¸² +label_issue_view_all: ดูปัà¸à¸«à¸²à¸—ั้งหมด +label_issues_by: ปัà¸à¸«à¸²à¹‚ดย %s +label_issue_added: ปัà¸à¸«à¸²à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡ +label_issue_updated: ปัà¸à¸«à¸²à¸–ูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡ +label_document: เà¸à¸à¸ªà¸²à¸£ +label_document_new: เà¸à¸à¸ªà¸²à¸£à¹ƒà¸«à¸¡à¹ˆ +label_document_plural: เà¸à¸à¸ªà¸²à¸£ +label_document_added: เà¸à¸à¸ªà¸²à¸£à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡ +label_role: บทบาท +label_role_plural: บทบาท +label_role_new: บทบาทใหม่ +label_role_and_permissions: บทบาทà¹à¸¥à¸°à¸ªà¸´à¸—ธิ +label_member: สมาชิภ+label_member_new: สมาชิà¸à¹ƒà¸«à¸¡à¹ˆ +label_member_plural: สมาชิภ+label_tracker: à¸à¸²à¸£à¸•ิดตาม +label_tracker_plural: à¸à¸²à¸£à¸•ิดตาม +label_tracker_new: à¸à¸²à¸£à¸•ิดตามใหม่ +label_workflow: ลำดับงาน +label_issue_status: สถานะขà¸à¸‡à¸›à¸±à¸à¸«à¸² +label_issue_status_plural: สถานะขà¸à¸‡à¸›à¸±à¸à¸«à¸² +label_issue_status_new: สถานะใหม +label_issue_category: ประเภทขà¸à¸‡à¸›à¸±à¸à¸«à¸² +label_issue_category_plural: ประเภทขà¸à¸‡à¸›à¸±à¸à¸«à¸² +label_issue_category_new: ประเภทใหม่ +label_custom_field: เขตข้à¸à¸¡à¸¹à¸¥à¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸à¸‡ +label_custom_field_plural: เขตข้à¸à¸¡à¸¹à¸¥à¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸à¸‡ +label_custom_field_new: สร้างเขตข้à¸à¸¡à¸¹à¸¥à¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸à¸‡ +label_enumerations: รายà¸à¸²à¸£ +label_enumeration_new: สร้างใหม่ +label_information: ข้à¸à¸¡à¸¹à¸¥ +label_information_plural: ข้à¸à¸¡à¸¹à¸¥ +label_please_login: à¸à¸£à¸¸à¸“าเข้าระบบà¸à¹ˆà¸à¸™ +label_register: ลงทะเบียน +label_password_lost: ลืมรหัสผ่าน +label_home: หน้าà¹à¸£à¸ +label_my_page: หน้าขà¸à¸‡à¸‰à¸±à¸™ +label_my_account: บัà¸à¸Šà¸µà¸‚à¸à¸‡à¸‰à¸±à¸™ +label_my_projects: โครงà¸à¸²à¸£à¸‚à¸à¸‡à¸‰à¸±à¸™ +label_administration: บริหารจัดà¸à¸²à¸£ +label_login: เข้าระบบ +label_logout: à¸à¸à¸à¸£à¸°à¸šà¸š +label_help: ช่วยเหลืภ+label_reported_issues: ปัà¸à¸«à¸²à¸—ี่à¹à¸ˆà¹‰à¸‡à¹„ว้ +label_assigned_to_me_issues: ปัà¸à¸«à¸²à¸—ี่มà¸à¸šà¸«à¸¡à¸²à¸¢à¹ƒà¸«à¹‰à¸‰à¸±à¸™ +label_last_login: ติดต่à¸à¸„รั้งสุดท้าย +label_last_updates: ปรับปรุงครั้งสุดท้าย +label_last_updates_plural: %d ปรับปรุงครั้งสุดท้าย +label_registered_on: ลงทะเบียนเมื่ภ+label_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ +label_activity_plural: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ +label_activity_latest: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸¥à¹ˆà¸²à¸ªà¸¸à¸” +label_overall_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¹‚ดยรวม +label_new: ใหม่ +label_logged_as: เข้าระบบในชื่ภ+label_environment: สภาพà¹à¸§à¸”ล้à¸à¸¡ +label_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน +label_auth_source: วิธีà¸à¸²à¸£à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน +label_auth_source_new: สร้างวิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนใหม่ +label_auth_source_plural: วิธีà¸à¸²à¸£ Authentication +label_subproject_plural: โครงà¸à¸²à¸£à¸¢à¹ˆà¸à¸¢ +label_min_max_length: สั้น-ยาว สุดที่ +label_list: รายà¸à¸²à¸£ +label_date: วันที่ +label_integer: จำนวนเต็ม +label_float: จำนวนจริง +label_boolean: ถูà¸à¸œà¸´à¸” +label_string: ข้à¸à¸„วาม +label_text: ข้à¸à¸„วามขนาดยาว +label_attribute: คุณลัà¸à¸©à¸“ะ +label_attribute_plural: คุณลัà¸à¸©à¸“ะ +label_download: %d ดาวน์โหลด +label_download_plural: %d ดาวน์โหลด +label_no_data: จำนวนข้à¸à¸¡à¸¹à¸¥à¸—ี่à¹à¸ªà¸”ง +label_change_status: เปลี่ยนสถานะ +label_history: ประวัติ +label_attachment: à¹à¸Ÿà¹‰à¸¡ +label_attachment_new: à¹à¸Ÿà¹‰à¸¡à¹ƒà¸«à¸¡à¹ˆ +label_attachment_delete: ลบà¹à¸Ÿà¹‰à¸¡ +label_attachment_plural: à¹à¸Ÿà¹‰à¸¡ +label_file_added: à¹à¸Ÿà¹‰à¸¡à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡ +label_report: รายงาน +label_report_plural: รายงาน +label_news: ข่าว +label_news_new: เพิ่มข่าว +label_news_plural: ข่าว +label_news_latest: ข่าวล่าสุด +label_news_view_all: ดูข่าวทั้งหมด +label_news_added: ข่าวถูà¸à¹€à¸žà¸´à¹ˆà¸¡ +label_change_log: บันทึà¸à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ +label_settings: ปรับà¹à¸•่ง +label_overview: ภาพรวม +label_version: รุ่น +label_version_new: รุ่นใหม่ +label_version_plural: รุ่น +label_confirmation: ยืนยัน +label_export_to: 'รูปà¹à¸šà¸šà¸à¸·à¹ˆà¸™à¹† :' +label_read: à¸à¹ˆà¸²à¸™... +label_public_projects: โครงà¸à¸²à¸£à¸ªà¸²à¸˜à¸²à¸£à¸“ะ +label_open_issues: เปิด +label_open_issues_plural: เปิด +label_closed_issues: ปิด +label_closed_issues_plural: ปิด +label_total: จำนวนรวม +label_permissions: สิทธิ +label_current_status: สถานะปัจจุบัน +label_new_statuses_allowed: à¸à¸™à¸¸à¸à¸²à¸•ให้มีสถานะใหม่ +label_all: ทั้งหมด +label_none: ไม่มี +label_nobody: ไม่มีใคร +label_next: ต่à¸à¹„ป +label_previous: à¸à¹ˆà¸à¸™à¸«à¸™à¹‰à¸² +label_used_by: ถูà¸à¹ƒà¸Šà¹‰à¹‚ดย +label_details: รายละเà¸à¸µà¸¢à¸” +label_add_note: เพิ่มบันทึภ+label_per_page: ต่à¸à¸«à¸™à¹‰à¸² +label_calendar: ปà¸à¸´à¸—ิน +label_months_from: เดืà¸à¸™à¸ˆà¸²à¸ +label_gantt: Gantt +label_internal: ภายใน +label_last_changes: last %d เปลี่ยนà¹à¸›à¸¥à¸‡ +label_change_view_all: ดูà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด +label_personalize_page: ปรับà¹à¸•่งหน้านี้ +label_comment: ความเห็น +label_comment_plural: ความเห็น +label_comment_add: เพิ่มความเห็น +label_comment_added: ความเห็นถูà¸à¹€à¸žà¸´à¹ˆà¸¡ +label_comment_delete: ลบความเห็น +label_query: à¹à¸šà¸šà¸ªà¸à¸šà¸–ามà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เà¸à¸‡ +label_query_plural: à¹à¸šà¸šà¸ªà¸à¸šà¸–ามà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เà¸à¸‡ +label_query_new: à¹à¸šà¸šà¸ªà¸à¸šà¸–ามใหม่ +label_filter_add: เพิ่มตัวà¸à¸£à¸à¸‡ +label_filter_plural: ตัวà¸à¸£à¸à¸‡ +label_equals: คืภ+label_not_equals: ไม่ใช่ +label_in_less_than: น้à¸à¸¢à¸à¸§à¹ˆà¸² +label_in_more_than: มาà¸à¸à¸§à¹ˆà¸² +label_in: ในช่วง +label_today: วันนี้ +label_all_time: ตลà¸à¸”เวลา +label_yesterday: เมื่à¸à¸§à¸²à¸™ +label_this_week: à¸à¸²à¸—ิตย์นี้ +label_last_week: à¸à¸²à¸—ิตย์ที่à¹à¸¥à¹‰à¸§ +label_last_n_days: %d วันย้à¸à¸™à¸«à¸¥à¸±à¸‡ +label_this_month: เดืà¸à¸™à¸™à¸µà¹‰ +label_last_month: เดืà¸à¸™à¸—ี่à¹à¸¥à¹‰à¸§ +label_this_year: ปีนี้ +label_date_range: ช่วงวันที่ +label_less_than_ago: น้à¸à¸¢à¸à¸§à¹ˆà¸²à¸«à¸™à¸¶à¹ˆà¸‡à¸§à¸±à¸™ +label_more_than_ago: มาà¸à¸à¸§à¹ˆà¸²à¸«à¸™à¸¶à¹ˆà¸‡à¸§à¸±à¸™ +label_ago: วันผ่านมาà¹à¸¥à¹‰à¸§ +label_contains: มี... +label_not_contains: ไม่มี... +label_day_plural: วัน +label_repository: ที่เà¸à¹‡à¸šà¸•้นฉบับ +label_repository_plural: ที่เà¸à¹‡à¸šà¸•้นฉบับ +label_browse: เปิดหา +label_modification: %d เปลี่ยนà¹à¸›à¸¥à¸‡ +label_modification_plural: %d เปลี่ยนà¹à¸›à¸¥à¸‡ +label_revision: à¸à¸²à¸£à¹à¸à¹‰à¹„ข +label_revision_plural: à¸à¸²à¸£à¹à¸à¹‰à¹„ข +label_associated_revisions: à¸à¸²à¸£à¹à¸à¹‰à¹„ขที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡ +label_added: ถูà¸à¹€à¸žà¸´à¹ˆà¸¡ +label_modified: ถูà¸à¹à¸à¹‰à¹„ข +label_deleted: ถูà¸à¸¥à¸š +label_latest_revision: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด +label_latest_revision_plural: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด +label_view_revisions: ดูà¸à¸²à¸£à¹à¸à¹‰à¹„ข +label_max_size: ขนาดใหà¸à¹ˆà¸ªà¸¸à¸” +label_on: 'ใน' +label_sort_highest: ย้ายไปบนสุด +label_sort_higher: ย้ายขึ้น +label_sort_lower: ย้ายลง +label_sort_lowest: ย้ายไปล่างสุด +label_roadmap: à¹à¸œà¸™à¸‡à¸²à¸™ +label_roadmap_due_in: ถึงà¸à¸³à¸«à¸™à¸”ใน +label_roadmap_overdue: %s ช้าà¸à¸§à¹ˆà¸²à¸à¸³à¸«à¸™à¸” +label_roadmap_no_issues: ไม่มีปัà¸à¸«à¸²à¸ªà¸³à¸«à¸£à¸±à¸šà¸£à¸¸à¹ˆà¸™à¸™à¸µà¹‰ +label_search: ค้นหา +label_result_plural: ผลà¸à¸²à¸£à¸„้นหา +label_all_words: ทุà¸à¸„ำ +label_wiki: Wiki +label_wiki_edit: à¹à¸à¹‰à¹„ข Wiki +label_wiki_edit_plural: à¹à¸à¹‰à¹„ข Wiki +label_wiki_page: หน้า Wiki +label_wiki_page_plural: หน้า Wiki +label_index_by_title: เรียงตามชื่à¸à¹€à¸£à¸·à¹ˆà¸à¸‡ +label_index_by_date: เรียงตามวัน +label_current_version: รุ่นปัจจุบัน +label_preview: ตัวà¸à¸¢à¹ˆà¸²à¸‡à¸à¹ˆà¸à¸™à¸ˆà¸±à¸”เà¸à¹‡à¸š +label_feed_plural: Feeds +label_changes_details: รายละเà¸à¸µà¸¢à¸”à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด +label_issue_tracking: ติดตามปัà¸à¸«à¸² +label_spent_time: เวลาที่ใช้ +label_f_hour: %.2f ชั่วโมง +label_f_hour_plural: %.2f ชั่วโมง +label_time_tracking: ติดตามà¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸² +label_change_plural: เปลี่ยนà¹à¸›à¸¥à¸‡ +label_statistics: สถิติ +label_commits_per_month: Commits ต่à¸à¹€à¸”ืà¸à¸™ +label_commits_per_author: Commits ต่à¸à¸œà¸¹à¹‰à¹à¸•่ง +label_view_diff: ดูความà¹à¸•à¸à¸•่าง +label_diff_inline: inline +label_diff_side_by_side: side by side +label_options: ตัวเลืà¸à¸ +label_copy_workflow_from: คัดลà¸à¸à¸¥à¸³à¸”ับงานจาภ+label_permissions_report: รายงานสิทธิ +label_watched_issues: เà¸à¹‰à¸²à¸”ูปัà¸à¸«à¸² +label_related_issues: ปัà¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡ +label_applied_status: จัดเà¸à¹‡à¸šà¸ªà¸–านะ +label_loading: à¸à¸³à¸¥à¸±à¸‡à¹‚หลด... +label_relation_new: ความสัมพันธ์ใหม่ +label_relation_delete: ลบความสัมพันธ์ +label_relates_to: สัมพันธ์à¸à¸±à¸š +label_duplicates: ซ้ำ +label_blocks: à¸à¸µà¸”à¸à¸±à¸™ +label_blocked_by: à¸à¸µà¸”à¸à¸±à¸™à¹‚ดย +label_precedes: นำหน้า +label_follows: ตามหลัง +label_end_to_start: จบ-เริ่ม +label_end_to_end: จบ-จบ +label_start_to_start: เริ่ม-เริ่ม +label_start_to_end: เริ่ม-จบ +label_stay_logged_in: à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸°à¸šà¸šà¸•่ภ+label_disabled: ไม่ใช้งาน +label_show_completed_versions: à¹à¸ªà¸”งรุ่นที่สมบูรณ์ +label_me: ฉัน +label_board: สภาà¸à¸²à¹à¸Ÿ +label_board_new: สร้างสภาà¸à¸²à¹à¸Ÿ +label_board_plural: สภาà¸à¸²à¹à¸Ÿ +label_topic_plural: หัวข้ภ+label_message_plural: ข้à¸à¸„วาม +label_message_last: ข้à¸à¸„วามล่าสุด +label_message_new: เขียนข้à¸à¸„วามใหม่ +label_message_posted: ข้à¸à¸„วามถูà¸à¹€à¸žà¸´à¹ˆà¸¡à¹à¸¥à¹‰à¸§ +label_reply_plural: ตà¸à¸šà¸à¸¥à¸±à¸š +label_send_information: ส่งรายละเà¸à¸µà¸¢à¸”ขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¹ƒà¸«à¹‰à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰ +label_year: ปี +label_month: เดืà¸à¸™ +label_week: สัปดาห์ +label_date_from: จาภ+label_date_to: ถึง +label_language_based: ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸ าษาขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰ +label_sort_by: เรียงโดย %s +label_send_test_email: ส่งจดหมายทดสà¸à¸š +label_feeds_access_key_created_on: RSS access key สร้างเมื่ภ%s ที่ผ่านมา +label_module_plural: ส่วนประà¸à¸à¸š +label_added_time_by: เพิ่มโดย %s %s ที่ผ่านมา +label_updated_time: ปรับปรุง %s ที่ผ่านมา +label_jump_to_a_project: ไปที่โครงà¸à¸²à¸£... +label_file_plural: à¹à¸Ÿà¹‰à¸¡ +label_changeset_plural: à¸à¸¥à¸¸à¹ˆà¸¡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ +label_default_columns: สดมภ์เริ่มต้น +label_no_change_option: (ไม่เปลี่ยนà¹à¸›à¸¥à¸‡) +label_bulk_edit_selected_issues: à¹à¸à¹‰à¹„ขปัà¸à¸«à¸²à¸—ี่เลืà¸à¸à¸—ั้งหมด +label_theme: ชุดรูปà¹à¸šà¸š +label_default: ค่าเริ่มต้น +label_search_titles_only: ค้นหาจาà¸à¸Šà¸·à¹ˆà¸à¹€à¸£à¸·à¹ˆà¸à¸‡à¹€à¸—่านั้น +label_user_mail_option_all: "ทุà¸à¹† เหตุà¸à¸²à¸£à¸“์ในโครงà¸à¸²à¸£à¸‚à¸à¸‡à¸‰à¸±à¸™" +label_user_mail_option_selected: "ทุà¸à¹† เหตุà¸à¸²à¸£à¸“์ในโครงà¸à¸²à¸£à¸—ี่เลืà¸à¸..." +label_user_mail_option_none: "เฉพาะสิ่งที่ฉันเลืà¸à¸à¸«à¸£à¸·à¸à¸¡à¸µà¸ªà¹ˆà¸§à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡" +label_user_mail_no_self_notified: "ฉันไม่ต้à¸à¸‡à¸à¸²à¸£à¹„ด้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ืà¸à¸™à¹ƒà¸™à¸ªà¸´à¹ˆà¸‡à¸—ี่ฉันทำเà¸à¸‡" +label_registration_activation_by_email: เปิดบัà¸à¸Šà¸µà¸œà¹ˆà¸²à¸™à¸à¸µà¹€à¸¡à¸¥à¹Œ +label_registration_manual_activation: à¸à¸™à¸¸à¸¡à¸±à¸•ิโดยผู้บริหารจัดà¸à¸²à¸£ +label_registration_automatic_activation: เปิดบัà¸à¸Šà¸µà¸à¸±à¸•โนมัติ +label_display_per_page: 'ต่à¸à¸«à¸™à¹‰à¸²: %s' +label_age: à¸à¸²à¸¢à¸¸ +label_change_properties: เปลี่ยนคุณสมบัติ +label_general: ทั่วๆ ไป +label_more: à¸à¸·à¹ˆà¸™ ๆ +label_scm: ตัวจัดà¸à¸²à¸£à¸•้นฉบับ +label_plugins: ส่วนเสริม +label_ldap_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนโดยใช้ LDAP +label_downloads_abbr: D/L +label_optional_description: รายละเà¸à¸µà¸¢à¸”เพิ่มเติม +label_add_another_file: เพิ่มà¹à¸Ÿà¹‰à¸¡à¸à¸·à¹ˆà¸™à¹† +label_preferences: ค่าที่ชà¸à¸šà¹ƒà¸ˆ +label_chronological_order: เรียงจาà¸à¹€à¸à¹ˆà¸²à¹„ปใหม่ +label_reverse_chronological_order: เรียงจาà¸à¹ƒà¸«à¸¡à¹ˆà¹„ปเà¸à¹ˆà¸² +label_planning: à¸à¸²à¸£à¸§à¸²à¸‡à¹à¸œà¸™ + +button_login: เข้าระบบ +button_submit: จัดส่งข้à¸à¸¡à¸¹à¸¥ +button_save: จัดเà¸à¹‡à¸š +button_check_all: เลืà¸à¸à¸—ั้งหมด +button_uncheck_all: ไม่เลืà¸à¸à¸—ั้งหมด +button_delete: ลบ +button_create: สร้าง +button_test: ทดสà¸à¸š +button_edit: à¹à¸à¹‰à¹„ข +button_add: เพิ่ม +button_change: เปลี่ยนà¹à¸›à¸¥à¸‡ +button_apply: ประยุà¸à¸•์ใช้ +button_clear: ล้างข้à¸à¸„วาม +button_lock: ล็à¸à¸„ +button_unlock: ยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸¥à¹‡à¸à¸„ +button_download: ดาวน์โหลด +button_list: รายà¸à¸²à¸£ +button_view: มุมมà¸à¸‡ +button_move: ย้าย +button_back: à¸à¸¥à¸±à¸š +button_cancel: ยà¸à¹€à¸¥à¸´à¸ +button_activate: เปิดใช้ +button_sort: จัดเรียง +button_log_time: บันทึà¸à¹€à¸§à¸¥à¸² +button_rollback: ถà¸à¸¢à¸à¸¥à¸±à¸šà¸¡à¸²à¸—ี่รุ่นนี้ +button_watch: เà¸à¹‰à¸²à¸”ู +button_unwatch: เลิà¸à¹€à¸à¹‰à¸²à¸”ู +button_reply: ตà¸à¸šà¸à¸¥à¸±à¸š +button_archive: เà¸à¹‡à¸šà¹€à¸‚้าโà¸à¸”ัง +button_unarchive: เà¸à¸²à¸à¸à¸à¸ˆà¸²à¸à¹‚à¸à¸”ัง +button_reset: เริ่มใหมท +button_rename: เปลี่ยนชื่ภ+button_change_password: เปลี่ยนรหัสผ่าน +button_copy: คัดลà¸à¸ +button_annotate: หมายเหตุประà¸à¸à¸š +button_update: ปรับปรุง +button_configure: ปรับà¹à¸•่ง + +status_active: เปิดใช้งานà¹à¸¥à¹‰à¸§ +status_registered: รà¸à¸à¸²à¸£à¸à¸™à¸¸à¸¡à¸±à¸•ิ +status_locked: ล็à¸à¸„ + +text_select_mail_notifications: เลืà¸à¸à¸à¸²à¸£à¸à¸£à¸°à¸—ำที่ต้à¸à¸‡à¸à¸²à¸£à¹ƒà¸«à¹‰à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥à¹Œà¹à¸ˆà¹‰à¸‡. +text_regexp_info: ตัวà¸à¸¢à¹ˆà¸²à¸‡ ^[A-Z0-9]+$ +text_min_max_length_info: 0 หมายถึงไม่จำà¸à¸±à¸” +text_project_destroy_confirmation: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¹‚ครงà¸à¸²à¸£à¹à¸¥à¸°à¸‚้à¸à¸¡à¸¹à¸¥à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้่à¸à¸‡ ? +text_subprojects_destroy_warning: 'โครงà¸à¸²à¸£à¸¢à¹ˆà¸à¸¢: %s จะถูà¸à¸¥à¸šà¸”้วย.' +text_workflow_edit: เลืà¸à¸à¸šà¸—บาทà¹à¸¥à¸°à¸à¸²à¸£à¸•ิดตาม เพื่à¸à¹à¸à¹‰à¹„ขลำดับงาน +text_are_you_sure: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หม ? +text_journal_changed: เปลี่ยนà¹à¸›à¸¥à¸‡à¸ˆà¸²à¸ %s เป็น %s +text_journal_set_to: ตั้งค่าเป็น %s +text_journal_deleted: ถูà¸à¸¥à¸š +text_tip_task_begin_day: งานที่เริ่มวันนี้ +text_tip_task_end_day: งานที่จบวันนี้ +text_tip_task_begin_end_day: งานที่เริ่มà¹à¸¥à¸°à¸ˆà¸šà¸§à¸±à¸™à¸™à¸µà¹‰ +text_project_identifier_info: 'ภาษาà¸à¸±à¸‡à¸à¸¤à¸©à¸•ัวเล็à¸(a-z), ตัวเลข(0-9) à¹à¸¥à¸°à¸‚ีด (-) เท่านั้น.<br />เมื่à¸à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹à¸¥à¹‰à¸§, ชื่à¸à¹€à¸‰à¸žà¸²à¸°à¹„ม่สามารถเปลี่ยนà¹à¸›à¸¥à¸‡à¹„ด้' +text_caracters_maximum: สูงสุด %d ตัวà¸à¸±à¸à¸©à¸£. +text_caracters_minimum: ต้à¸à¸‡à¸¢à¸²à¸§à¸à¸¢à¹ˆà¸²à¸‡à¸™à¹‰à¸à¸¢ %d ตัวà¸à¸±à¸à¸©à¸£. +text_length_between: ความยาวระหว่าง %d ถึง %d ตัวà¸à¸±à¸à¸©à¸£. +text_tracker_no_workflow: ไม่ได้บัà¸à¸à¸±à¸•ิลำดับงานสำหรับà¸à¸²à¸£à¸•ิดตามนี้ +text_unallowed_characters: ตัวà¸à¸±à¸à¸©à¸£à¸•้à¸à¸‡à¸«à¹‰à¸²à¸¡ +text_comma_separated: ใส่ได้หลายค่า โดยคั่นด้วยลูà¸à¸™à¹‰à¸³( ,). +text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages +text_issue_added: ปัà¸à¸«à¸² %s ถูà¸à¹à¸ˆà¹‰à¸‡à¹‚ดย %s. +text_issue_updated: ปัà¸à¸«à¸² %s ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹‚ดย %s. +text_wiki_destroy_confirmation: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸à¸§à¹ˆà¸²à¸•้à¸à¸‡à¸à¸²à¸£à¸¥à¸š wiki นี้พร้à¸à¸¡à¸—ั้งเนี้à¸à¸«à¸²? +text_issue_category_destroy_question: บางปัà¸à¸«à¸² (%d) à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸›à¸£à¸°à¹€à¸ ทนี้. คุณต้à¸à¸‡à¸à¸²à¸£à¸—ำà¸à¸¢à¹ˆà¸²à¸‡à¹„ร ? +text_issue_category_destroy_assignments: ลบประเภทนี้ +text_issue_category_reassign_to: ระบุปัà¸à¸«à¸²à¹ƒà¸™à¸›à¸£à¸°à¹€à¸ ทนี้ +text_user_mail_option: "ในโครงà¸à¸²à¸£à¸—ี่ไม่ได้เลืà¸à¸, คุณจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸ªà¸´à¹ˆà¸‡à¸—ี่คุณเà¸à¹‰à¸²à¸”ูหรืà¸à¸¡à¸µà¸ªà¹ˆà¸§à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡ (เช่นปัà¸à¸«à¸²à¸—ี่คุณà¹à¸ˆà¹‰à¸‡à¹„ว้หรืà¸à¹„ด้รับมà¸à¸šà¸«à¸¡à¸²à¸¢)." +text_no_configuration_data: "บทบาท, à¸à¸²à¸£à¸•ิดตาม, สถานะปัà¸à¸«à¸² à¹à¸¥à¸°à¸¥à¸³à¸”ับงานยังไม่ได้ถูà¸à¸•ั้งค่า.\nขà¸à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹‚หลดค่าเริ่มต้น. คุณสามารถà¹à¸à¹‰à¹„ขค่าได้หลังจาà¸à¹‚หลดà¹à¸¥à¹‰à¸§." +text_load_default_configuration: โหลดค่าเริ่มต้น +text_status_changed_by_changeset: ประยุà¸à¸•์ใช้ในà¸à¸¥à¸¸à¹ˆà¸¡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ %s. +text_issues_destroy_confirmation: 'คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¸›à¸±à¸à¸«à¸²(ทั้งหลาย)ที่เลืà¸à¸à¹„ว้?' +text_select_project_modules: 'เลืà¸à¸à¸ªà¹ˆà¸§à¸™à¸›à¸£à¸°à¸à¸à¸šà¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸ªà¸³à¸«à¸£à¸±à¸šà¹‚ครงà¸à¸²à¸£à¸™à¸µà¹‰:' +text_default_administrator_account_changed: ค่าเริ่มต้นขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸–ูà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ +text_file_repository_writable: ที่เà¸à¹‡à¸šà¸•้นฉบับสามารถเขียนได้ +text_rmagick_available: RMagick มีให้ใช้ (เป็นตัวเลืà¸à¸) +text_destroy_time_entries_question: %.02f ชั่วโมงที่ถูà¸à¹à¸ˆà¹‰à¸‡à¹ƒà¸™à¸›à¸±à¸à¸«à¸²à¸™à¸µà¹‰à¸ˆà¸°à¹‚ดนลบ. คุณต้à¸à¸‡à¸à¸²à¸£à¸—ำà¸à¸¢à¹ˆà¸²à¸‡à¹„ร? +text_destroy_time_entries: ลบเวลาที่รายงานไว้ +text_assign_time_entries_to_project: ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹‰ +text_reassign_time_entries: 'ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹ˆà¸à¸µà¸à¸„รั้ง:' + +default_role_manager: ผู้จัดà¸à¸²à¸£ +default_role_developper: ผู้พัฒนา +default_role_reporter: ผู้รายงาน +default_tracker_bug: บั๊ภ+default_tracker_feature: ลัà¸à¸©à¸“ะเด่น +default_tracker_support: สนับสนุน +default_issue_status_new: เà¸à¸´à¸”ขึ้น +default_issue_status_assigned: รับมà¸à¸šà¸«à¸¡à¸²à¸¢ +default_issue_status_resolved: ดำเนินà¸à¸²à¸£ +default_issue_status_feedback: รà¸à¸„ำตà¸à¸š +default_issue_status_closed: จบ +default_issue_status_rejected: ยà¸à¹€à¸¥à¸´à¸ +default_doc_category_user: เà¸à¸à¸ªà¸²à¸£à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰ +default_doc_category_tech: เà¸à¸à¸ªà¸²à¸£à¸—างเทคนิค +default_priority_low: ต่ำ +default_priority_normal: ปà¸à¸•ิ +default_priority_high: สูง +default_priority_urgent: เร่งด่วน +default_priority_immediate: ด่วนมาภ+default_activity_design: à¸à¸à¸à¹à¸šà¸š +default_activity_development: พัฒนา + +enumeration_issue_priorities: ความสำคัà¸à¸‚à¸à¸‡à¸›à¸±à¸à¸«à¸² +enumeration_doc_categories: ประเภทเà¸à¸à¸ªà¸²à¸£ +enumeration_activities: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ (ใช้ในà¸à¸²à¸£à¸•ิดตามเวลา) +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/uk.yml b/groups/lang/uk.yml index a52a05603..7ba152413 100644 --- a/groups/lang/uk.yml +++ b/groups/lang/uk.yml @@ -48,6 +48,7 @@ general_text_no: 'ÐÑ–' general_text_yes: 'Так' general_lang_name: 'Ukrainian (УкраїнÑька)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: UTF-8 general_pdf_encoding: UTF-8 general_day_names: Понеділок,Вівторок,Середа,Четвер,П'ÑтницÑ,Субота,ÐÐµÐ´Ñ–Ð»Ñ @@ -620,3 +621,20 @@ setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +label_and_its_subprojects: %s and its subprojects +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +mail_subject_reminder: "%d issue(s) due in the next days" +text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' +label_incoming_emails: Incoming emails +label_generate_key: Generate a key +setting_mail_handler_api_enabled: Enable WS for incoming emails +setting_mail_handler_api_key: API key +text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/zh-tw.yml b/groups/lang/zh-tw.yml index a0c7fafb3..6a1441364 100644 --- a/groups/lang/zh-tw.yml +++ b/groups/lang/zh-tw.yml @@ -48,6 +48,7 @@ general_text_no: 'å¦' general_text_yes: '是' general_lang_name: 'Traditional Chinese (ç¹é«”䏿–‡)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: Big5 general_pdf_encoding: Big5 general_day_names: 星期一,星期二,星期三,星期四,星期五,星期å…,星期日 @@ -91,6 +92,8 @@ mail_body_account_information_external: 您å¯ä»¥ä½¿ç”¨ "%s" 帳號登入 Redmin mail_body_account_information: 您的 Redmine 帳號資訊 mail_subject_account_activation_request: Redmine 帳號啟用需求通知 mail_body_account_activation_request: 'æœ‰ä½æ–°ç”¨æˆ¶ (%s) 已經完æˆè¨»å†Šï¼Œæ£ç‰å€™æ‚¨çš„å¯©æ ¸ï¼š' +mail_subject_reminder: "您有 %d å€‹é …ç›®å³å°‡åˆ°æœŸ" +mail_body_reminder: "%d å€‹æŒ‡æ´¾çµ¦æ‚¨çš„é …ç›®ï¼Œå°‡æ–¼ %d 天之內到期:" gui_validation_error: 1 個錯誤 gui_validation_error_plural: %d 個錯誤 @@ -110,7 +113,7 @@ field_created_on: 建立日期 field_updated_on: æ›´æ–° field_field_format: æ ¼å¼ field_is_for_all: 給所有專案 -field_possible_values: Possible values +field_possible_values: å¯èƒ½å€¼ field_regexp: æ£è¦è¡¨ç¤ºå¼ field_min_length: 最å°é•·åº¦ field_max_length: 最大長度 @@ -153,9 +156,9 @@ field_account: 帳戶 field_base_dn: Base DN field_attr_login: 登入屬性 field_attr_firstname: åå—屬性 -field_attr_lastname: Lastname attribute -field_attr_mail: Email attribute -field_onthefly: On-the-fly user creation +field_attr_lastname: å§“æ°å±¬æ€§ +field_attr_mail: é›»å郵件信箱屬性 +field_onthefly: 峿™‚建立使用者 field_start_date: 開始日期 field_done_ratio: 完æˆç™¾åˆ†æ¯” field_auth_source: èªè‰æ¨¡å¼ @@ -168,13 +171,13 @@ field_hours: å°æ™‚ field_activity: 活動 field_spent_on: 日期 field_identifier: 代碼 -field_is_filter: Used as a filter -field_issue_to_id: Related issue +field_is_filter: ç”¨ä¾†ä½œç‚ºéŽæ¿¾å™¨ +field_issue_to_id: ç›¸é—œé …ç›® field_delay: 逾期 field_assignable: é …ç›®å¯è¢«åˆ†æ´¾è‡³æ¤è§’色 -field_redirect_existing_links: Redirect existing links +field_redirect_existing_links: 釿–°å°Žå‘ç¾æœ‰é€£çµ field_estimated_hours: é 估工時 -field_column_names: Columns +field_column_names: æ¬„ä½ field_time_zone: æ™‚å€ field_searchable: å¯ç”¨åšæœå°‹æ¢ä»¶ field_default_value: é è¨å€¼ @@ -193,7 +196,7 @@ setting_bcc_recipients: 使用密件副本 (BCC) setting_host_name: 主機å稱 setting_text_formatting: æ–‡å—æ ¼å¼ setting_wiki_compression: 壓縮 Wiki æ·å²æ–‡ç« -setting_feeds_limit: Feed content limit +setting_feeds_limit: RSS æ–°èžé™åˆ¶ setting_autofetch_changesets: 自動å–å¾—é€äº¤ç‰ˆæ¬¡ setting_default_projects_public: 新建立之專案é è¨ç‚ºã€Œå…¬é–‹ã€ setting_sys_api_enabled: 啟用管ç†ç‰ˆæœ¬åº«ä¹‹ç¶²é æœå‹™ (Web Service) @@ -210,7 +213,10 @@ setting_protocol: å”定 setting_per_page_options: æ¯é 顯示個數é¸é … setting_user_format: ä½¿ç”¨è€…é¡¯ç¤ºæ ¼å¼ setting_activity_days_default: 專案活動顯示天數 -setting_display_subprojects_issues: é è¨æ–¼ä¸»æŽ§å°ˆæ¡ˆä¸é¡¯ç¤ºå¾žå±¬å°ˆæ¡ˆçš„é …ç›® +setting_display_subprojects_issues: é è¨æ–¼çˆ¶å°ˆæ¡ˆä¸é¡¯ç¤ºåå°ˆæ¡ˆçš„é …ç›® +setting_enabled_scm: 啟用的 SCM +setting_mail_handler_api_enabled: 啟用處ç†å‚³å…¥é›»å郵件的æœå‹™ +setting_mail_handler_api_key: API 金鑰 project_module_issue_tracking: é …ç›®è¿½è¹¤ project_module_time_tracking: 工時追蹤 @@ -291,6 +297,7 @@ label_auth_source: èªè‰æ¨¡å¼ label_auth_source_new: 建立新èªè‰æ¨¡å¼ label_auth_source_plural: èªè‰æ¨¡å¼æ¸…å–® label_subproject_plural: å專案 +label_and_its_subprojects: %s 與其å專案 label_min_max_length: æœ€å° - 最大 長度 label_list: 清單 label_date: 日期 @@ -327,7 +334,7 @@ label_version_new: 建立新的版本 label_version_plural: 版本 label_confirmation: ç¢ºèª label_export_to: 匯出至 -label_read: Read... +label_read: 讀å–... label_public_projects: 公開專案 label_open_issues: é€²è¡Œä¸ label_open_issues_plural: é€²è¡Œä¸ @@ -339,7 +346,7 @@ label_current_status: ç›®å‰ç‹€æ…‹ label_new_statuses_allowed: å¯è®Šæ›´è‡³ä»¥ä¸‹ç‹€æ…‹ label_all: 全部 label_none: 空值 -label_nobody: nobody +label_nobody: ç„¡å label_next: 下一é label_previous: 上一é label_used_by: Used by @@ -349,7 +356,7 @@ label_per_page: æ¯é label_calendar: 日曆 label_months_from: 個月, 開始月份 label_gantt: 甘特圖 -label_internal: Internal +label_internal: 內部 label_last_changes: 最近 %d 個變更 label_change_view_all: 檢視所有變更 label_personalize_page: è‡ªè¨‚ç‰ˆé¢ @@ -426,7 +433,7 @@ label_issue_tracking: é …ç›®è¿½è¹¤ label_spent_time: 耗用時間 label_f_hour: %.2f å°æ™‚ label_f_hour_plural: %.2f å°æ™‚ -label_time_tracking: Time tracking +label_time_tracking: 工時追蹤 label_change_plural: 變更 label_statistics: 統計資訊 label_commits_per_month: 便œˆä»½çµ±è¨ˆé€äº¤æ¬¡æ•¸ @@ -445,14 +452,15 @@ label_relation_new: å»ºç«‹æ–°é—œè¯ label_relation_delete: åˆªé™¤é—œè¯ label_relates_to: é—œè¯è‡³ label_duplicates: å·²é‡è¤‡ +label_duplicated_by: èˆ‡å¾Œé¢æ‰€åˆ—é …ç›®é‡è¤‡ label_blocks: 阻擋 label_blocked_by: 被阻擋 label_precedes: 優先於 label_follows: 跟隨於 -label_end_to_start: end to start -label_end_to_end: end to end -label_start_to_start: start to start -label_start_to_end: start to end +label_end_to_start: çµæŸâ”€é–‹å§‹ +label_end_to_end: çµæŸâ”€çµæŸ +label_start_to_start: 開始─開始 +label_start_to_end: é–‹å§‹â”€çµæŸ label_stay_logged_in: ç¶æŒå·²ç™»å…¥ç‹€æ…‹ label_disabled: 關閉 label_show_completed_versions: 顯示已完æˆçš„版本 @@ -496,7 +504,7 @@ label_registration_activation_by_email: é€éŽé›»å郵件啟用帳戶 label_registration_manual_activation: 手動啟用帳戶 label_registration_automatic_activation: 自動啟用帳戶 label_display_per_page: 'æ¯é 顯示: %s 個' -label_age: Age +label_age: 年齡 label_change_properties: 變更屬性 label_general: 一般 label_more: 更多 » @@ -510,6 +518,8 @@ label_preferences: å好é¸é … label_chronological_order: 以時間由é è‡³è¿‘æŽ’åº label_reverse_chronological_order: ä»¥æ™‚é–“ç”±è¿‘è‡³é æŽ’åº label_planning: 計劃表 +label_incoming_emails: 傳入的電å郵件 +label_generate_key: 產生金鑰 button_login: 登入 button_submit: é€å‡º @@ -527,10 +537,10 @@ button_clear: 清除 button_lock: 鎖定 button_unlock: 解除鎖定 button_download: 下載 -button_list: List +button_list: 清單 button_view: 檢視 button_move: 移動 -button_back: Back +button_back: 返回 button_cancel: å–æ¶ˆ button_activate: 啟用 button_sort: æŽ’åº @@ -557,6 +567,7 @@ text_select_mail_notifications: 鏿“‡æ¬²å¯„é€æé†’é€šçŸ¥éƒµä»¶ä¹‹å‹•ä½œ text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 代表「ä¸é™åˆ¶ã€ text_project_destroy_confirmation: 您確定è¦åˆªé™¤é€™å€‹å°ˆæ¡ˆå’Œå…¶ä»–相關資料? +text_subprojects_destroy_warning: '下列å專案: %s 將一併被刪除。' text_workflow_edit: 鏿“‡è§’色與追蹤標籤以è¨å®šå…¶å·¥ä½œæµç¨‹ text_are_you_sure: 確定執行? text_journal_changed: 從 %s 變更為 %s @@ -579,8 +590,8 @@ text_wiki_destroy_confirmation: 您確定è¦åˆªé™¤é€™å€‹ wiki 和其ä¸çš„æ‰€æœ‰ text_issue_category_destroy_question: 有 (%d) å€‹é …ç›®è¢«æŒ‡æ´¾åˆ°æ¤åˆ†é¡ž. è«‹é¸æ“‡æ‚¨æƒ³è¦çš„動作? text_issue_category_destroy_assignments: ç§»é™¤é€™äº›é …ç›®çš„åˆ†é¡ž text_issue_category_reassign_to: 釿–°æŒ‡æ´¾é€™äº›é …目至其它分類 -text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." -text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." +text_user_mail_option: "å°æ–¼é‚£äº›æœªè¢«é¸æ“‡çš„å°ˆæ¡ˆï¼Œå°‡åªæœƒæŽ¥æ”¶åˆ°æ‚¨æ£åœ¨è§€å¯Ÿä¸ï¼Œæˆ–是åƒèˆ‡ä¸çš„é …ç›®é€šçŸ¥ã€‚ï¼ˆã€Œåƒèˆ‡ä¸çš„é …ç›®ã€åŒ…嫿‚¨å»ºç«‹çš„æˆ–æ˜¯æŒ‡æ´¾çµ¦æ‚¨çš„é …ç›®ï¼‰" +text_no_configuration_data: "角色ã€è¿½è¹¤å™¨ã€é …目狀態與æµç¨‹å°šæœªè¢«è¨å®šå®Œæˆã€‚\nå¼·çƒˆå»ºè°æ‚¨å…ˆè¼‰å…¥é è¨çš„è¨å®šï¼Œç„¶å¾Œä¿®æ”¹æˆæ‚¨æƒ³è¦çš„è¨å®šã€‚" text_load_default_configuration: 載入é è¨çµ„æ…‹ text_status_changed_by_changeset: 已套用至變更集 %s. text_issues_destroy_confirmation: 'ç¢ºå®šåˆªé™¤å·²é¸æ“‡çš„é …ç›®ï¼Ÿ' @@ -592,6 +603,10 @@ text_destroy_time_entries_question: 您å³å°‡åˆªé™¤çš„é …ç›®å·²å ±å·¥ %.02f å°æ text_destroy_time_entries: åˆªé™¤å·²å ±å·¥çš„æ™‚æ•¸ text_assign_time_entries_to_project: æŒ‡å®šå·²å ±å·¥çš„æ™‚æ•¸è‡³å°ˆæ¡ˆä¸ text_reassign_time_entries: '釿–°æŒ‡å®šå·²å ±å·¥çš„æ™‚數至æ¤é …目:' +text_user_wrote: '%s å…ˆå‰æåˆ°:' +text_enumeration_destroy_question: 'ç›®å‰æœ‰ %d 個物件使用æ¤åˆ—舉值。' +text_enumeration_category_reassign_to: '釿–°è¨å®šå…¶åˆ—舉值為:' +text_email_delivery_not_configured: "您尚未è¨å®šé›»åéƒµä»¶å‚³é€æ–¹å¼ï¼Œå› æ¤æé†’é¸é …已被åœç”¨ã€‚\n請在 config/email.yml ä¸è¨å®š SMTP ä¹‹å¾Œï¼Œé‡æ–°å•Ÿå‹• Redmine,以啟用電å郵件æé†’é¸é …。" default_role_manager: 管ç†äººå“¡ default_role_developper: 開發人員 @@ -618,4 +633,7 @@ default_activity_development: 開發 enumeration_issue_priorities: é …ç›®å„ªå…ˆæ¬Š enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +field_parent_title: Parent page +label_issue_watchers: Watchers +setting_commit_logs_encoding: Commit messages encoding +button_quote: Quote diff --git a/groups/lang/zh.yml b/groups/lang/zh.yml index 12fb8cb3e..bfe551093 100644 --- a/groups/lang/zh.yml +++ b/groups/lang/zh.yml @@ -48,6 +48,7 @@ general_text_no: 'å¦' general_text_yes: '是' general_lang_name: 'Simplified Chinese (ç®€ä½“ä¸æ–‡)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: gb2312 general_pdf_encoding: gb2312 general_day_names: 星期一,星期二,星期三,星期四,星期五,星期å…,星期日 @@ -91,6 +92,8 @@ mail_body_account_information_external: 您å¯ä»¥ä½¿ç”¨æ‚¨çš„ "%s" å¸å·æ¥ç™»å mail_body_account_information: 您的å¸å·ä¿¡æ¯ mail_subject_account_activation_request: %så¸å·æ¿€æ´»è¯·æ±‚ mail_body_account_activation_request: '新用户(%sï¼‰å·²å®Œæˆæ³¨å†Œï¼Œæ£åœ¨ç‰å€™æ‚¨çš„å®¡æ ¸ï¼š' +mail_subject_reminder: "%d 个问题需è¦å°½å¿«è§£å†³" +mail_body_reminder: "指派给您的 %d 个问题需è¦åœ¨ %d 天内完æˆï¼š" gui_validation_error: 1 个错误 gui_validation_error_plural: %d 个错误 @@ -179,6 +182,7 @@ field_time_zone: 时区 field_searchable: å¯ç”¨ä½œæœç´¢æ¡ä»¶ field_default_value: 默认值 field_comments_sorting: 显示注释 +field_parent_title: ä¸Šçº§é¡µé¢ setting_app_title: åº”ç”¨ç¨‹åºæ ‡é¢˜ setting_app_subtitle: 应用程åºåæ ‡é¢˜ @@ -198,19 +202,23 @@ setting_default_projects_public: 新建项目默认为公开项目 setting_autofetch_changesets: 自动获å–程åºå˜æ›´ setting_sys_api_enabled: å¯ç”¨ç”¨äºŽç‰ˆæœ¬åº“管ç†çš„Web Service setting_commit_ref_keywords: ç”¨äºŽå¼•ç”¨é—®é¢˜çš„å…³é”®å— -setting_commit_fix_keywords: ç”¨äºŽä¿®è®¢é—®é¢˜çš„å…³é”®å— +setting_commit_fix_keywords: ç”¨äºŽè§£å†³é—®é¢˜çš„å…³é”®å— setting_autologin: 自动登录 setting_date_format: æ—¥æœŸæ ¼å¼ setting_time_format: æ—¶é—´æ ¼å¼ setting_cross_project_issue_relations: å…许ä¸åŒé¡¹ç›®ä¹‹é—´çš„é—®é¢˜å…³è” setting_issue_list_default_columns: é—®é¢˜åˆ—è¡¨ä¸æ˜¾ç¤ºçš„默认列 setting_repositories_encodings: 版本库编ç +setting_commit_logs_encoding: æäº¤æ³¨é‡Šçš„ç¼–ç setting_emails_footer: 邮件ç¾å setting_protocol: åè®® setting_per_page_options: æ¯é¡µæ˜¾ç¤ºæ¡ç›®ä¸ªæ•°çš„设置 setting_user_format: ç”¨æˆ·æ˜¾ç¤ºæ ¼å¼ setting_activity_days_default: åœ¨é¡¹ç›®æ´»åŠ¨ä¸æ˜¾ç¤ºçš„天数 setting_display_subprojects_issues: 在项目页é¢ä¸Šé»˜è®¤æ˜¾ç¤ºå项目的问题 +setting_enabled_scm: å¯ç”¨ SCM +setting_mail_handler_api_enabled: å¯ç”¨ç”¨äºŽæŽ¥æ”¶é‚®ä»¶çš„Web Service +setting_mail_handler_api_key: API key project_module_issue_tracking: 问题跟踪 project_module_time_tracking: 时间跟踪 @@ -257,9 +265,9 @@ label_issue_status_new: æ–°å»ºé—®é¢˜çŠ¶æ€ label_issue_category: 问题类别 label_issue_category_plural: 问题类别 label_issue_category_new: 新建问题类别 -label_custom_field: è‡ªå®šä¹‰å—æ®µ -label_custom_field_plural: è‡ªå®šä¹‰å—æ®µ -label_custom_field_new: æ–°å»ºè‡ªå®šä¹‰å—æ®µ +label_custom_field: 自定义属性 +label_custom_field_plural: 自定义属性 +label_custom_field_new: 新建自定义属性 label_enumerations: 枚举值 label_enumeration_new: 新建枚举值 label_information: ä¿¡æ¯ @@ -291,6 +299,7 @@ label_auth_source: è®¤è¯æ¨¡å¼ label_auth_source_new: æ–°å»ºè®¤è¯æ¨¡å¼ label_auth_source_plural: è®¤è¯æ¨¡å¼ label_subproject_plural: å项目 +label_and_its_subprojects: %s åŠå…¶å项目 label_min_max_length: æœ€å° - 最大 长度 label_list: 列表 label_date: 日期 @@ -445,6 +454,7 @@ label_relation_new: æ–°å»ºå…³è” label_relation_delete: åˆ é™¤å…³è” label_relates_to: å…³è”到 label_duplicates: é‡å¤ +label_duplicated_by: 与其é‡å¤ label_blocks: 阻挡 label_blocked_by: 被阻挡 label_precedes: 优先于 @@ -510,6 +520,9 @@ label_preferences: 首选项 label_chronological_order: æŒ‰æ—¶é—´é¡ºåº label_reverse_chronological_order: 按时间顺åºï¼ˆå€’åºï¼‰ label_planning: 计划 +label_incoming_emails: 接收邮件 +label_generate_key: 生æˆä¸€ä¸ªkey +label_issue_watchers: 跟踪者 button_login: 登录 button_submit: æäº¤ @@ -554,9 +567,10 @@ status_registered: 已注册 status_locked: å·²é”定 text_select_mail_notifications: 选择需è¦å‘é€é‚®ä»¶é€šçŸ¥çš„动作 -text_regexp_info: eg. ^[A-Z0-9]+$ +text_regexp_info: 例如:^[A-Z0-9]+$ text_min_max_length_info: 0 表示没有é™åˆ¶ text_project_destroy_confirmation: 您确信è¦åˆ é™¤è¿™ä¸ªé¡¹ç›®ä»¥åŠæ‰€æœ‰ç›¸å…³çš„æ•°æ®å—? +text_subprojects_destroy_warning: '以下åé¡¹ç›®ä¹Ÿå°†è¢«åŒæ—¶åˆ 除:%s' text_workflow_edit: é€‰æ‹©è§’è‰²å’Œè·Ÿè¸ªæ ‡ç¾æ¥ç¼–辑工作æµç¨‹ text_are_you_sure: 您确定? text_journal_changed: 从 %s å˜æ›´ä¸º %s @@ -572,7 +586,7 @@ text_length_between: 长度必须在 %d 到 %d 个å—符之间。 text_tracker_no_workflow: æ¤è·Ÿè¸ªæ ‡ç¾æœªå®šä¹‰å·¥ä½œæµç¨‹ text_unallowed_characters: éžæ³•å—符 text_comma_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆç”¨é€—å·,分开)。 -text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸å¼•用和修订问题 +text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸å¼•用和解决问题 text_issue_added: 问题 %s 已由 %s æäº¤ã€‚ text_issue_updated: 问题 %s 已由 %s 更新。 text_wiki_destroy_confirmation: 您确定è¦åˆ 除这个 wiki åŠå…¶æ‰€æœ‰å†…容å—? @@ -592,6 +606,10 @@ text_destroy_time_entries_question: 您è¦åˆ 除的问题已ç»ä¸ŠæŠ¥äº† %.02f å text_destroy_time_entries: åˆ é™¤ä¸ŠæŠ¥çš„å·¥ä½œé‡ text_assign_time_entries_to_project: å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æäº¤åˆ°é¡¹ç›®ä¸ text_reassign_time_entries: 'å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æŒ‡å®šåˆ°æ¤é—®é¢˜ï¼š' +text_user_wrote: '%s 写到:' +text_enumeration_category_reassign_to: '将它们关è”到新的枚举值:' +text_enumeration_destroy_question: '%d 个对象被关è”到了这个枚举值。' +text_email_delivery_not_configured: "邮件傿•°å°šæœªé…ç½®ï¼Œå› æ¤é‚®ä»¶é€šçŸ¥åŠŸèƒ½å·²è¢«ç¦ç”¨ã€‚\n请在config/email.ymlä¸é…置您的SMTPæœåŠ¡å™¨ä¿¡æ¯å¹¶é‡æ–°å¯åŠ¨ä»¥ä½¿å…¶ç”Ÿæ•ˆã€‚" default_role_manager: 管ç†äººå‘˜ default_role_developper: å¼€å‘人员 @@ -618,4 +636,4 @@ default_activity_development: å¼€å‘ enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +button_quote: Quote diff --git a/groups/lib/SVG/Graph/Graph.rb b/groups/lib/SVG/Graph/Graph.rb index 403a0202b..a5e1ea732 100644 --- a/groups/lib/SVG/Graph/Graph.rb +++ b/groups/lib/SVG/Graph/Graph.rb @@ -829,7 +829,7 @@ module SVG @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} ) if style_sheet && style_sheet != '' - @doc << ProcessingInstruction.new( "xml-stylesheet", + @doc << Instruction.new( "xml-stylesheet", %Q{href="#{style_sheet}" type="text/css"} ) end @root = @doc.add_element( "svg", { diff --git a/groups/lib/redcloth.rb b/groups/lib/redcloth.rb index 7e0c71839..df19de22d 100644 --- a/groups/lib/redcloth.rb +++ b/groups/lib/redcloth.rb @@ -299,6 +299,8 @@ class RedCloth < String hard_break text unless @lite_mode refs text + # need to do this before text is split by #blocks + block_textile_quotes text blocks text end inline text @@ -376,13 +378,13 @@ class RedCloth < String re = case rtype when :limit - /(^|[>\s]) + /(^|[>\s\(]) (#{rcq}) (#{C}) (?::(\S+?))? ([^\s\-].*?[^\s\-]|\w) #{rcq} - (?=[[:punct:]]|\s|$)/x + (?=[[:punct:]]|\s|\)|$)/x else /(#{rcq}) (#{C}) @@ -502,26 +504,19 @@ class RedCloth < String tatts = shelve( tatts ) if tatts rows = [] - fullrow. - split( /\|$/m ). - delete_if { |x| x.empty? }. - each do |row| - + fullrow.each_line do |row| ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m - cells = [] - #row.split( /\(?!\[\[[^\]])|(?![^\[]\]\])/ ).each do |cell| - row.split( /\|(?![^\[\|]*\]\])/ ).each do |cell| + row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell| + next if cell == '|' ctyp = 'd' ctyp = 'h' if cell =~ /^_/ catts = '' catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/ - unless cell.strip.empty? - catts = shelve( catts ) if catts - cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>" - end + catts = shelve( catts ) if catts + cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>" end ratts = shelve( ratts ) if ratts rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>" @@ -576,6 +571,29 @@ class RedCloth < String lines.join( "\n" ) end end + + QUOTES_RE = /(^>+([^\n]*?)\n?)+/m + QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m + + def block_textile_quotes( text ) + text.gsub!( QUOTES_RE ) do |match| + lines = match.split( /\n/ ) + quotes = '' + indent = 0 + lines.each do |line| + line =~ QUOTES_CONTENT_RE + bq,content = $1, $2 + l = bq.count('>') + if l != indent + quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n") + indent = l + end + quotes << (content + "\n") + end + quotes << ("\n" + '</blockquote>' * indent + "\n\n") + quotes + end + end CODE_RE = /(\W) @ @@ -726,7 +744,7 @@ class RedCloth < String end MARKDOWN_RULE_RE = /^(#{ - ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' ) + ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' ) })$/ def block_markdown_rule( text ) @@ -764,11 +782,11 @@ class RedCloth < String ([\s\[{(]|[#{PUNCT}])? # $pre " # start (#{C}) # $atts - ([^"]+?) # $text + ([^"\n]+?) # $text \s? (?:\(([^)]+?)\)(?="))? # $title ": - (\S+?) # $url + ([\w\/]\S+?) # $url (\/)? # $slash ([^\w\/;]*?) # $post (?=<|\s|$) @@ -1131,10 +1149,10 @@ class RedCloth < String end end - ALLOWED_TAGS = %w(redpre pre code) + ALLOWED_TAGS = %w(redpre pre code notextile) def escape_html_tags(text) - text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' if $3}" } + text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" } end end diff --git a/groups/lib/redmine.rb b/groups/lib/redmine.rb index 2697e8f5f..33d33752b 100644 --- a/groups/lib/redmine.rb +++ b/groups/lib/redmine.rb @@ -1,5 +1,6 @@ require 'redmine/access_control' require 'redmine/menu_manager' +require 'redmine/activity' require 'redmine/mime_type' require 'redmine/core_ext' require 'redmine/themes' @@ -11,7 +12,7 @@ rescue LoadError # RMagick is not available end -REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git ) +REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem ) # Permissions Redmine::AccessControl.map do |map| @@ -32,9 +33,9 @@ Redmine::AccessControl.map do |map| :queries => :index, :reports => :issue_report}, :public => true map.permission :add_issues, {:issues => :new} - map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]} + map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} - map.permission :add_issue_notes, {:issues => :edit} + map.permission :add_issue_notes, {:issues => [:edit, :reply]} map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin map.permission :move_issues, {:issues => :move}, :require => :loggedin @@ -45,6 +46,9 @@ Redmine::AccessControl.map do |map| # Gantt & calendar map.permission :view_gantt, :projects => :gantt map.permission :view_calendar, :projects => :calendar + # Watchers + map.permission :view_issue_watchers, {} + map.permission :add_issue_watchers, {:watchers => :new} end map.project_module :time_tracking do |map| @@ -76,6 +80,7 @@ Redmine::AccessControl.map do |map| map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special] map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment] + map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member end map.project_module :repository do |map| @@ -87,25 +92,25 @@ Redmine::AccessControl.map do |map| map.project_module :boards do |map| map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true - map.permission :add_messages, {:messages => [:new, :reply]} + map.permission :add_messages, {:messages => [:new, :reply, :quote]} map.permission :edit_messages, {:messages => :edit}, :require => :member map.permission :delete_messages, {:messages => :destroy}, :require => :member end end Redmine::MenuManager.map :top_menu do |menu| - menu.push :home, :home_url, :html => { :class => 'home' } + menu.push :home, :home_path, :html => { :class => 'home' } menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? } menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' } - menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? } - menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' } + menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }, :last => true + menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }, :last => true end Redmine::MenuManager.map :account_menu do |menu| - menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? } + menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? } menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? } menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? } - menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? } + menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? } end Redmine::MenuManager.map :application_menu do |menu| @@ -129,5 +134,15 @@ Redmine::MenuManager.map :project_menu do |menu| menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural menu.push :repository, { :controller => 'repositories', :action => 'show' }, :if => Proc.new { |p| p.repository && !p.repository.new_record? } - menu.push :settings, { :controller => 'projects', :action => 'settings' } + menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true +end + +Redmine::Activity.map do |activity| + activity.register :issues, :class_name => %w(Issue Journal) + activity.register :changesets + activity.register :news + activity.register :documents, :class_name => %w(Document Attachment) + activity.register :files, :class_name => 'Attachment' + activity.register :wiki_pages, :class_name => 'WikiContent::Version', :default => false + activity.register :messages, :default => false end diff --git a/groups/lib/redmine/activity.rb b/groups/lib/redmine/activity.rb new file mode 100644 index 000000000..565a53f36 --- /dev/null +++ b/groups/lib/redmine/activity.rb @@ -0,0 +1,46 @@ +# Redmine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Activity + + mattr_accessor :available_event_types, :default_event_types, :providers + + @@available_event_types = [] + @@default_event_types = [] + @@providers = Hash.new {|h,k| h[k]=[] } + + class << self + def map(&block) + yield self + end + + # Registers an activity provider + def register(event_type, options={}) + options.assert_valid_keys(:class_name, :default) + + event_type = event_type.to_s + providers = options[:class_name] || event_type.classify + providers = ([] << providers) unless providers.is_a?(Array) + + @@available_event_types << event_type unless @@available_event_types.include?(event_type) + @@default_event_types << event_type unless options[:default] == false + @@providers[event_type] += providers + end + end + end +end diff --git a/groups/lib/redmine/activity/fetcher.rb b/groups/lib/redmine/activity/fetcher.rb new file mode 100644 index 000000000..adaead564 --- /dev/null +++ b/groups/lib/redmine/activity/fetcher.rb @@ -0,0 +1,79 @@ +# Redmine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Activity + # Class used to retrieve activity events + class Fetcher + attr_reader :user, :project, :scope + + # Needs to be unloaded in development mode + @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } } + + def initialize(user, options={}) + options.assert_valid_keys(:project, :with_subprojects) + @user = user + @project = options[:project] + @options = options + + @scope = event_types + end + + # Returns an array of available event types + def event_types + return @event_types unless @event_types.nil? + + @event_types = Redmine::Activity.available_event_types + @event_types = @event_types.select {|o| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project + @event_types + end + + # Yields to filter the activity scope + def scope_select(&block) + @scope = @scope.select {|t| yield t } + end + + # Sets the scope + def scope=(s) + @scope = s & event_types + end + + # Resets the scope to the default scope + def default_scope! + @scope = Redmine::Activity.default_event_types + end + + # Returns an array of events for the given date range + def events(from, to) + e = [] + + @scope.each do |event_type| + constantized_providers(event_type).each do |provider| + e += provider.find_events(event_type, @user, from, to, @options) + end + end + e + end + + private + + def constantized_providers(event_type) + @@constantized_providers[event_type] + end + end + end +end diff --git a/groups/lib/redmine/core_ext/string/conversions.rb b/groups/lib/redmine/core_ext/string/conversions.rb index 7444445b0..41149f5ea 100644 --- a/groups/lib/redmine/core_ext/string/conversions.rb +++ b/groups/lib/redmine/core_ext/string/conversions.rb @@ -32,7 +32,7 @@ module Redmine #:nodoc: end # 2,5 => 2.5 s.gsub!(',', '.') - s.to_f + begin; Kernel.Float(s); rescue; nil; end end end end diff --git a/groups/lib/redmine/imap.rb b/groups/lib/redmine/imap.rb new file mode 100644 index 000000000..a6cd958cd --- /dev/null +++ b/groups/lib/redmine/imap.rb @@ -0,0 +1,51 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'net/imap' + +module Redmine + module IMAP + class << self + def check(imap_options={}, options={}) + host = imap_options[:host] || '127.0.0.1' + port = imap_options[:port] || '143' + ssl = !imap_options[:ssl].nil? + folder = imap_options[:folder] || 'INBOX' + + imap = Net::IMAP.new(host, port, ssl) + imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil? + imap.select(folder) + imap.search(['NOT', 'SEEN']).each do |message_id| + msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822'] + logger.debug "Receiving message #{message_id}" if logger && logger.debug? + if MailHandler.receive(msg, options) + imap.store(message_id, "+FLAGS", [:Seen, :Deleted]) + else + imap.store(message_id, "+FLAGS", [:Seen]) + end + end + imap.expunge + end + + private + + def logger + RAILS_DEFAULT_LOGGER + end + end + end +end diff --git a/groups/lib/redmine/menu_manager.rb b/groups/lib/redmine/menu_manager.rb index af54b41fe..f6431928e 100644 --- a/groups/lib/redmine/menu_manager.rb +++ b/groups/lib/redmine/menu_manager.rb @@ -80,9 +80,10 @@ module Redmine else item.url end - #url = (project && item.url.is_a?(Hash)) ? {item.param => project}.merge(item.url) : (item.url.is_a?(Symbol) ? send(item.url) : item.url) + caption = item.caption(project) + caption = l(caption) if caption.is_a?(Symbol) links << content_tag('li', - link_to(l(item.caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options))) + link_to(h(caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options))) end end links.empty? ? nil : content_tag('ul', links.join("\n")) @@ -91,11 +92,9 @@ module Redmine class << self def map(menu_name) - mapper = Mapper.new - yield mapper @items ||= {} - @items[menu_name.to_sym] ||= [] - @items[menu_name.to_sym] += mapper.items + mapper = Mapper.new(menu_name.to_sym, @items) + yield mapper end def items(menu_name) @@ -108,17 +107,46 @@ module Redmine end class Mapper + def initialize(menu, items) + items[menu] ||= [] + @menu = menu + @menu_items = items[menu] + end + + @@last_items_count = Hash.new {|h,k| h[k] = 0} + # Adds an item at the end of the menu. Available options: # * param: the parameter name that is used for the project id (default is :id) - # * if: a proc that is called before rendering the item, the item is displayed only if it returns true - # * caption: the localized string key that is used as the item label + # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true + # * caption that can be: + # * a localized string Symbol + # * a String + # * a Proc that can take the project as argument + # * before, after: specify where the menu item should be inserted (eg. :after => :activity) + # * last: menu item will stay at the end (eg. :last => true) # * html_options: a hash of html options that are passed to link_to def push(name, url, options={}) - items << MenuItem.new(name, url, options) + options = options.dup + + # menu item position + if before = options.delete(:before) + position = @menu_items.collect(&:name).index(before) + elsif after = options.delete(:after) + position = @menu_items.collect(&:name).index(after) + position += 1 unless position.nil? + elsif options.delete(:last) + position = @menu_items.size + @@last_items_count[@menu] += 1 + end + # default position + position ||= @menu_items.size - @@last_items_count[@menu] + + @menu_items.insert(position, MenuItem.new(name, url, options)) end - def items - @items ||= [] + # Removes a menu item + def delete(name) + @menu_items.delete_if {|i| i.name == name} end end @@ -133,13 +161,19 @@ module Redmine @url = url @condition = options[:if] @param = options[:param] || :id - @caption_key = options[:caption] + @caption = options[:caption] @html_options = options[:html] || {} end - def caption - # check if localized string exists on first render (after GLoc strings are loaded) - @caption ||= (@caption_key || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize)) + def caption(project=nil) + if @caption.is_a?(Proc) + c = @caption.call(project).to_s + c = @name.to_s.humanize if c.blank? + c + else + # check if localized string exists on first render (after GLoc strings are loaded) + @caption_key ||= (@caption || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize)) + end end end end diff --git a/groups/lib/redmine/platform.rb b/groups/lib/redmine/platform.rb new file mode 100644 index 000000000..f41b92f2e --- /dev/null +++ b/groups/lib/redmine/platform.rb @@ -0,0 +1,26 @@ +# Redmine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Platform + class << self + def mswin? + (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) + end + end + end +end diff --git a/groups/lib/redmine/plugin.rb b/groups/lib/redmine/plugin.rb index 36632c13e..cf6c194a2 100644 --- a/groups/lib/redmine/plugin.rb +++ b/groups/lib/redmine/plugin.rb @@ -116,6 +116,32 @@ module Redmine #:nodoc: self.instance_eval(&block) @project_module = nil end + + # Registers an activity provider. + # + # Options: + # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default) + # * <tt>:default</tt> - setting this option to false will make the events not displayed by default + # + # A model can provide several activity event types. + # + # Examples: + # register :news + # register :scrums, :class_name => 'Meeting' + # register :issues, :class_name => ['Issue', 'Journal'] + # + # Retrieving events: + # Associated model(s) must implement the find_events class method. + # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method. + # + # The following call should return all the scrum events visible by current user that occured in the 5 last days: + # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today) + # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only + # + # Note that :view_scrums permission is required to view these events in the activity view. + def activity_provider(*args) + Redmine::Activity.register(*args) + end # Returns +true+ if the plugin can be configured. def configurable? diff --git a/groups/lib/redmine/scm/adapters/abstract_adapter.rb b/groups/lib/redmine/scm/adapters/abstract_adapter.rb index 2c254d48d..9f400880d 100644 --- a/groups/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/groups/lib/redmine/scm/adapters/abstract_adapter.rb @@ -24,6 +24,29 @@ module Redmine end class AbstractAdapter #:nodoc: + class << self + # Returns the version of the scm client + # Eg: [1, 5, 0] or [] if unknown + def client_version + [] + end + + # Returns the version string of the scm client + # Eg: '1.5.0' or 'Unknown version' if unknown + def client_version_string + v = client_version || 'Unknown version' + v.is_a?(Array) ? v.join('.') : v.to_s + end + + # Returns true if the current client version is above + # or equals the given one + # If option is :unknown is set to true, it will return + # true if the client version is unknown + def client_version_above?(v, options={}) + ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown]) + end + end + def initialize(url, root_url=nil, login=nil, password=nil) @url = url @login = login if login && !login.empty? @@ -77,12 +100,16 @@ module Redmine def entries(path=nil, identifier=nil) return nil end + + def properties(path, identifier=nil) + return nil + end def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) return nil end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) return nil end @@ -94,15 +121,30 @@ module Redmine path ||= '' (path[0,1]!="/") ? "/#{path}" : path end + + def with_trailling_slash(path) + path ||= '' + (path[-1,1] == "/") ? path : "#{path}/" + end + + def without_leading_slash(path) + path ||= '' + path.gsub(%r{^/+}, '') + end + + def without_trailling_slash(path) + path ||= '' + (path[-1,1] == "/") ? path[0..-2] : path + end def shell_quote(str) - if RUBY_PLATFORM =~ /mswin/ + if Redmine::Platform.mswin? '"' + str.gsub(/"/, '\\"') + '"' else "'" + str.gsub(/'/, "'\"'\"'") + "'" end end - + private def retrieve_root_url info = self.info @@ -116,10 +158,18 @@ module Redmine end def logger - RAILS_DEFAULT_LOGGER + self.class.logger end - + def shellout(cmd, &block) + self.class.shellout(cmd, &block) + end + + def self.logger + RAILS_DEFAULT_LOGGER + end + + def self.shellout(cmd, &block) logger.debug "Shelling out: #{cmd}" if logger && logger.debug? begin IO.popen(cmd, "r+") do |io| @@ -127,11 +177,22 @@ module Redmine block.call(io) if block_given? end rescue Errno::ENOENT => e + msg = strip_credential(e.message) # The command failed, log it and re-raise - logger.error("SCM command failed: #{cmd}\n with: #{e.message}") - raise CommandFailed.new(e.message) + logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}") + raise CommandFailed.new(msg) end end + + # Hides username/password in a given command + def self.strip_credential(cmd) + q = (Redmine::Platform.mswin? ? '"' : "'") + cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx') + end + + def strip_credential(cmd) + self.class.strip_credential(cmd) + end end class Entries < Array @@ -208,167 +269,7 @@ module Redmine end end - - # A line of Diff - class Diff - attr_accessor :nb_line_left - attr_accessor :line_left - attr_accessor :nb_line_right - attr_accessor :line_right - attr_accessor :type_diff_right - attr_accessor :type_diff_left - def initialize () - self.nb_line_left = '' - self.nb_line_right = '' - self.line_left = '' - self.line_right = '' - self.type_diff_right = '' - self.type_diff_left = '' - end - - def inspect - puts '### Start Line Diff ###' - puts self.nb_line_left - puts self.line_left - puts self.nb_line_right - puts self.line_right - end - end - - class DiffTableList < Array - def initialize (diff, type="inline") - diff_table = DiffTable.new type - diff.each do |line| - if line =~ /^(---|\+\+\+) (.*)$/ - self << diff_table if diff_table.length > 1 - diff_table = DiffTable.new type - end - a = diff_table.add_line line - end - self << diff_table unless diff_table.empty? - self - end - end - - # Class for create a Diff - class DiffTable < Hash - attr_reader :file_name, :line_num_l, :line_num_r - - # Initialize with a Diff file and the type of Diff View - # The type view must be inline or sbs (side_by_side) - def initialize(type="inline") - @parsing = false - @nb_line = 1 - @start = false - @before = 'same' - @second = true - @type = type - end - - # Function for add a line of this Diff - def add_line(line) - unless @parsing - if line =~ /^(---|\+\+\+) (.*)$/ - @file_name = $2 - return false - elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ - @line_num_l = $5.to_i - @line_num_r = $2.to_i - @parsing = true - end - else - if line =~ /^[^\+\-\s@\\]/ - self.delete(self.keys.sort.last) - @parsing = false - return false - elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ - @line_num_l = $5.to_i - @line_num_r = $2.to_i - else - @nb_line += 1 if parse_line(line, @type) - end - end - return true - end - - def inspect - puts '### DIFF TABLE ###' - puts "file : #{file_name}" - self.each do |d| - d.inspect - end - end - - private - # Test if is a Side By Side type - def sbs?(type, func) - if @start and type == "sbs" - if @before == func and @second - tmp_nb_line = @nb_line - self[tmp_nb_line] = Diff.new - else - @second = false - tmp_nb_line = @start - @start += 1 - @nb_line -= 1 - end - else - tmp_nb_line = @nb_line - @start = @nb_line - self[tmp_nb_line] = Diff.new - @second = true - end - unless self[tmp_nb_line] - @nb_line += 1 - self[tmp_nb_line] = Diff.new - else - self[tmp_nb_line] - end - end - - # Escape the HTML for the diff - def escapeHTML(line) - CGI.escapeHTML(line) - end - - def parse_line(line, type="inline") - if line[0, 1] == "+" - diff = sbs? type, 'add' - @before = 'add' - diff.line_left = escapeHTML line[1..-1] - diff.nb_line_left = @line_num_l - diff.type_diff_left = 'diff_in' - @line_num_l += 1 - true - elsif line[0, 1] == "-" - diff = sbs? type, 'remove' - @before = 'remove' - diff.line_right = escapeHTML line[1..-1] - diff.nb_line_right = @line_num_r - diff.type_diff_right = 'diff_out' - @line_num_r += 1 - true - elsif line[0, 1] =~ /\s/ - @before = 'same' - @start = false - diff = Diff.new - diff.line_right = escapeHTML line[1..-1] - diff.nb_line_right = @line_num_r - diff.line_left = escapeHTML line[1..-1] - diff.nb_line_left = @line_num_l - self[@nb_line] = diff - @line_num_l += 1 - @line_num_r += 1 - true - elsif line[0, 1] = "\\" - true - else - false - end - end - end - class Annotate attr_reader :lines, :revisions diff --git a/groups/lib/redmine/scm/adapters/bazaar_adapter.rb b/groups/lib/redmine/scm/adapters/bazaar_adapter.rb index 2225a627c..ff69e3e6b 100644 --- a/groups/lib/redmine/scm/adapters/bazaar_adapter.rb +++ b/groups/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -132,7 +132,7 @@ module Redmine revisions end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) path ||= '' if identifier_to identifier_to = identifier_to.to_i @@ -147,7 +147,7 @@ module Redmine end end #return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def cat(path, identifier=nil) diff --git a/groups/lib/redmine/scm/adapters/cvs_adapter.rb b/groups/lib/redmine/scm/adapters/cvs_adapter.rb index 37920b599..089a6b153 100644 --- a/groups/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/groups/lib/redmine/scm/adapters/cvs_adapter.rb @@ -65,7 +65,7 @@ module Redmine entries = Entries.new cmd = "#{CVS_BIN} -d #{root_url} rls -ed" cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier - cmd << " #{path_with_project}" + cmd << " #{shell_quote path_with_project}" shellout(cmd) do |io| io.each_line(){|line| fields=line.chop.split('/',-1) @@ -110,7 +110,7 @@ module Redmine path_with_project="#{url}#{with_leading_slash(path)}" cmd = "#{CVS_BIN} -d #{root_url} rlog" cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from - cmd << " #{path_with_project}" + cmd << " #{shell_quote path_with_project}" shellout(cmd) do |io| state="entry_start" @@ -227,10 +227,10 @@ module Redmine end end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}" path_with_project="#{url}#{with_leading_slash(path)}" - cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}" + cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}" diff = [] shellout(cmd) do |io| io.each_line do |line| @@ -238,14 +238,16 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def cat(path, identifier=nil) identifier = (identifier) ? identifier : "HEAD" logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}" path_with_project="#{url}#{with_leading_slash(path)}" - cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}" + cmd = "#{CVS_BIN} -d #{root_url} co" + cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier + cmd << " -p #{shell_quote path_with_project}" cat = nil shellout(cmd) do |io| cat = io.read @@ -258,7 +260,7 @@ module Redmine identifier = (identifier) ? identifier : "HEAD" logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}" path_with_project="#{url}#{with_leading_slash(path)}" - cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}" + cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}" blame = Annotate.new shellout(cmd) do |io| io.each_line do |line| diff --git a/groups/lib/redmine/scm/adapters/darcs_adapter.rb b/groups/lib/redmine/scm/adapters/darcs_adapter.rb index a1d1867b1..4a5183f79 100644 --- a/groups/lib/redmine/scm/adapters/darcs_adapter.rb +++ b/groups/lib/redmine/scm/adapters/darcs_adapter.rb @@ -25,16 +25,36 @@ module Redmine # Darcs executable name DARCS_BIN = "darcs" + class << self + def client_version + @@client_version ||= (darcs_binary_version || []) + end + + def darcs_binary_version + cmd = "#{DARCS_BIN} --version" + version = nil + shellout(cmd) do |io| + # Read darcs version in first returned line + if m = io.gets.match(%r{((\d+\.)+\d+)}) + version = m[0].scan(%r{\d+}).collect(&:to_i) + end + end + return nil if $? && $?.exitstatus != 0 + version + end + end + def initialize(url, root_url=nil, login=nil, password=nil) @url = url @root_url = url end def supports_cat? - false + # cat supported in darcs 2.0.0 and higher + self.class.client_version_above?([2, 0, 0]) end - - # Get info about the svn repository + + # Get info about the darcs repository def info rev = revisions(nil,nil,nil,{:limit => 1}) rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil @@ -94,7 +114,7 @@ module Redmine revisions end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) path = '*' if path.blank? cmd = "#{DARCS_BIN} diff --repodir #{@url}" if identifier_to.nil? @@ -111,9 +131,22 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end + def cat(path, identifier=nil) + cmd = "#{DARCS_BIN} show content --repodir #{@url}" + cmd << " --match \"hash #{identifier}\"" if identifier + cmd << " #{shell_quote path}" + cat = nil + shellout(cmd) do |io| + io.binmode + cat = io.read + end + return nil if $? && $?.exitstatus != 0 + cat + end + private def entry_from_xml(element, path_prefix) diff --git a/groups/lib/redmine/scm/adapters/filesystem_adapter.rb b/groups/lib/redmine/scm/adapters/filesystem_adapter.rb new file mode 100644 index 000000000..99296a090 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/filesystem_adapter.rb @@ -0,0 +1,93 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# FileSystem adapter +# File written by Paul Rivier, at Demotera. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/abstract_adapter' +require 'find' + +module Redmine + module Scm + module Adapters + class FilesystemAdapter < AbstractAdapter + + + def initialize(url, root_url=nil, login=nil, password=nil) + @url = with_trailling_slash(url) + end + + def format_path_ends(path, leading=true, trailling=true) + path = leading ? with_leading_slash(path) : + without_leading_slash(path) + trailling ? with_trailling_slash(path) : + without_trailling_slash(path) + end + + def info + info = Info.new({:root_url => target(), + :lastrev => nil + }) + info + rescue CommandFailed + return nil + end + + def entries(path="", identifier=nil) + entries = Entries.new + Dir.new(target(path)).each do |e| + relative_path = format_path_ends((format_path_ends(path, + false, + true) + e), + false,false) + target = target(relative_path) + entries << + Entry.new({ :name => File.basename(e), + # below : list unreadable files, but dont link them. + :path => File.readable?(target) ? relative_path : "", + :kind => (File.directory?(target) ? 'dir' : 'file'), + :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first), + :lastrev => + Revision.new({:time => (File.mtime(target)).localtime, + }) + }) if File.exist?(target) and # paranoid test + %w{file directory}.include?(File.ftype(target)) and # avoid special types + not File.basename(e).match(/^\.+$/) # avoid . and .. + end + entries.sort_by_name + end + + def cat(path, identifier=nil) + File.new(target(path), "rb").read + end + + private + + # AbstractAdapter::target is implicitly made to quote paths. + # Here we do not shell-out, so we do not want quotes. + def target(path=nil) + #Prevent the use of .. + if path and !path.match(/(^|\/)\.\.(\/|$)/) + return "#{self.url}#{without_leading_slash(path)}" + end + return self.url + end + + end + end + end +end diff --git a/groups/lib/redmine/scm/adapters/git_adapter.rb b/groups/lib/redmine/scm/adapters/git_adapter.rb index 77604f283..30d624001 100644 --- a/groups/lib/redmine/scm/adapters/git_adapter.rb +++ b/groups/lib/redmine/scm/adapters/git_adapter.rb @@ -27,9 +27,13 @@ module Redmine # Get the revision of a particuliar file def get_rev (rev,path) - cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?) - cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if - rev=='latest' or rev.nil? + + if rev != 'latest' && !rev.nil? + cmd="#{GIT_BIN} --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" + else + branch = shellout("#{GIT_BIN} --git-dir #{target('')} branch") { |io| io.grep(/\*/)[0].strip.match(/\* (.*)/)[1] } + cmd="#{GIT_BIN} --git-dir #{target('')} log -1 #{branch} -- #{shell_quote path}" + end rev=[] i=0 shellout(cmd) do |io| @@ -135,10 +139,10 @@ module Redmine def revisions(path, identifier_from, identifier_to, options={}) revisions = Revisions.new cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw " + cmd << " --reverse" if options[:reverse] cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit] cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from cmd << " #{shell_quote identifier_to} " if identifier_to - #cmd << " HEAD " if !identifier_to shellout(cmd) do |io| files=[] changeset = {} @@ -151,13 +155,18 @@ module Redmine value = $1 if (parsing_descr == 1 || parsing_descr == 2) parsing_descr = 0 - revisions << Revision.new({:identifier => changeset[:commit], - :scmid => changeset[:commit], - :author => changeset[:author], - :time => Time.parse(changeset[:date]), - :message => changeset[:description], - :paths => files - }) + revision = Revision.new({:identifier => changeset[:commit], + :scmid => changeset[:commit], + :author => changeset[:author], + :time => Time.parse(changeset[:date]), + :message => changeset[:description], + :paths => files + }) + if block_given? + yield revision + else + revisions << revision + end changeset = {} files = [] revno = revno + 1 @@ -186,21 +195,27 @@ module Redmine end end - revisions << Revision.new({:identifier => changeset[:commit], + if changeset[:commit] + revision = Revision.new({:identifier => changeset[:commit], :scmid => changeset[:commit], :author => changeset[:author], :time => Time.parse(changeset[:date]), :message => changeset[:description], :paths => files - }) if changeset[:commit] - + }) + if block_given? + yield revision + else + revisions << revision + end + end end return nil if $? && $?.exitstatus != 0 revisions end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) path ||= '' if !identifier_to identifier_to = nil @@ -216,7 +231,7 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def annotate(path, identifier=nil) diff --git a/groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl b/groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl new file mode 100644 index 000000000..b3029e6ff --- /dev/null +++ b/groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl @@ -0,0 +1,12 @@ +changeset = 'This template must be used with --debug option\n' +changeset_quiet = 'This template must be used with --debug option\n' +changeset_verbose = 'This template must be used with --debug option\n' +changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' + +file = '<path action="M">{file|escape}</path>\n' +file_add = '<path action="A">{file_add|escape}</path>\n' +file_del = '<path action="D">{file_del|escape}</path>\n' +file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n' +tag = '<tag>{tag|escape}</tag>\n' +header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' +# footer="</log>"
\ No newline at end of file diff --git a/groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl new file mode 100644 index 000000000..3eef85016 --- /dev/null +++ b/groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl @@ -0,0 +1,12 @@ +changeset = 'This template must be used with --debug option\n' +changeset_quiet = 'This template must be used with --debug option\n' +changeset_verbose = 'This template must be used with --debug option\n' +changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n' + +file_mod = '<path action="M">{file_mod|escape}</path>\n' +file_add = '<path action="A">{file_add|escape}</path>\n' +file_del = '<path action="D">{file_del|escape}</path>\n' +file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n' +tag = '<tag>{tag|escape}</tag>\n' +header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' +# footer="</log>" diff --git a/groups/lib/redmine/scm/adapters/mercurial_adapter.rb b/groups/lib/redmine/scm/adapters/mercurial_adapter.rb index 6f42dda06..4eed776d8 100644 --- a/groups/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/groups/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -21,9 +21,45 @@ module Redmine module Scm module Adapters class MercurialAdapter < AbstractAdapter - + # Mercurial executable name HG_BIN = "hg" + TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" + TEMPLATE_NAME = "hg-template" + TEMPLATE_EXTENSION = "tmpl" + + class << self + def client_version + @@client_version ||= (hgversion || []) + end + + def hgversion + # The hg version is expressed either as a + # release number (eg 0.9.5 or 1.0) or as a revision + # id composed of 12 hexa characters. + theversion = hgversion_from_command_line + if theversion.match(/^\d+(\.\d+)+/) + theversion.split(".").collect(&:to_i) + end + end + + def hgversion_from_command_line + %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] + end + + def template_path + @@template_path ||= template_path_for(client_version) + end + + def template_path_for(version) + if ((version <=> [0,9,5]) > 0) || version.empty? + ver = "1.0" + else + ver = "0.9.5" + end + "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" + end + end def info cmd = "#{HG_BIN} -R #{target('')} root" @@ -33,8 +69,8 @@ module Redmine end return nil if $? && $?.exitstatus != 0 info = Info.new({:root_url => root_url.chomp, - :lastrev => revisions(nil,nil,nil,{:limit => 1}).last - }) + :lastrev => revisions(nil,nil,nil,{:limit => 1}).last + }) info rescue CommandFailed return nil @@ -43,68 +79,78 @@ module Redmine def entries(path=nil, identifier=nil) path ||= '' entries = Entries.new - cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate" - cmd << " -r #{identifier.to_i}" if identifier - cmd << " " + shell_quote('glob:**') + cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" + cmd << " -r " + (identifier ? identifier.to_s : "tip") + cmd << " " + shell_quote("path:#{path}") unless path.empty? shellout(cmd) do |io| io.each_line do |line| - e = line.chomp.split(%r{[\/\\]}) - entries << Entry.new({:name => e.first, - :path => (path.empty? ? e.first : "#{path}/#{e.first}"), - :kind => (e.size > 1 ? 'dir' : 'file'), - :lastrev => Revision.new - }) unless entries.detect{|entry| entry.name == e.first} + # HG uses antislashs as separator on Windows + line = line.gsub(/\\/, "/") + if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') + e ||= line + e = e.chomp.split(%r{[\/\\]}) + entries << Entry.new({:name => e.first, + :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"), + :kind => (e.size > 1 ? 'dir' : 'file'), + :lastrev => Revision.new + }) unless entries.detect{|entry| entry.name == e.first} + end end end return nil if $? && $?.exitstatus != 0 entries.sort_by_name end - - def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + + # Fetch the revisions by using a template file that + # makes Mercurial produce a xml output. + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) revisions = Revisions.new - cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log" + cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.class.template_path}" if identifier_from && identifier_to cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" elsif identifier_from cmd << " -r #{identifier_from.to_i}:" end cmd << " --limit #{options[:limit].to_i}" if options[:limit] + cmd << " #{path}" if path shellout(cmd) do |io| - changeset = {} - parsing_descr = false - line_feeds = 0 - - io.each_line do |line| - if line =~ /^(\w+):\s*(.*)$/ - key = $1 - value = $2 - if parsing_descr && line_feeds > 1 - parsing_descr = false - revisions << build_revision_from_changeset(changeset) - changeset = {} - end - if !parsing_descr - changeset.store key.to_sym, value - if $1 == "description" - parsing_descr = true - line_feeds = 0 - next + begin + # HG doesn't close the XML Document... + doc = REXML::Document.new(io.read << "</log>") + doc.elements.each("log/logentry") do |logentry| + paths = [] + copies = logentry.get_elements('paths/path-copied') + logentry.elements.each("paths/path") do |path| + # Detect if the added file is a copy + if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } + from_path = c.attributes['copyfrom-path'] + from_rev = logentry.attributes['revision'] end + paths << {:action => path.attributes['action'], + :path => "/#{path.text}", + :from_path => from_path ? "/#{from_path}" : nil, + :from_revision => from_rev ? from_rev : nil + } end + paths.sort! { |x,y| x[:path] <=> y[:path] } + + revisions << Revision.new({:identifier => logentry.attributes['revision'], + :scmid => logentry.attributes['node'], + :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), + :time => Time.parse(logentry.elements['date'].text).localtime, + :message => logentry.elements['msg'].text, + :paths => paths + }) end - if parsing_descr - changeset[:description] << line - line_feeds += 1 if line.chomp.empty? - end + rescue + logger.debug($!) end - # Add the last changeset if there is one left - revisions << build_revision_from_changeset(changeset) if changeset[:date] end return nil if $? && $?.exitstatus != 0 revisions end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) path ||= '' if identifier_to identifier_to = identifier_to.to_i @@ -120,12 +166,12 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def cat(path, identifier=nil) cmd = "#{HG_BIN} -R #{target('')} cat" - cmd << " -r #{identifier.to_i}" if identifier + cmd << " -r " + (identifier ? identifier.to_s : "tip") cmd << " #{target(path)}" cat = nil shellout(cmd) do |io| @@ -140,6 +186,7 @@ module Redmine path ||= '' cmd = "#{HG_BIN} -R #{target('')}" cmd << " annotate -n -u" + cmd << " -r " + (identifier ? identifier.to_s : "tip") cmd << " -r #{identifier.to_i}" if identifier cmd << " #{target(path)}" blame = Annotate.new @@ -152,47 +199,6 @@ module Redmine return nil if $? && $?.exitstatus != 0 blame end - - private - - # Builds a revision objet from the changeset returned by hg command - def build_revision_from_changeset(changeset) - rev_id = changeset[:changeset].to_s.split(':').first.to_i - - # Changes - paths = (rev_id == 0) ? - # Can't get changes for revision 0 with hg status - changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} : - status(rev_id) - - Revision.new({:identifier => rev_id, - :scmid => changeset[:changeset].to_s.split(':').last, - :author => changeset[:user], - :time => Time.parse(changeset[:date]), - :message => changeset[:description], - :paths => paths - }) - end - - # Returns the file changes for a given revision - def status(rev_id) - cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}" - result = [] - shellout(cmd) do |io| - io.each_line do |line| - action, file = line.chomp.split - next unless action && file - file.gsub!("\\", "/") - case action - when 'R' - result << { :action => 'D' , :path => "/#{file}" } - else - result << { :action => action, :path => "/#{file}" } - end - end - end - result - end end end end diff --git a/groups/lib/redmine/scm/adapters/subversion_adapter.rb b/groups/lib/redmine/scm/adapters/subversion_adapter.rb index 40c7eb3f1..2b7f0192e 100644 --- a/groups/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/groups/lib/redmine/scm/adapters/subversion_adapter.rb @@ -26,6 +26,25 @@ module Redmine # SVN executable name
SVN_BIN = "svn"
+ class << self
+ def client_version
+ @@client_version ||= (svn_binary_version || [])
+ end
+
+ def svn_binary_version
+ cmd = "#{SVN_BIN} --version"
+ version = nil
+ shellout(cmd) do |io|
+ # Read svn version in first returned line
+ if m = io.gets.match(%r{((\d+\.)+\d+)})
+ version = m[0].scan(%r{\d+}).collect(&:to_i)
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ version
+ end
+ end
+
# Get info about the svn repository
def info
cmd = "#{SVN_BIN} info --xml #{target('')}"
@@ -64,6 +83,9 @@ module Redmine begin
doc = REXML::Document.new(output)
doc.elements.each("lists/list/entry") do |entry|
+ # Skip directory if there is no commit date (usually that
+ # means that we don't have read access to it)
+ next if entry.attributes['kind'] == 'dir' && entry.elements['commit'].elements['date'].nil?
entries << Entry.new({:name => entry.elements['name'].text,
:path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
:kind => entry.attributes['kind'],
@@ -84,7 +106,29 @@ module Redmine logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
entries.sort_by_name
end
-
+
+ def properties(path, identifier=nil)
+ # proplist xml output supported in svn 1.5.0 and higher
+ return nil unless self.class.client_version_above?([1, 5, 0])
+
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
+ cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
+ cmd << credentials_string
+ properties = {}
+ shellout(cmd) do |io|
+ output = io.read
+ begin
+ doc = REXML::Document.new(output)
+ doc.elements.each("properties/target/property") do |property|
+ properties[ property.attributes['name'] ] = property.text
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ properties
+ end
+
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path ||= ''
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
@@ -139,7 +183,7 @@ module Redmine end
end
return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
def cat(path, identifier=nil)
diff --git a/groups/lib/redmine/unified_diff.rb b/groups/lib/redmine/unified_diff.rb new file mode 100644 index 000000000..aa8994454 --- /dev/null +++ b/groups/lib/redmine/unified_diff.rb @@ -0,0 +1,178 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + # Class used to parse unified diffs + class UnifiedDiff < Array + def initialize(diff, type="inline") + diff_table = DiffTable.new type + diff.each do |line| + if line =~ /^(---|\+\+\+) (.*)$/ + self << diff_table if diff_table.length > 1 + diff_table = DiffTable.new type + end + a = diff_table.add_line line + end + self << diff_table unless diff_table.empty? + self + end + end + + # Class that represents a file diff + class DiffTable < Hash + attr_reader :file_name, :line_num_l, :line_num_r + + # Initialize with a Diff file and the type of Diff View + # The type view must be inline or sbs (side_by_side) + def initialize(type="inline") + @parsing = false + @nb_line = 1 + @start = false + @before = 'same' + @second = true + @type = type + end + + # Function for add a line of this Diff + def add_line(line) + unless @parsing + if line =~ /^(---|\+\+\+) (.*)$/ + @file_name = $2 + return false + elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ + @line_num_l = $5.to_i + @line_num_r = $2.to_i + @parsing = true + end + else + if line =~ /^[^\+\-\s@\\]/ + @parsing = false + return false + elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ + @line_num_l = $5.to_i + @line_num_r = $2.to_i + else + @nb_line += 1 if parse_line(line, @type) + end + end + return true + end + + def inspect + puts '### DIFF TABLE ###' + puts "file : #{file_name}" + self.each do |d| + d.inspect + end + end + + private + # Test if is a Side By Side type + def sbs?(type, func) + if @start and type == "sbs" + if @before == func and @second + tmp_nb_line = @nb_line + self[tmp_nb_line] = Diff.new + else + @second = false + tmp_nb_line = @start + @start += 1 + @nb_line -= 1 + end + else + tmp_nb_line = @nb_line + @start = @nb_line + self[tmp_nb_line] = Diff.new + @second = true + end + unless self[tmp_nb_line] + @nb_line += 1 + self[tmp_nb_line] = Diff.new + else + self[tmp_nb_line] + end + end + + # Escape the HTML for the diff + def escapeHTML(line) + CGI.escapeHTML(line) + end + + def parse_line(line, type="inline") + if line[0, 1] == "+" + diff = sbs? type, 'add' + @before = 'add' + diff.line_left = escapeHTML line[1..-1] + diff.nb_line_left = @line_num_l + diff.type_diff_left = 'diff_in' + @line_num_l += 1 + true + elsif line[0, 1] == "-" + diff = sbs? type, 'remove' + @before = 'remove' + diff.line_right = escapeHTML line[1..-1] + diff.nb_line_right = @line_num_r + diff.type_diff_right = 'diff_out' + @line_num_r += 1 + true + elsif line[0, 1] =~ /\s/ + @before = 'same' + @start = false + diff = Diff.new + diff.line_right = escapeHTML line[1..-1] + diff.nb_line_right = @line_num_r + diff.line_left = escapeHTML line[1..-1] + diff.nb_line_left = @line_num_l + self[@nb_line] = diff + @line_num_l += 1 + @line_num_r += 1 + true + elsif line[0, 1] = "\\" + true + else + false + end + end + end + + # A line of diff + class Diff + attr_accessor :nb_line_left + attr_accessor :line_left + attr_accessor :nb_line_right + attr_accessor :line_right + attr_accessor :type_diff_right + attr_accessor :type_diff_left + + def initialize() + self.nb_line_left = '' + self.nb_line_right = '' + self.line_left = '' + self.line_right = '' + self.type_diff_right = '' + self.type_diff_left = '' + end + + def inspect + puts '### Start Line Diff ###' + puts self.nb_line_left + puts self.line_left + puts self.nb_line_right + puts self.line_right + end + end +end diff --git a/groups/lib/redmine/wiki_formatting.rb b/groups/lib/redmine/wiki_formatting.rb index 79da2a38a..8c18d547f 100644 --- a/groups/lib/redmine/wiki_formatting.rb +++ b/groups/lib/redmine/wiki_formatting.rb @@ -26,7 +26,7 @@ module Redmine class TextileFormatter < RedCloth # auto_link rule after textile rules so that it doesn't break !image_url! tags - RULES = [:textile, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] + RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] def initialize(*args) super @@ -45,7 +45,7 @@ module Redmine # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> def hard_break( text ) - text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks + text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks end # Patch to add code highlighting support to RedCloth @@ -56,7 +56,7 @@ module Redmine content = @pre_list[$1.to_i] if content.match(/<code\s+class="(\w+)">\s?(.+)/m) content = "<code class=\"#{$1} CodeRay\">" + - CodeRay.scan($2, $1).html(:escape => false, :line_numbers => :inline) + CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline) end content end @@ -65,10 +65,22 @@ module Redmine # Patch to add 'table of content' support to RedCloth def textile_p_withtoc(tag, atts, cite, content) - if tag =~ /^h(\d)$/ - @toc << [$1.to_i, content] + # removes wiki links from the item + toc_item = content.gsub(/(\[\[|\]\])/, '') + # removes styles + # eg. %{color:red}Triggers% => Triggers + toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1' + + # replaces non word caracters by dashes + anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + + unless anchor.blank? + if tag =~ /^h(\d)$/ + @toc << [$1.to_i, anchor, toc_item] + end + atts << " id=\"#{anchor}\"" + content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a>" end - content = "<a name=\"#{@toc.length}\" class=\"wiki-page\"></a>" + content textile_p(tag, atts, cite, content) end @@ -81,13 +93,12 @@ module Redmine div_class = 'toc' div_class << ' right' if $1 == '>' div_class << ' left' if $1 == '<' - out = "<div class=\"#{div_class}\">" - @toc.each_with_index do |heading, index| - # remove wiki links from the item - toc_item = heading.last.gsub(/(\[\[|\]\])/, '') - out << "<a href=\"##{index+1}\" class=\"heading#{heading.first}\">#{toc_item}</a>" + out = "<ul class=\"#{div_class}\">" + @toc.each do |heading| + level, anchor, toc_item = heading + out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n" end - out << '</div>' + out << '</ul>' out end end @@ -126,6 +137,7 @@ module Redmine ) ( (?:https?://)| # protocol spec, or + (?:ftp://)| (?:www\.) # www.* ) ( @@ -149,12 +161,16 @@ module Redmine end end end - + # Turns all email addresses into clickable links (code from Rails). def inline_auto_mailto(text) text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do - text = $1 - %{<a href="mailto:#{$1}" class="email">#{text}</a>} + mail = $1 + if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) + mail + else + %{<a href="mailto:#{mail}" class="email">#{mail}</a>} + end end end end diff --git a/groups/lib/redmine/wiki_formatting/macros.rb b/groups/lib/redmine/wiki_formatting/macros.rb index 0848aee4e..adfc590e4 100644 --- a/groups/lib/redmine/wiki_formatting/macros.rb +++ b/groups/lib/redmine/wiki_formatting/macros.rb @@ -77,6 +77,12 @@ module Redmine content_tag('dl', out) end + desc "Displays a list of child pages." + macro :child_pages do |obj, args| + raise 'This macro applies to wiki pages only.' unless obj.is_a?(WikiContent) + render_page_hierarchy(obj.page.descendants.group_by(&:parent_id), obj.page.id) + end + desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}" macro :include do |obj, args| project = @project diff --git a/groups/lib/tabular_form_builder.rb b/groups/lib/tabular_form_builder.rb index 5b331fe3f..88e35a6d2 100644 --- a/groups/lib/tabular_form_builder.rb +++ b/groups/lib/tabular_form_builder.rb @@ -22,7 +22,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder 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 + super end (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector| diff --git a/groups/lib/tasks/email.rake b/groups/lib/tasks/email.rake new file mode 100644 index 000000000..a37b3e197 --- /dev/null +++ b/groups/lib/tasks/email.rake @@ -0,0 +1,105 @@ +# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+namespace :redmine do
+ namespace :email do
+
+ desc <<-END_DESC
+Read an email from standard input.
+
+Issue attributes control options:
+ project=PROJECT identifier of the target project
+ tracker=TRACKER name of the target tracker
+ category=CATEGORY name of the target category
+ priority=PRIORITY name of the target priority
+ allow_override=ATTRS allow email content to override attributes
+ specified by previous options
+ ATTRS is a comma separated list of attributes
+
+Examples:
+ # No project specified. Emails MUST contain the 'Project' keyword:
+ rake redmine:email:read RAILS_ENV="production" < raw_email
+
+ # Fixed project and default tracker specified, but emails can override
+ # both tracker and priority attributes:
+ rake redmine:email:read RAILS_ENV="production" \\
+ project=foo \\
+ tracker=bug \\
+ allow_override=tracker,priority < raw_email
+END_DESC
+
+ task :read => :environment do
+ options = { :issue => {} }
+ %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
+ options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
+
+ MailHandler.receive(STDIN.read, options)
+ end
+
+ desc <<-END_DESC
+Read emails from an IMAP server.
+
+Available IMAP options:
+ host=HOST IMAP server host (default: 127.0.0.1)
+ port=PORT IMAP server port (default: 143)
+ ssl=SSL Use SSL? (default: false)
+ username=USERNAME IMAP account
+ password=PASSWORD IMAP password
+ folder=FOLDER IMAP folder to read (default: INBOX)
+
+Issue attributes control options:
+ project=PROJECT identifier of the target project
+ tracker=TRACKER name of the target tracker
+ category=CATEGORY name of the target category
+ priority=PRIORITY name of the target priority
+ allow_override=ATTRS allow email content to override attributes
+ specified by previous options
+ ATTRS is a comma separated list of attributes
+
+Examples:
+ # No project specified. Emails MUST contain the 'Project' keyword:
+
+ rake redmine:email:receive_iamp RAILS_ENV="production" \\
+ host=imap.foo.bar username=redmine@example.net password=xxx
+
+
+ # Fixed project and default tracker specified, but emails can override
+ # both tracker and priority attributes:
+
+ rake redmine:email:receive_iamp RAILS_ENV="production" \\
+ host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
+ project=foo \\
+ tracker=bug \\
+ allow_override=tracker,priority
+END_DESC
+
+ task :receive_imap => :environment do
+ imap_options = {:host => ENV['host'],
+ :port => ENV['port'],
+ :ssl => ENV['ssl'],
+ :username => ENV['username'],
+ :password => ENV['password'],
+ :folder => ENV['folder']}
+
+ options = { :issue => {} }
+ %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
+ options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
+
+ Redmine::IMAP.check(imap_options, options)
+ end
+ end
+end
diff --git a/groups/lib/tasks/migrate_from_trac.rake b/groups/lib/tasks/migrate_from_trac.rake index 7fe1f09ac..880964ff8 100644 --- a/groups/lib/tasks/migrate_from_trac.rake +++ b/groups/lib/tasks/migrate_from_trac.rake @@ -92,12 +92,17 @@ namespace :redmine do set_table_name :milestone def due - if read_attribute(:due) > 0 + if read_attribute(:due) && read_attribute(:due) > 0 Time.at(read_attribute(:due)).to_date else nil end end + + def description + # Attribute is named descr in Trac v0.8.x + has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description) + end end class TracTicketCustom < ActiveRecord::Base @@ -126,6 +131,10 @@ namespace :redmine do File.open("#{trac_fullpath}", 'rb').read end + def description + read_attribute(:description).to_s.slice(0,255) + end + private def trac_fullpath attachment_type = read_attribute(:type) @@ -140,7 +149,10 @@ namespace :redmine do # ticket changes: only migrate status changes and comments has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket - has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'" + has_many :attachments, :class_name => "TracAttachment", + :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" + + " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" + + ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\'' has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket def ticket_type @@ -177,7 +189,10 @@ namespace :redmine do set_table_name :wiki set_primary_key :name - has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'" + has_many :attachments, :class_name => "TracAttachment", + :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" + + " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" + + ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\'' def self.columns # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0) @@ -191,6 +206,10 @@ namespace :redmine do set_table_name :permission end + class TracSessionAttribute < ActiveRecord::Base + set_table_name :session_attribute + end + def self.find_or_create_user(username, project_member = false) return User.anonymous if username.blank? @@ -198,10 +217,23 @@ namespace :redmine do if !u # Create a new user if not found mail = username[0,limit_for(User, 'mail')] + if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email') + mail = mail_attr.value + end mail = "#{mail}@foo.bar" unless mail.include?("@") - u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'), - :lastname => '-', - :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-') + + name = username + if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name') + name = name_attr.value + end + name =~ (/(.*)(\s+\w+)?/) + fn = $1.strip + ln = ($2 || '-').strip + + u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'), + :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'), + :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-') + u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-') u.password = 'trac' u.admin = true if TracPermission.find_by_username_and_action(username, 'admin') @@ -233,7 +265,8 @@ namespace :redmine do text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} - text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"} # Links to pages UsingJustWikiCaps text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]') @@ -408,6 +441,7 @@ namespace :redmine do a.file = attachment a.author = find_or_create_user(attachment.author) a.container = i + a.description = attachment.description migrated_ticket_attachments += 1 if a.save end @@ -456,6 +490,7 @@ namespace :redmine do a = Attachment.new :created_on => attachment.time a.file = attachment a.author = find_or_create_user(attachment.author) + a.description = attachment.description a.container = p migrated_wiki_attachments += 1 if a.save end diff --git a/groups/lib/tasks/reminder.rake b/groups/lib/tasks/reminder.rake new file mode 100644 index 000000000..73844fb79 --- /dev/null +++ b/groups/lib/tasks/reminder.rake @@ -0,0 +1,39 @@ +# redMine - project management software +# Copyright (C) 2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +desc <<-END_DESC +Send reminders about issues due in the next days. + +Available options: + * days => number of days to remind about (defaults to 7) + * tracker => id of tracker (defaults to all trackers) + * project => id or identifier of project (defaults to all projects) + +Example: + rake redmine:send_reminders days=7 RAILS_ENV="production" +END_DESC + +namespace :redmine do + task :send_reminders => :environment do + options = {} + options[:days] = ENV['days'].to_i if ENV['days'] + options[:project] = ENV['project'] if ENV['project'] + options[:tracker] = ENV['tracker'].to_i if ENV['tracker'] + + Mailer.reminders(options) + end +end diff --git a/groups/lib/tasks/testing.rake b/groups/lib/tasks/testing.rake new file mode 100644 index 000000000..42f756f68 --- /dev/null +++ b/groups/lib/tasks/testing.rake @@ -0,0 +1,46 @@ +### From http://svn.geekdaily.org/public/rails/plugins/generally_useful/tasks/coverage_via_rcov.rake + +### Inspired by http://blog.labratz.net/articles/2006/12/2/a-rake-task-for-rcov +begin + require 'rcov/rcovtask' + + rcov_options = "--rails --aggregate test/coverage.data --exclude '/gems/'" + + namespace :test do + desc "Aggregate code coverage for all tests" + Rcov::RcovTask.new('coverage') do |t| + t.libs << 'test' + t.test_files = FileList['test/{unit,integration,functional}/*_test.rb'] + t.verbose = true + t.rcov_opts << rcov_options + end + + namespace :coverage do + desc "Delete coverage test data" + task :clean do + rm_f "test/coverage.data" + rm_rf "test/coverage" + end + + desc "Aggregate code coverage for all tests with HTML output" + Rcov::RcovTask.new('html') do |t| + t.libs << 'test' + t.test_files = FileList['test/{unit,integration,functional}/*_test.rb'] + t.output_dir = "test/coverage" + t.verbose = true + t.rcov_opts << rcov_options + end + + desc "Open the HTML coverage report" + task :show_results do + system "open test/coverage/index.html" + end + + task :full => "test:coverage:clean" + task :full => "test:coverage:html" + task :full => "test:coverage:show_results" + end + end +rescue LoadError + # rcov not available +end diff --git a/groups/public/help/wiki_syntax.html b/groups/public/help/wiki_syntax.html index 6a0e10022..846fe1bf7 100644 --- a/groups/public/help/wiki_syntax.html +++ b/groups/public/help/wiki_syntax.html @@ -22,13 +22,13 @@ table td h3 { font-size: 1.2em; text-align: left; } <table width="100%"> <tr><th colspan="3">Font Styles</th></tr> -<tr><th><img src="../../images/jstoolbar/bt_strong.png" style="border: 1px solid #bbb;" alt="Strong" /></th><td width="50%">*Strong*</td><td width="50%"><strong>Strong</strong></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_em.png" style="border: 1px solid #bbb;" alt="Italic" /></th><td>_Italic_</td><td><em>Italic</em></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_ins.png" style="border: 1px solid #bbb;" alt="Underline" /></th><td>+Underline+</td><td><ins>Underline</ins></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_del.png" style="border: 1px solid #bbb;" alt="Deleted" /></th><td>-Deleted-</td><td><del>Deleted</del></td></tr> +<tr><th><img src="../images/jstoolbar/bt_strong.png" style="border: 1px solid #bbb;" alt="Strong" /></th><td width="50%">*Strong*</td><td width="50%"><strong>Strong</strong></td></tr> +<tr><th><img src="../images/jstoolbar/bt_em.png" style="border: 1px solid #bbb;" alt="Italic" /></th><td>_Italic_</td><td><em>Italic</em></td></tr> +<tr><th><img src="../images/jstoolbar/bt_ins.png" style="border: 1px solid #bbb;" alt="Underline" /></th><td>+Underline+</td><td><ins>Underline</ins></td></tr> +<tr><th><img src="../images/jstoolbar/bt_del.png" style="border: 1px solid #bbb;" alt="Deleted" /></th><td>-Deleted-</td><td><del>Deleted</del></td></tr> <tr><th></th><td>??Quote??</td><td><cite>Quote</cite></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_code.png" style="border: 1px solid #bbb;" alt="Inline Code" /></th><td>@Inline Code@</td><td><code>Inline Code</code></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_pre.png" style="border: 1px solid #bbb;" alt="Preformatted text" /></th><td><pre><br /> lines<br /> of code<br /></pre></td><td> +<tr><th><img src="../images/jstoolbar/bt_code.png" style="border: 1px solid #bbb;" alt="Inline Code" /></th><td>@Inline Code@</td><td><code>Inline Code</code></td></tr> +<tr><th><img src="../images/jstoolbar/bt_pre.png" style="border: 1px solid #bbb;" alt="Preformatted text" /></th><td><pre><br /> lines<br /> of code<br /></pre></td><td> <pre> lines of code @@ -36,27 +36,27 @@ table td h3 { font-size: 1.2em; text-align: left; } </td></tr> <tr><th colspan="3">Lists</th></tr> -<tr><th><img src="../../images/jstoolbar/bt_ul.png" style="border: 1px solid #bbb;" alt="Unordered list" /></th><td>* Item 1<br />* Item 2</td><td><ul><li>Item 1</li><li>Item 2</li></ul></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_ol.png" style="border: 1px solid #bbb;" alt="Ordered list" /></th><td># Item 1<br /># Item 2</td><td><ol><li>Item 1</li><li>Item 2</li></ol></td></tr> +<tr><th><img src="../images/jstoolbar/bt_ul.png" style="border: 1px solid #bbb;" alt="Unordered list" /></th><td>* Item 1<br />* Item 2</td><td><ul><li>Item 1</li><li>Item 2</li></ul></td></tr> +<tr><th><img src="../images/jstoolbar/bt_ol.png" style="border: 1px solid #bbb;" alt="Ordered list" /></th><td># Item 1<br /># Item 2</td><td><ol><li>Item 1</li><li>Item 2</li></ol></td></tr> <tr><th colspan="3">Headings</th></tr> -<tr><th><img src="../../images/jstoolbar/bt_h1.png" style="border: 1px solid #bbb;" alt="Heading 1" /></th><td>h1. Title 1</td><td><h1>Title 1</h1></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_h2.png" style="border: 1px solid #bbb;" alt="Heading 2" /></th><td>h2. Title 2</td><td><h2>Title 2</h2></td></tr> -<tr><th><img src="../../images/jstoolbar/bt_h3.png" style="border: 1px solid #bbb;" alt="Heading 3" /></th><td>h3. Title 3</td><td><h3>Title 3</h3></td></tr> +<tr><th><img src="../images/jstoolbar/bt_h1.png" style="border: 1px solid #bbb;" alt="Heading 1" /></th><td>h1. Title 1</td><td><h1>Title 1</h1></td></tr> +<tr><th><img src="../images/jstoolbar/bt_h2.png" style="border: 1px solid #bbb;" alt="Heading 2" /></th><td>h2. Title 2</td><td><h2>Title 2</h2></td></tr> +<tr><th><img src="../images/jstoolbar/bt_h3.png" style="border: 1px solid #bbb;" alt="Heading 3" /></th><td>h3. Title 3</td><td><h3>Title 3</h3></td></tr> <tr><th colspan="3">Links</th></tr> <tr><th></th><td>http://foo.bar</td><td><a href="#">http://foo.bar</a></td></tr> <tr><th></th><td>"Foo":http://foo.bar</td><td><a href="#">Foo</a></td></tr> <tr><th colspan="3">Redmine links</th></tr> -<tr><th><img src="../../images/jstoolbar/bt_link.png" style="border: 1px solid #bbb;" alt="Link to a Wiki page" /></th><td>[[Wiki page]]</td><td><a href="#">Wiki page</a></td></tr> +<tr><th><img src="../images/jstoolbar/bt_link.png" style="border: 1px solid #bbb;" alt="Link to a Wiki page" /></th><td>[[Wiki page]]</td><td><a href="#">Wiki page</a></td></tr> <tr><th></th><td>Issue #12</td><td>Issue <a href="#">#12</a></td></tr> <tr><th></th><td>Revision r43</td><td>Revision <a href="#">r43</a></td></tr> <tr><th></th><td>commit:"f30e13e43"</td><td><a href="#">f30e13e4</a></td></tr> <tr><th></th><td>source:some/file</td><td><a href="#">source:some/file</a></td></tr> <tr><th colspan="3">Inline images</th></tr> -<tr><th><img src="../../images/jstoolbar/bt_img.png" style="border: 1px solid #bbb;" alt="Image" /></th><td>!<em>image_url</em>!</td><td></td></tr> +<tr><th><img src="../images/jstoolbar/bt_img.png" style="border: 1px solid #bbb;" alt="Image" /></th><td>!<em>image_url</em>!</td><td></td></tr> <tr><th></th><td>!<em>attached_image</em>!</td><td></td></tr> </table> diff --git a/groups/public/images/bullet_toggle_minus.png b/groups/public/images/bullet_toggle_minus.png Binary files differnew file mode 100644 index 000000000..5ce75938f --- /dev/null +++ b/groups/public/images/bullet_toggle_minus.png diff --git a/groups/public/images/bullet_toggle_plus.png b/groups/public/images/bullet_toggle_plus.png Binary files differnew file mode 100644 index 000000000..b3603d30a --- /dev/null +++ b/groups/public/images/bullet_toggle_plus.png diff --git a/groups/public/images/comment.png b/groups/public/images/comment.png Binary files differnew file mode 100644 index 000000000..7bc9233ea --- /dev/null +++ b/groups/public/images/comment.png diff --git a/groups/public/images/expand.png b/groups/public/images/expand.png Binary files differdeleted file mode 100644 index 3e3aaa441..000000000 --- a/groups/public/images/expand.png +++ /dev/null diff --git a/groups/public/images/jstoolbar/bt_bq.png b/groups/public/images/jstoolbar/bt_bq.png Binary files differnew file mode 100644 index 000000000..c3af4e07f --- /dev/null +++ b/groups/public/images/jstoolbar/bt_bq.png diff --git a/groups/public/images/jstoolbar/bt_bq_remove.png b/groups/public/images/jstoolbar/bt_bq_remove.png Binary files differnew file mode 100644 index 000000000..05d5ff7c7 --- /dev/null +++ b/groups/public/images/jstoolbar/bt_bq_remove.png diff --git a/groups/public/images/locked.png b/groups/public/images/locked.png Binary files differindex c2789e35c..82d629961 100644 --- a/groups/public/images/locked.png +++ b/groups/public/images/locked.png diff --git a/groups/public/images/projects.png b/groups/public/images/projects.png Binary files differindex 244c896f0..073c7219d 100644 --- a/groups/public/images/projects.png +++ b/groups/public/images/projects.png diff --git a/groups/public/images/ticket_note.png b/groups/public/images/ticket_note.png Binary files differnew file mode 100644 index 000000000..c69db223f --- /dev/null +++ b/groups/public/images/ticket_note.png diff --git a/groups/public/images/unlock.png b/groups/public/images/unlock.png Binary files differindex e0d414978..f15fead72 100644 --- a/groups/public/images/unlock.png +++ b/groups/public/images/unlock.png diff --git a/groups/public/javascripts/application.js b/groups/public/javascripts/application.js index 4e8849842..3becbeb21 100644 --- a/groups/public/javascripts/application.js +++ b/groups/public/javascripts/application.js @@ -2,14 +2,27 @@ Copyright (C) 2006-2008 Jean-Philippe Lang */ function checkAll (id, checked) { - var el = document.getElementById(id); - for (var i = 0; i < el.elements.length; i++) { - if (el.elements[i].disabled==false) { - el.elements[i].checked = checked; + var els = Element.descendants(id); + for (var i = 0; i < els.length; i++) { + if (els[i].disabled==false) { + els[i].checked = checked; } } } +function toggleCheckboxesBySelector(selector) { + boxes = $$(selector); + var all_checked = true; + for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } + for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; } +} + +function showAndScrollTo(id, focus) { + Element.show(id); + if (focus!=null) { Form.Element.focus(focus); } + Element.scrollTo(id); +} + var fileFieldCount = 1; function addFileField() { @@ -56,7 +69,7 @@ function setPredecessorFieldsVisibility() { function promptToRemote(text, param, url) { value = prompt(text + ':'); if (value) { - new Ajax.Request(url + '?' + param + '=' + value, {asynchronous:true, evalScripts:true}); + new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true}); return false; } } @@ -107,6 +120,15 @@ function scmEntryLoaded(id) { Element.removeClassName(id, 'loading'); } +function randomKey(size) { + var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); + var key = ''; + for (i = 0; i < size; i++) { + key += chars[Math.floor(Math.random() * chars.length)]; + } + return key; +} + /* shows and hides ajax indicator */ Ajax.Responders.register({ onCreate: function(){ diff --git a/groups/public/javascripts/calendar/lang/calendar-he.js b/groups/public/javascripts/calendar/lang/calendar-he.js index bd92e0073..9d4c87db0 100644 --- a/groups/public/javascripts/calendar/lang/calendar-he.js +++ b/groups/public/javascripts/calendar/lang/calendar-he.js @@ -113,7 +113,7 @@ Calendar._TT["DAY_FIRST"] = "הצג %s קוד×"; // This may be locale-dependent. It specifies the week-end days, as an array // of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 // means Monday, etc. -Calendar._TT["WEEKEND"] = "6,7"; +Calendar._TT["WEEKEND"] = "5,6"; Calendar._TT["CLOSE"] = "סגור"; Calendar._TT["TODAY"] = "היו×"; diff --git a/groups/public/javascripts/calendar/lang/calendar-hu.js b/groups/public/javascripts/calendar/lang/calendar-hu.js new file mode 100644 index 000000000..0e219c123 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-hu.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar HU language +// Author: Takács Gábor +// Encoding: UTF-8 +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("Vasárnap", + "HétfÅ‘", + "Kedd", + "Szerda", + "Csütörtök", + "Péntek", + "Szombat", + "Vasárnap"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("Vas", + "Hét", + "Ked", + "Sze", + "Csü", + "Pén", + "Szo", + "Vas"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("Január", + "Február", + "Március", + "Ãprilis", + "Május", + "Június", + "Július", + "Augusztus", + "Szeptember", + "Október", + "November", + "December"); + +// short month names +Calendar._SMN = new Array +("Jan", + "Feb", + "Már", + "Ãpr", + "Máj", + "Jún", + "Júl", + "Aug", + "Szep", + "Okt", + "Nov", + "Dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "A naptár leÃrása"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "ElÅ‘zÅ‘ év (nyomvatart = menü)"; +Calendar._TT["PREV_MONTH"] = "ElÅ‘zÅ‘ hónap (nyomvatart = menü)"; +Calendar._TT["GO_TODAY"] = "Irány a Ma"; +Calendar._TT["NEXT_MONTH"] = "KövetkezÅ‘ hónap (nyomvatart = menü)"; +Calendar._TT["NEXT_YEAR"] = "KövetkezÅ‘ év (nyomvatart = menü)"; +Calendar._TT["SEL_DATE"] = "Válasszon dátumot"; +Calendar._TT["DRAG_TO_MOVE"] = "Fogd és vidd"; +Calendar._TT["PART_TODAY"] = " (ma)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "%s megjelenÃtése elsÅ‘ként"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "Bezár"; +Calendar._TT["TODAY"] = "Ma"; +Calendar._TT["TIME_PART"] = "(Shift-)Click vagy húzd az érték változtatásához"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y.%m.%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%B %e, %A"; + +Calendar._TT["WK"] = "hét"; +Calendar._TT["TIME"] = "IdÅ‘:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-pt-br.js b/groups/public/javascripts/calendar/lang/calendar-pt-br.js index 5d4d014ce..bf7734ab3 100644 --- a/groups/public/javascripts/calendar/lang/calendar-pt-br.js +++ b/groups/public/javascripts/calendar/lang/calendar-pt-br.js @@ -2,7 +2,8 @@ // Calendar pt_BR language // Author: Adalberto Machado, <betosm@terra.com.br> -// Encoding: any +// Review: Alexandre da Silva, <simpsomboy@gmail.com> +// Encoding: UTF-8 // Distributed under the same terms as the calendar itself. // For translators: please use UTF-8 if possible. We strongly believe that @@ -13,7 +14,7 @@ Calendar._DN = new Array ("Domingo", "Segunda", - "Terca", + "Terça", "Quarta", "Quinta", "Sexta", @@ -45,13 +46,13 @@ Calendar._SDN = new Array // First day of the week. "0" means display Sunday first, "1" means display // Monday first, etc. -Calendar._FD = 1; +Calendar._FD = 0; // full month names Calendar._MN = new Array ("Janeiro", "Fevereiro", - "Marco", + "Março", "Abril", "Maio", "Junho", @@ -79,29 +80,30 @@ Calendar._SMN = new Array // tooltips Calendar._TT = {}; -Calendar._TT["INFO"] = "Sobre o calendario"; +Calendar._TT["INFO"] = "Sobre o calendário"; Calendar._TT["ABOUT"] = "DHTML Date/Time Selector\n" + "(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) -"Ultima versao visite: http://www.dynarch.com/projects/calendar/\n" + -"Distribuido sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." + +"Última versão visite: http://www.dynarch.com/projects/calendar/\n" + +"DistribuÃdo sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." + "\n\n" + -"Selecao de data:\n" + -"- Use os botoes \xab, \xbb para selecionar o ano\n" + -"- Use os botoes " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para selecionar o mes\n" + -"- Segure o botao do mouse em qualquer um desses botoes para selecao rapida."; +"Seleção de data:\n" + +"- Use os botões \xab, \xbb para selecionar o ano\n" + +"- Use os botões " + String.fromCharCode(0x2039) + ", " + +String.fromCharCode(0x203a) + " para selecionar o mês\n" + +"- Segure o botão do mouse em qualquer um desses botões para seleção rápida."; Calendar._TT["ABOUT_TIME"] = "\n\n" + -"Selecao de hora:\n" + +"Seleção de hora:\n" + "- Clique em qualquer parte da hora para incrementar\n" + "- ou Shift-click para decrementar\n" + -"- ou clique e segure para selecao rapida."; +"- ou clique e segure para seleção rápida."; Calendar._TT["PREV_YEAR"] = "Ant. ano (segure para menu)"; -Calendar._TT["PREV_MONTH"] = "Ant. mes (segure para menu)"; +Calendar._TT["PREV_MONTH"] = "Ant. mês (segure para menu)"; Calendar._TT["GO_TODAY"] = "Hoje"; -Calendar._TT["NEXT_MONTH"] = "Prox. mes (segure para menu)"; -Calendar._TT["NEXT_YEAR"] = "Prox. ano (segure para menu)"; +Calendar._TT["NEXT_MONTH"] = "Próx. mes (segure para menu)"; +Calendar._TT["NEXT_YEAR"] = "Próx. ano (segure para menu)"; Calendar._TT["SEL_DATE"] = "Selecione a data"; Calendar._TT["DRAG_TO_MOVE"] = "Arraste para mover"; Calendar._TT["PART_TODAY"] = " (hoje)"; diff --git a/groups/public/javascripts/calendar/lang/calendar-th.js b/groups/public/javascripts/calendar/lang/calendar-th.js new file mode 100644 index 000000000..dc4809e52 --- /dev/null +++ b/groups/public/javascripts/calendar/lang/calendar-th.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Gampol Thitinilnithi, <gampolt@gmail.com> +// Encoding: UTF-8 +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("à¸à¸²à¸—ิตย์", + "จันทร์", + "à¸à¸±à¸‡à¸„าร", + "พุธ", + "พฤหัสบดี", + "ศุà¸à¸£à¹Œ", + "เสาร์", + "à¸à¸²à¸—ิตย์"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("à¸à¸².", + "จ.", + "à¸.", + "พ.", + "พฤ.", + "ศ.", + "ส.", + "à¸à¸²."); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("มà¸à¸£à¸²à¸„ม", + "à¸à¸¸à¸¡à¸ าพันธ์", + "มีนาคม", + "เมษายน", + "พฤษภาคม", + "มิถุนายน", + "à¸à¸£à¸à¸Žà¸²à¸„ม", + "สิงหาคม", + "à¸à¸±à¸™à¸¢à¸²à¸¢à¸™", + "ตุลาคม", + "พฤศจิà¸à¸²à¸¢à¸™", + "ธันวาคม"); + +// short month names +Calendar._SMN = new Array +("ม.ค.", + "à¸.พ.", + "มี.ค.", + "เม.ย.", + "พ.ค.", + "มิ.ย.", + "à¸.ค.", + "ส.ค.", + "à¸.ย.", + "ต.ค.", + "พ.ย.", + "ธ.ค."); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸›à¸à¸´à¸—ิน"; + +Calendar._TT["ABOUT"] = +"DHTML Date/Time Selector\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Time selection:\n" + +"- Click on any of the time parts to increase it\n" + +"- or Shift-click to decrease it\n" + +"- or click and drag for faster selection."; + +Calendar._TT["PREV_YEAR"] = "ปีที่à¹à¸¥à¹‰à¸§ (ถ้าà¸à¸”ค้างจะมีเมนู)"; +Calendar._TT["PREV_MONTH"] = "เดืà¸à¸™à¸—ี่à¹à¸¥à¹‰à¸§ (ถ้าà¸à¸”ค้างจะมีเมนู)"; +Calendar._TT["GO_TODAY"] = "ไปที่วันนี้"; +Calendar._TT["NEXT_MONTH"] = "เดืà¸à¸™à¸«à¸™à¹‰à¸² (ถ้าà¸à¸”ค้างจะมีเมนู)"; +Calendar._TT["NEXT_YEAR"] = "ปีหน้า (ถ้าà¸à¸”ค้างจะมีเมนู)"; +Calendar._TT["SEL_DATE"] = "เลืà¸à¸à¸§à¸±à¸™"; +Calendar._TT["DRAG_TO_MOVE"] = "à¸à¸”à¹à¸¥à¹‰à¸§à¸¥à¸²à¸à¹€à¸žà¸·à¹ˆà¸à¸¢à¹‰à¸²à¸¢"; +Calendar._TT["PART_TODAY"] = " (วันนี้)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "à¹à¸ªà¸”ง %s เป็นวันà¹à¸£à¸"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "ปิด"; +Calendar._TT["TODAY"] = "วันนี้"; +Calendar._TT["TIME_PART"] = "(Shift-)à¸à¸”หรืà¸à¸à¸”à¹à¸¥à¹‰à¸§à¸¥à¸²à¸à¹€à¸žà¸·à¹ˆà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¸„่า"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "%a %e %b"; + +Calendar._TT["WK"] = "wk"; +Calendar._TT["TIME"] = "เวลา:"; diff --git a/groups/public/javascripts/calendar/lang/calendar-zh-tw.js b/groups/public/javascripts/calendar/lang/calendar-zh-tw.js index c48d25b0e..1e759db10 100644 --- a/groups/public/javascripts/calendar/lang/calendar-zh-tw.js +++ b/groups/public/javascripts/calendar/lang/calendar-zh-tw.js @@ -84,13 +84,13 @@ Calendar._TT["INFO"] = "關於 calendar"; Calendar._TT["ABOUT"] = "DHTML 日期/時間 鏿“‡å™¨\n" + "(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) -"最For latest version visit: http://www.dynarch.com/projects/calendar/\n" + -"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"最新版本å–å¾—ä½å€: http://www.dynarch.com/projects/calendar/\n" + +"使用 GNU LGPL 發行. åƒè€ƒ http://gnu.org/licenses/lgpl.html 以å–得更多關於 LGPL 之細節。" + "\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."; +"æ—¥æœŸé¸æ“‡æ–¹å¼:\n" + +"- ä½¿ç”¨æ»‘é¼ é»žæ“Š \xab 〠\xbb æŒ‰éˆ•é¸æ“‡å¹´ä»½\n" + +"- ä½¿ç”¨æ»‘é¼ é»žæ“Š " + String.fromCharCode(0x2039) + " 〠" + String.fromCharCode(0x203a) + " æŒ‰éˆ•é¸æ“‡æœˆä»½\n" + +"- ä½¿ç”¨æ»‘é¼ é»žæ“Šä¸Šè¿°æŒ‰éˆ•ä¸¦æŒ‰ä½ä¸æ”¾ï¼Œå¯é–‹å•Ÿå¿«é€Ÿé¸å–®ã€‚"; Calendar._TT["ABOUT_TIME"] = "\n\n" + "æ™‚é–“é¸æ“‡æ–¹å¼ï¼š\n" + "- ã€Œå–®æ“Šã€æ™‚分秒為éžå¢ž\n" + diff --git a/groups/public/javascripts/calendar/lang/calendar-zh.js b/groups/public/javascripts/calendar/lang/calendar-zh.js index ddb092bfa..121653fba 100644 --- a/groups/public/javascripts/calendar/lang/calendar-zh.js +++ b/groups/public/javascripts/calendar/lang/calendar-zh.js @@ -82,33 +82,33 @@ Calendar._TT = {}; Calendar._TT["INFO"] = "关于日历"; Calendar._TT["ABOUT"] = -"DHTML Date/Time Selector\n" + +"DHTML 日期/æ—¶é—´ 选择器\n" + "(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) -"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + -"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"最新版本请访问: http://www.dynarch.com/projects/calendar/\n" + +"éµå¾ª GNU LGPL å‘布。详情请查阅 http://gnu.org/licenses/lgpl.html " + "\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."; +"日期选择:\n" + +"- 使用 \xab,\xbb 按钮选择年\n" + +"- 使用 " + String.fromCharCode(0x2039) + "," + String.fromCharCode(0x203a) + " 按钮选择月\n" + +"- 在上述按钮上按ä½ä¸æ”¾å¯ä»¥å¿«é€Ÿé€‰æ‹©"; Calendar._TT["ABOUT_TIME"] = "\n\n" + -"Time selection:\n" + -"- Click on any of the time parts to increase it\n" + -"- or Shift-click to decrease it\n" + -"- or click and drag for faster selection."; +"时间选择:\n" + +"- 点击时间的任æ„部分æ¥å¢žåŠ \n" + +"- ShiftåŠ ç‚¹å‡»æ¥å‡å°‘\n" + +"- ç‚¹å‡»åŽæ‹–动进行快速选择"; -Calendar._TT["PREV_YEAR"] = "上年 (hold for menu)"; -Calendar._TT["PREV_MONTH"] = "上月 (hold for menu)"; +Calendar._TT["PREV_YEAR"] = "上年(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)"; +Calendar._TT["PREV_MONTH"] = "上月(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)"; Calendar._TT["GO_TODAY"] = "回到今天"; -Calendar._TT["NEXT_MONTH"] = "下月 (hold for menu)"; -Calendar._TT["NEXT_YEAR"] = "下年 (hold for menu)"; +Calendar._TT["NEXT_MONTH"] = "下月(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)"; +Calendar._TT["NEXT_YEAR"] = "下年(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)"; Calendar._TT["SEL_DATE"] = "选择日期"; Calendar._TT["DRAG_TO_MOVE"] = "拖动"; Calendar._TT["PART_TODAY"] = " (今日)"; // the following is to inform that "%s" is to be the first day of week // %s will be replaced with the day name. -Calendar._TT["DAY_FIRST"] = "Display %s first"; +Calendar._TT["DAY_FIRST"] = "一周开始于 %s"; // This may be locale-dependent. It specifies the week-end days, as an array // of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 @@ -117,11 +117,11 @@ Calendar._TT["WEEKEND"] = "0,6"; Calendar._TT["CLOSE"] = "å…³é—"; Calendar._TT["TODAY"] = "今天"; -Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; +Calendar._TT["TIME_PART"] = "ShiftåŠ ç‚¹å‡»æˆ–è€…æ‹–åŠ¨æ¥å˜æ›´"; // date formats Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; -Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e"; +Calendar._TT["TT_DATE_FORMAT"] = "星期%a %b%eæ—¥"; -Calendar._TT["WK"] = "wk"; -Calendar._TT["TIME"] = "Time:"; +Calendar._TT["WK"] = "周"; +Calendar._TT["TIME"] = "时间:"; diff --git a/groups/public/javascripts/context_menu.js b/groups/public/javascripts/context_menu.js index 3e2d571fa..20f0fc5a7 100644 --- a/groups/public/javascripts/context_menu.js +++ b/groups/public/javascripts/context_menu.js @@ -28,11 +28,11 @@ ContextMenu.prototype = { RightClick: function(e) { this.hideMenu(); // do not show the context menu on links - if (Event.findElement(e, 'a') != document) { return; } + if (Event.element(e).tagName == 'A') { return; } // right-click simulated by Alt+Click with Opera if (window.opera && !e.altKey) { return; } var tr = Event.findElement(e, 'tr'); - if ((tr == document) || !tr.hasClassName('hascontextmenu')) { return; } + if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; } Event.stop(e); if (!this.isSelected(tr)) { this.unselectAll(); @@ -44,14 +44,14 @@ ContextMenu.prototype = { Click: function(e) { this.hideMenu(); - if (Event.findElement(e, 'a') != document) { return; } + if (Event.element(e).tagName == 'A') { return; } if (window.opera && e.altKey) { return; } if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) { var tr = Event.findElement(e, 'tr'); if (tr!=document && tr.hasClassName('hascontextmenu')) { // a row was clicked, check if the click was on checkbox var box = Event.findElement(e, 'input'); - if (box!=document) { + if (box!=document && box!=undefined) { // a checkbox may be clicked if (box.checked) { tr.addClassName('context-menu-selection'); @@ -90,6 +90,9 @@ ContextMenu.prototype = { } } } + else{ + this.RightClick(e); + } }, showMenu: function(e) { diff --git a/groups/public/javascripts/controls.js b/groups/public/javascripts/controls.js index 8c273f874..5aaf0bb2b 100644 --- a/groups/public/javascripts/controls.js +++ b/groups/public/javascripts/controls.js @@ -1,6 +1,6 @@ -// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan) -// (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com) +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava @@ -37,22 +37,23 @@ if(typeof Effect == 'undefined') throw("controls.js requires including script.aculo.us' effects.js library"); -var Autocompleter = {} -Autocompleter.Base = function() {}; -Autocompleter.Base.prototype = { +var Autocompleter = { } +Autocompleter.Base = Class.create({ baseInitialize: function(element, update, options) { - this.element = $(element); + element = $(element) + this.element = element; this.update = $(update); this.hasFocus = false; this.changed = false; this.active = false; this.index = 0; this.entryCount = 0; + this.oldElementValue = this.element.value; if(this.setOptions) this.setOptions(options); else - this.options = options || {}; + this.options = options || { }; this.options.paramName = this.options.paramName || this.element.name; this.options.tokens = this.options.tokens || []; @@ -74,6 +75,9 @@ Autocompleter.Base.prototype = { if(typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); + // Force carriage returns as token delimiters anyway + if (!this.options.tokens.include('\n')) + this.options.tokens.push('\n'); this.observer = null; @@ -81,15 +85,14 @@ Autocompleter.Base.prototype = { Element.hide(this.update); - Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); - Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); }, show: function() { if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); if(!this.iefix && - (navigator.appVersion.indexOf('MSIE')>0) && - (navigator.userAgent.indexOf('Opera')<0) && + (Prototype.Browser.IE) && (Element.getStyle(this.update, 'position')=='absolute')) { new Insertion.After(this.update, '<iframe id="' + this.update.id + '_iefix" '+ @@ -139,17 +142,17 @@ Autocompleter.Base.prototype = { case Event.KEY_UP: this.markPrevious(); this.render(); - if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + Event.stop(event); return; case Event.KEY_DOWN: this.markNext(); this.render(); - if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + Event.stop(event); return; } else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || - (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; + (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; this.changed = true; this.hasFocus = true; @@ -195,7 +198,6 @@ Autocompleter.Base.prototype = { this.index==i ? Element.addClassName(this.getEntry(i),"selected") : Element.removeClassName(this.getEntry(i),"selected"); - if(this.hasFocus) { this.show(); this.active = true; @@ -238,21 +240,22 @@ Autocompleter.Base.prototype = { } var value = ''; if (this.options.select) { - var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + var nodes = $(selectedElement).select('.' + this.options.select) || []; if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); } else value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); - var 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+/); + var bounds = this.getTokenBounds(); + if (bounds[0] != -1) { + var newValue = this.element.value.substr(0, bounds[0]); + var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); if (whitespace) newValue += whitespace[0]; - this.element.value = newValue + value; + this.element.value = newValue + value + this.element.value.substr(bounds[1]); } else { this.element.value = value; } + this.oldElementValue = this.element.value; this.element.focus(); if (this.options.afterUpdateElement) @@ -296,39 +299,48 @@ Autocompleter.Base.prototype = { onObserverEvent: function() { this.changed = false; + this.tokenBounds = null; if(this.getToken().length>=this.options.minChars) { - this.startIndicator(); this.getUpdatedChoices(); } else { this.active = false; this.hide(); } + this.oldElementValue = this.element.value; }, 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<this.options.tokens.length; i++) { - var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); - if (thisTokenPos > lastTokenPos) - lastTokenPos = thisTokenPos; + var bounds = this.getTokenBounds(); + return this.element.value.substring(bounds[0], bounds[1]).strip(); + }, + + getTokenBounds: function() { + if (null != this.tokenBounds) return this.tokenBounds; + var value = this.element.value; + if (value.strip().empty()) return [-1, 0]; + var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); + var offset = (diff == this.oldElementValue.length ? 1 : 0); + var prevTokenPos = -1, nextTokenPos = value.length; + var tp; + for (var index = 0, l = this.options.tokens.length; index < l; ++index) { + tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); + if (tp > prevTokenPos) prevTokenPos = tp; + tp = value.indexOf(this.options.tokens[index], diff + offset); + if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; } - return lastTokenPos; + return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); } -} +}); + +Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { + var boundary = Math.min(newS.length, oldS.length); + for (var index = 0; index < boundary; ++index) + if (newS[index] != oldS[index]) + return index; + return boundary; +}; -Ajax.Autocompleter = Class.create(); -Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { +Ajax.Autocompleter = Class.create(Autocompleter.Base, { initialize: function(element, update, url, options) { this.baseInitialize(element, update, options); this.options.asynchronous = true; @@ -338,7 +350,9 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro }, getUpdatedChoices: function() { - entry = encodeURIComponent(this.options.paramName) + '=' + + this.startIndicator(); + + var entry = encodeURIComponent(this.options.paramName) + '=' + encodeURIComponent(this.getToken()); this.options.parameters = this.options.callback ? @@ -346,14 +360,13 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro 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 @@ -391,8 +404,7 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro // 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(), { +Autocompleter.Local = Class.create(Autocompleter.Base, { initialize: function(element, update, array, options) { this.baseInitialize(element, update, options); this.options.array = array; @@ -448,13 +460,12 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) return "<ul>" + ret.join('') + "</ul>"; } - }, options || {}); + }, options || { }); } }); -// AJAX in-place editor -// -// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor +// AJAX in-place editor and collection editor +// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007). // Use this if you notice weird scrolling problems on some browsers, // the DOM might be a bit confused when this gets called so do this @@ -465,353 +476,472 @@ Field.scrollFreeActivate = function(field) { }, 1); } -Ajax.InPlaceEditor = Class.create(); -Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; -Ajax.InPlaceEditor.prototype = { +Ajax.InPlaceEditor = Class.create({ initialize: function(element, url, options) { this.url = url; - this.element = $(element); - - this.options = Object.extend({ - paramName: "value", - okButton: true, - okText: "ok", - cancelLink: true, - cancelText: "cancel", - savingText: "Saving...", - clickToEditText: "Click to edit", - okText: "ok", - rows: 1, - onComplete: function(transport, element) { - new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); - }, - onFailure: function(transport) { - alert("Error communicating with the server: " + transport.responseText.stripTags()); - }, - callback: function(form) { - return Form.serialize(form); - }, - handleLineBreaks: true, - loadingText: 'Loading...', - savingClassName: 'inplaceeditor-saving', - loadingClassName: 'inplaceeditor-loading', - formClassName: 'inplaceeditor-form', - highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, - highlightendcolor: "#FFFFFF", - externalControl: null, - submitOnBlur: false, - ajaxOptions: {}, - evalScripts: false - }, options || {}); - - if(!this.options.formId && this.element.id) { - this.options.formId = this.element.id + "-inplaceeditor"; - if ($(this.options.formId)) { - // there's already a form with that name, don't specify an id - this.options.formId = null; - } + this.element = element = $(element); + this.prepareOptions(); + this._controls = { }; + arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! + Object.extend(this.options, options || { }); + if (!this.options.formId && this.element.id) { + this.options.formId = this.element.id + '-inplaceeditor'; + if ($(this.options.formId)) + this.options.formId = ''; } - - if (this.options.externalControl) { + if (this.options.externalControl) this.options.externalControl = $(this.options.externalControl); - } - - this.originalBackground = Element.getStyle(this.element, 'background-color'); - if (!this.originalBackground) { - this.originalBackground = "transparent"; - } - + if (!this.options.externalControl) + this.options.externalControlOnly = false; + this._originalBackground = this.element.getStyle('background-color') || '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); + this._boundCancelHandler = this.handleFormCancellation.bind(this); + this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); + this._boundFailureHandler = this.handleAJAXFailure.bind(this); + this._boundSubmitHandler = this.handleFormSubmission.bind(this); + this._boundWrapperHandler = this.wrapUp.bind(this); + this.registerListeners(); + }, + checkForEscapeOrReturn: function(e) { + if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; + if (Event.KEY_ESC == e.keyCode) + this.handleFormCancellation(e); + else if (Event.KEY_RETURN == e.keyCode) + this.handleFormSubmission(e); + }, + createControl: function(mode, handler, extraClasses) { + var control = this.options[mode + 'Control']; + var text = this.options[mode + 'Text']; + if ('button' == control) { + var btn = document.createElement('input'); + btn.type = 'submit'; + btn.value = text; + btn.className = 'editor_' + mode + '_button'; + if ('cancel' == mode) + btn.onclick = this._boundCancelHandler; + this._form.appendChild(btn); + this._controls[mode] = btn; + } else if ('link' == control) { + var link = document.createElement('a'); + link.href = '#'; + link.appendChild(document.createTextNode(text)); + link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; + link.className = 'editor_' + mode + '_link'; + if (extraClasses) + link.className += ' ' + extraClasses; + this._form.appendChild(link); + this._controls[mode] = link; } }, - enterEditMode: function(evt) { - if (this.saving) return; - if (this.editing) return; - this.editing = true; - this.onEnterEditMode(); - if (this.options.externalControl) { - Element.hide(this.options.externalControl); - } - Element.hide(this.element); - this.createForm(); - this.element.parentNode.insertBefore(this.form, this.element); - if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField); - // stop the event to avoid a page refresh in Safari - if (evt) { - Event.stop(evt); - } - return false; - }, - createForm: function() { - this.form = document.createElement("form"); - this.form.id = this.options.formId; - Element.addClassName(this.form, this.options.formClassName) - this.form.onsubmit = this.onSubmit.bind(this); - - this.createEditField(); - - if (this.options.textarea) { - var br = document.createElement("br"); - this.form.appendChild(br); - } - - if (this.options.okButton) { - okButton = document.createElement("input"); - okButton.type = "submit"; - okButton.value = this.options.okText; - okButton.className = 'editor_ok_button'; - this.form.appendChild(okButton); - } - - if (this.options.cancelLink) { - cancelLink = document.createElement("a"); - cancelLink.href = "#"; - cancelLink.appendChild(document.createTextNode(this.options.cancelText)); - cancelLink.onclick = this.onclickCancel.bind(this); - cancelLink.className = 'editor_cancel'; - this.form.appendChild(cancelLink); - } - }, - hasHTMLLineBreaks: function(string) { - if (!this.options.handleLineBreaks) return false; - return string.match(/<br/i) || string.match(/<p>/i); - }, - convertHTMLLineBreaks: function(string) { - return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, ""); - }, createEditField: function() { - var text; - if(this.options.loadTextURL) { - text = this.options.loadingText; - } else { - text = this.getText(); - } - - var obj = this; - - if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { - this.options.textarea = false; - var textField = document.createElement("input"); - textField.obj = this; - textField.type = "text"; - textField.name = this.options.paramName; - textField.value = text; - textField.style.backgroundColor = this.options.highlightcolor; - textField.className = 'editor_field'; + var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); + var fld; + if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { + fld = document.createElement('input'); + fld.type = 'text'; var size = this.options.size || this.options.cols || 0; - if (size != 0) textField.size = size; - if (this.options.submitOnBlur) - textField.onblur = this.onSubmit.bind(this); - this.editField = textField; + if (0 < size) fld.size = size; } else { - this.options.textarea = true; - var textArea = document.createElement("textarea"); - textArea.obj = this; - textArea.name = this.options.paramName; - textArea.value = this.convertHTMLLineBreaks(text); - textArea.rows = this.options.rows; - textArea.cols = this.options.cols || 40; - textArea.className = 'editor_field'; - if (this.options.submitOnBlur) - textArea.onblur = this.onSubmit.bind(this); - this.editField = textArea; + fld = document.createElement('textarea'); + fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); + fld.cols = this.options.cols || 40; } - - if(this.options.loadTextURL) { + fld.name = this.options.paramName; + fld.value = text; // No HTML breaks conversion anymore + fld.className = 'editor_field'; + if (this.options.submitOnBlur) + fld.onblur = this._boundSubmitHandler; + this._controls.editor = fld; + if (this.options.loadTextURL) this.loadExternalText(); - } - this.form.appendChild(this.editField); + this._form.appendChild(this._controls.editor); + }, + createForm: function() { + var ipe = this; + function addText(mode, condition) { + var text = ipe.options['text' + mode + 'Controls']; + if (!text || condition === false) return; + ipe._form.appendChild(document.createTextNode(text)); + }; + this._form = $(document.createElement('form')); + this._form.id = this.options.formId; + this._form.addClassName(this.options.formClassName); + this._form.onsubmit = this._boundSubmitHandler; + this.createEditField(); + if ('textarea' == this._controls.editor.tagName.toLowerCase()) + this._form.appendChild(document.createElement('br')); + if (this.options.onFormCustomization) + this.options.onFormCustomization(this, this._form); + addText('Before', this.options.okControl || this.options.cancelControl); + this.createControl('ok', this._boundSubmitHandler); + addText('Between', this.options.okControl && this.options.cancelControl); + this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); + addText('After', this.options.okControl || this.options.cancelControl); + }, + destroy: function() { + if (this._oldInnerHTML) + this.element.innerHTML = this._oldInnerHTML; + this.leaveEditMode(); + this.unregisterListeners(); + }, + enterEditMode: function(e) { + if (this._saving || this._editing) return; + this._editing = true; + this.triggerCallback('onEnterEditMode'); + if (this.options.externalControl) + this.options.externalControl.hide(); + this.element.hide(); + this.createForm(); + this.element.parentNode.insertBefore(this._form, this.element); + if (!this.options.loadTextURL) + this.postProcessEditField(); + if (e) Event.stop(e); + }, + enterHover: function(e) { + if (this.options.hoverClassName) + this.element.addClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onEnterHover'); }, getText: function() { return this.element.innerHTML; }, - loadExternalText: function() { - Element.addClassName(this.form, this.options.loadingClassName); - this.editField.disabled = true; - new Ajax.Request( - this.options.loadTextURL, - Object.extend({ - asynchronous: true, - onComplete: this.onLoadedExternalText.bind(this) - }, this.options.ajaxOptions) - ); - }, - onLoadedExternalText: function(transport) { - Element.removeClassName(this.form, this.options.loadingClassName); - this.editField.disabled = false; - this.editField.value = transport.responseText.stripTags(); - Field.scrollFreeActivate(this.editField); - }, - onclickCancel: function() { - this.onComplete(); - this.leaveEditMode(); - return false; - }, - onFailure: function(transport) { - this.options.onFailure(transport); - if (this.oldInnerHTML) { - this.element.innerHTML = this.oldInnerHTML; - this.oldInnerHTML = null; + handleAJAXFailure: function(transport) { + this.triggerCallback('onFailure', transport); + if (this._oldInnerHTML) { + this.element.innerHTML = this._oldInnerHTML; + this._oldInnerHTML = null; } - return false; }, - onSubmit: function() { - // onLoading resets these so we need to save them away for the Ajax call - var form = this.form; - var value = this.editField.value; - - // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... - // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... - // to be displayed indefinitely - this.onLoading(); - - if (this.options.evalScripts) { - new Ajax.Request( - this.url, Object.extend({ - parameters: this.options.callback(form, value), - onComplete: this.onComplete.bind(this), - onFailure: this.onFailure.bind(this), - asynchronous:true, - evalScripts:true - }, this.options.ajaxOptions)); - } else { - new Ajax.Updater( - { success: this.element, - // don't update on failure (this could be an option) - failure: null }, - this.url, Object.extend({ - parameters: this.options.callback(form, value), - onComplete: this.onComplete.bind(this), - onFailure: this.onFailure.bind(this) - }, this.options.ajaxOptions)); - } - // stop the event to avoid a page refresh in Safari - if (arguments.length > 1) { - Event.stop(arguments[0]); + handleFormCancellation: function(e) { + this.wrapUp(); + if (e) Event.stop(e); + }, + handleFormSubmission: function(e) { + var form = this._form; + var value = $F(this._controls.editor); + this.prepareSubmission(); + var params = this.options.callback(form, value) || ''; + if (Object.isString(params)) + params = params.toQueryParams(); + params.editorId = this.element.id; + if (this.options.htmlResponse) { + var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Updater({ success: this.element }, this.url, options); + } else { + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: params, + onComplete: this._boundWrapperHandler, + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.url, options); } - return false; + if (e) Event.stop(e); + }, + leaveEditMode: function() { + this.element.removeClassName(this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); + if (this.options.externalControl) + this.options.externalControl.show(); + this._saving = false; + this._editing = false; + this._oldInnerHTML = null; + this.triggerCallback('onLeaveEditMode'); + }, + leaveHover: function(e) { + if (this.options.hoverClassName) + this.element.removeClassName(this.options.hoverClassName); + if (this._saving) return; + this.triggerCallback('onLeaveHover'); }, - onLoading: function() { - this.saving = true; + loadExternalText: function() { + this._form.addClassName(this.options.loadingClassName); + this._controls.editor.disabled = true; + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._form.removeClassName(this.options.loadingClassName); + var text = transport.responseText; + if (this.options.stripLoadedTextTags) + text = text.stripTags(); + this._controls.editor.value = text; + this._controls.editor.disabled = false; + this.postProcessEditField(); + }.bind(this), + onFailure: this._boundFailureHandler + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + postProcessEditField: function() { + var fpc = this.options.fieldPostCreation; + if (fpc) + $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); + }, + prepareOptions: function() { + this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); + Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); + [this._extraDefaultOptions].flatten().compact().each(function(defs) { + Object.extend(this.options, defs); + }.bind(this)); + }, + prepareSubmission: function() { + this._saving = true; this.removeForm(); this.leaveHover(); this.showSaving(); }, + registerListeners: function() { + this._listeners = { }; + var listener; + $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { + listener = this[pair.value].bind(this); + this._listeners[pair.key] = listener; + if (!this.options.externalControlOnly) + this.element.observe(pair.key, listener); + if (this.options.externalControl) + this.options.externalControl.observe(pair.key, listener); + }.bind(this)); + }, + removeForm: function() { + if (!this._form) return; + this._form.remove(); + this._form = null; + this._controls = { }; + }, showSaving: function() { - this.oldInnerHTML = this.element.innerHTML; + this._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); + this.element.addClassName(this.options.savingClassName); + this.element.style.backgroundColor = this._originalBackground; + this.element.show(); }, - removeForm: function() { - if(this.form) { - if (this.form.parentNode) Element.remove(this.form); - this.form = null; + triggerCallback: function(cbName, arg) { + if ('function' == typeof this.options[cbName]) { + this.options[cbName](this, arg); } }, - 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) + unregisterListeners: function() { + $H(this._listeners).each(function(pair) { + if (!this.options.externalControlOnly) + this.element.stopObserving(pair.key, pair.value); + if (this.options.externalControl) + this.options.externalControl.stopObserving(pair.key, pair.value); + }.bind(this)); }, - 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 + wrapUp: function(transport) { + this.leaveEditMode(); + // Can't use triggerCallback due to backward compatibility: requires + // binding + direct element + this._boundComplete(transport, this.element); + } +}); + +Object.extend(Ajax.InPlaceEditor.prototype, { + dispose: Ajax.InPlaceEditor.prototype.destroy +}); + +Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { + initialize: function($super, element, url, options) { + this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; + $super(element, url, options); + }, + + createEditField: function() { + var list = document.createElement('select'); + list.name = this.options.paramName; + list.size = 1; + this._controls.editor = list; + this._collection = this.options.collection || []; + if (this.options.loadCollectionURL) + this.loadCollection(); + else + this.checkForExternalText(); + this._form.appendChild(this._controls.editor); + }, + + loadCollection: function() { + this._form.addClassName(this.options.loadingClassName); + this.showLoadingText(this.options.loadingCollectionText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw 'Server returned an invalid collection representation.'; + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure }); + new Ajax.Request(this.options.loadCollectionURL, options); }, - 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); + + showLoadingText: function(text) { + this._controls.editor.disabled = true; + var tempOption = this._controls.editor.firstChild; + if (!tempOption) { + tempOption = document.createElement('option'); + tempOption.value = ''; + this._controls.editor.appendChild(tempOption); + tempOption.selected = true; } - this.editing = false; - this.saving = false; - this.oldInnerHTML = null; - this.onLeaveEditMode(); + tempOption.update((text || '').stripScripts().stripTags()); }, - onComplete: function(transport) { - this.leaveEditMode(); - this.options.onComplete.bind(this)(transport, this.element); + + checkForExternalText: function() { + this._text = this.getText(); + if (this.options.loadTextURL) + this.loadExternalText(); + else + this.buildOptionList(); }, - 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); - } + + loadExternalText: function() { + this.showLoadingText(this.options.loadingText); + var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); + Object.extend(options, { + parameters: 'editorId=' + encodeURIComponent(this.element.id), + onComplete: Prototype.emptyFunction, + onSuccess: function(transport) { + this._text = transport.responseText.strip(); + this.buildOptionList(); + }.bind(this), + onFailure: this.onFailure + }); + new Ajax.Request(this.options.loadTextURL, options); + }, + + buildOptionList: function() { + this._form.removeClassName(this.options.loadingClassName); + this._collection = this._collection.map(function(entry) { + return 2 === entry.length ? entry : [entry, entry].flatten(); + }); + var marker = ('value' in this.options) ? this.options.value : this._text; + var textFound = this._collection.any(function(entry) { + return entry[0] == marker; + }.bind(this)); + this._controls.editor.update(''); + var option; + this._collection.each(function(entry, index) { + option = document.createElement('option'); + option.value = entry[0]; + option.selected = textFound ? entry[0] == marker : 0 == index; + option.appendChild(document.createTextNode(entry[1])); + this._controls.editor.appendChild(option); + }.bind(this)); + this._controls.editor.disabled = false; + Field.scrollFreeActivate(this._controls.editor); } -}; +}); -Ajax.InPlaceCollectionEditor = Class.create(); -Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); -Object.extend(Ajax.InPlaceCollectionEditor.prototype, { - createEditField: function() { - if (!this.cached_selectTag) { - var selectTag = document.createElement("select"); - var collection = this.options.collection || []; - var optionTag; - collection.each(function(e,i) { - optionTag = document.createElement("option"); - optionTag.value = (e instanceof Array) ? e[0] : e; - if((typeof this.options.value == 'undefined') && - ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true; - if(this.options.value==optionTag.value) optionTag.selected = true; - optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); - selectTag.appendChild(optionTag); - }.bind(this)); - this.cached_selectTag = selectTag; - } +//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** +//**** This only exists for a while, in order to let **** +//**** users adapt to the new API. Read up on the new **** +//**** API and convert your code to it ASAP! **** + +Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { + if (!options) return; + function fallback(name, expr) { + if (name in options || expr === undefined) return; + options[name] = expr; + }; + fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : + options.cancelLink == options.cancelButton == false ? false : undefined))); + fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : + options.okLink == options.okButton == false ? false : undefined))); + fallback('highlightColor', options.highlightcolor); + fallback('highlightEndColor', options.highlightendcolor); +}; - this.editField = this.cached_selectTag; - if(this.options.loadTextURL) this.loadExternalText(); - this.form.appendChild(this.editField); - this.options.callback = function(form, value) { - return "value=" + encodeURIComponent(value); +Object.extend(Ajax.InPlaceEditor, { + DefaultOptions: { + ajaxOptions: { }, + autoRows: 3, // Use when multi-line w/ rows == 1 + cancelControl: 'link', // 'link'|'button'|false + cancelText: 'cancel', + clickToEditText: 'Click to edit', + externalControl: null, // id|elt + externalControlOnly: false, + fieldPostCreation: 'activate', // 'activate'|'focus'|false + formClassName: 'inplaceeditor-form', + formId: null, // id|elt + highlightColor: '#ffff99', + highlightEndColor: '#ffffff', + hoverClassName: '', + htmlResponse: true, + loadingClassName: 'inplaceeditor-loading', + loadingText: 'Loading...', + okControl: 'button', // 'link'|'button'|false + okText: 'ok', + paramName: 'value', + rows: 1, // If 1 and multi-line, uses autoRows + savingClassName: 'inplaceeditor-saving', + savingText: 'Saving...', + size: 0, + stripLoadedTextTags: false, + submitOnBlur: false, + textAfterControls: '', + textBeforeControls: '', + textBetweenControls: '' + }, + DefaultCallbacks: { + callback: function(form) { + return Form.serialize(form); + }, + onComplete: function(transport, element) { + // For backward compatibility, this one is bound to the IPE, and passes + // the element directly. It was too often customized, so we don't break it. + new Effect.Highlight(element, { + startcolor: this.options.highlightColor, keepBackgroundImage: true }); + }, + onEnterEditMode: null, + onEnterHover: function(ipe) { + ipe.element.style.backgroundColor = ipe.options.highlightColor; + if (ipe._effect) + ipe._effect.cancel(); + }, + onFailure: function(transport, ipe) { + alert('Error communication with the server: ' + transport.responseText.stripTags()); + }, + onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. + onLeaveEditMode: null, + onLeaveHover: function(ipe) { + ipe._effect = new Effect.Highlight(ipe.element, { + startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, + restorecolor: ipe._originalBackground, keepBackgroundImage: true + }); } + }, + Listeners: { + click: 'enterEditMode', + keydown: 'checkForEscapeOrReturn', + mouseover: 'enterHover', + mouseout: 'leaveHover' } }); +Ajax.InPlaceCollectionEditor.DefaultOptions = { + loadingCollectionText: 'Loading options...' +}; + // Delayed observer, like Form.Element.Observer, // but waits for delay after last key input // Ideal for live-search fields -Form.Element.DelayedObserver = Class.create(); -Form.Element.DelayedObserver.prototype = { +Form.Element.DelayedObserver = Class.create({ initialize: function(element, delay, callback) { this.delay = delay || 0.5; this.element = $(element); @@ -830,4 +960,4 @@ Form.Element.DelayedObserver.prototype = { this.timer = null; this.callback(this.element, $F(this.element)); } -}; +}); diff --git a/groups/public/javascripts/dragdrop.js b/groups/public/javascripts/dragdrop.js index c71ddb827..bf5cfea66 100644 --- a/groups/public/javascripts/dragdrop.js +++ b/groups/public/javascripts/dragdrop.js @@ -1,10 +1,10 @@ -// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ -if(typeof Effect == 'undefined') +if(Object.isUndefined(Effect)) throw("dragdrop.js requires including script.aculo.us' effects.js library"); var Droppables = { @@ -20,14 +20,13 @@ var Droppables = { greedy: true, hoverclass: null, tree: false - }, arguments[1] || {}); + }, arguments[1] || { }); // cache containers if(options.containment) { options._containers = []; var containment = options.containment; - if((typeof containment == 'object') && - (containment.constructor == Array)) { + if(Object.isArray(containment)) { containment.each( function(c) { options._containers.push($(c)) }); } else { options._containers.push($(containment)); @@ -87,21 +86,23 @@ var Droppables = { show: function(point, element) { if(!this.drops.length) return; - var affected = []; + var drop, affected = []; - if(this.last_active) this.deactivate(this.last_active); this.drops.each( function(drop) { if(Droppables.isAffected(point, element, drop)) affected.push(drop); }); - if(affected.length>0) { + if(affected.length>0) drop = Droppables.findDeepestChild(affected); + + if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); + if (drop) { Position.within(drop.element, point[0], point[1]); if(drop.onHover) drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); - Droppables.activate(drop); + if (drop != this.last_active) Droppables.activate(drop); } }, @@ -110,8 +111,10 @@ var Droppables = { 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); + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } }, reset: function() { @@ -219,10 +222,7 @@ var Draggables = { /*--------------------------------------------------------------------------*/ -var Draggable = Class.create(); -Draggable._dragging = {}; - -Draggable.prototype = { +var Draggable = Class.create({ initialize: function(element) { var defaults = { handle: false, @@ -233,7 +233,7 @@ Draggable.prototype = { }); }, endeffect: function(element) { - var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0; + var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, queue: {scope:'_draggable', position:'end'}, afterFinish: function(){ @@ -243,6 +243,7 @@ Draggable.prototype = { }, zindex: 1000, revert: false, + quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, @@ -250,7 +251,7 @@ Draggable.prototype = { delay: 0 }; - if(!arguments[1] || typeof arguments[1].endeffect == 'undefined') + if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) Object.extend(defaults, { starteffect: function(element) { element._opacity = Element.getOpacity(element); @@ -259,11 +260,11 @@ Draggable.prototype = { } }); - var options = Object.extend(defaults, arguments[1] || {}); + var options = Object.extend(defaults, arguments[1] || { }); this.element = $(element); - if(options.handle && (typeof options.handle == 'string')) + if(options.handle && Object.isString(options.handle)) this.handle = this.element.down('.'+options.handle, 0); if(!this.handle) this.handle = $(options.handle); @@ -276,7 +277,6 @@ Draggable.prototype = { Element.makePositioned(this.element); // fix IE - this.delta = this.currentDelta(); this.options = options; this.dragging = false; @@ -298,17 +298,17 @@ Draggable.prototype = { }, initDrag: function(event) { - if(typeof Draggable._dragging[this.element] != 'undefined' && + if(!Object.isUndefined(Draggable._dragging[this.element]) && Draggable._dragging[this.element]) return; if(Event.isLeftClick(event)) { // abort on form elements, fixes a Firefox issue var src = Event.element(event); - if(src.tagName && ( - src.tagName=='INPUT' || - src.tagName=='SELECT' || - src.tagName=='OPTION' || - src.tagName=='BUTTON' || - src.tagName=='TEXTAREA')) return; + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; var pointer = [Event.pointerX(event), Event.pointerY(event)]; var pos = Position.cumulativeOffset(this.element); @@ -321,6 +321,8 @@ Draggable.prototype = { startDrag: function(event) { this.dragging = true; + if(!this.delta) + this.delta = this.currentDelta(); if(this.options.zindex) { this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); @@ -329,7 +331,9 @@ Draggable.prototype = { if(this.options.ghosting) { this._clone = this.element.cloneNode(true); - Position.absolutize(this.element); + this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); + if (!this.element._originallyAbsolute) + Position.absolutize(this.element); this.element.parentNode.insertBefore(this._clone, this.element); } @@ -351,8 +355,12 @@ Draggable.prototype = { updateDrag: function(event, pointer) { if(!this.dragging) this.startDrag(event); - Position.prepare(); - Droppables.show(pointer, this.element); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + Draggables.notify('onDrag', this, event); this.draw(pointer); @@ -380,30 +388,44 @@ Draggable.prototype = { } // fix AppleWebKit rendering - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + if(Prototype.Browser.WebKit) window.scrollBy(0,0); Event.stop(event); }, finishDrag: function(event, success) { this.dragging = false; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } if(this.options.ghosting) { - Position.relativize(this.element); + if (!this.element._originallyAbsolute) + Position.relativize(this.element); + delete this.element._originallyAbsolute; Element.remove(this._clone); this._clone = null; } - if(success) Droppables.fire(event, this.element); + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); Draggables.notify('onEnd', this, event); var revert = this.options.revert; - if(revert && typeof revert == 'function') revert = revert(this.element); + if(revert && Object.isFunction(revert)) 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]); + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); } else { this.delta = d; } @@ -451,15 +473,15 @@ Draggable.prototype = { }.bind(this)); if(this.options.snap) { - if(typeof this.options.snap == 'function') { + if(Object.isFunction(this.options.snap)) { p = this.options.snap(p[0],p[1],this); } else { - if(this.options.snap instanceof Array) { + if(Object.isArray(this.options.snap)) { p = p.map( function(v, i) { - return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)) } else { p = p.map( function(v) { - return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + return (v/this.options.snap).round()*this.options.snap }.bind(this)) } }} @@ -543,12 +565,13 @@ Draggable.prototype = { } return { top: T, left: L, width: W, height: H }; } -} +}); + +Draggable._dragging = { }; /*--------------------------------------------------------------------------*/ -var SortableObserver = Class.create(); -SortableObserver.prototype = { +var SortableObserver = Class.create({ initialize: function(element, observer) { this.element = $(element); this.observer = observer; @@ -564,15 +587,15 @@ SortableObserver.prototype = { if(this.lastValue != Sortable.serialize(this.element)) this.observer(this.element) } -} +}); var Sortable = { SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, - sortables: {}, + sortables: { }, _findRootElement: function(element) { - while (element.tagName != "BODY") { + while (element.tagName.toUpperCase() != "BODY") { if(element.id && Sortable.sortables[element.id]) return element; element = element.parentNode; } @@ -612,13 +635,20 @@ var Sortable = { delay: 0, hoverclass: null, ghosting: false, + quiet: false, scroll: false, scrollSensitivity: 20, scrollSpeed: 15, format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + onChange: Prototype.emptyFunction, onUpdate: Prototype.emptyFunction - }, arguments[1] || {}); + }, arguments[1] || { }); // clear any old sortable with same element this.destroy(element); @@ -626,6 +656,7 @@ var Sortable = { // build options for the draggables var options_for_draggable = { revert: true, + quiet: options.quiet, scroll: options.scroll, scrollSpeed: options.scrollSpeed, scrollSensitivity: options.scrollSensitivity, @@ -679,10 +710,9 @@ var Sortable = { options.droppables.push(element); } - (this.findElements(element, options) || []).each( function(e) { - // handles are per-draggable - var handle = options.handle ? - $(e).down('.'+options.handle,0) : e; + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).select('.' + options.handle)[0] : e); options.draggables.push( new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); Droppables.add(e, options_for_droppable); @@ -842,7 +872,7 @@ var Sortable = { only: sortableOptions.only, name: element.id, format: sortableOptions.format - }, arguments[1] || {}); + }, arguments[1] || { }); var root = { id: null, @@ -866,7 +896,7 @@ var Sortable = { sequence: function(element) { element = $(element); - var options = Object.extend(this.options(element), arguments[1] || {}); + var options = Object.extend(this.options(element), arguments[1] || { }); return $(this.findElements(element, options) || []).map( function(item) { return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; @@ -875,9 +905,9 @@ var Sortable = { setSequence: function(element, new_sequence) { element = $(element); - var options = Object.extend(this.options(element), arguments[2] || {}); + var options = Object.extend(this.options(element), arguments[2] || { }); - var nodeMap = {}; + var nodeMap = { }; this.findElements(element, options).each( function(n) { if (n.id.match(options.format)) nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; @@ -895,7 +925,7 @@ var Sortable = { serialize: function(element) { element = $(element); - var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var options = Object.extend(Sortable.options(element), arguments[1] || { }); var name = encodeURIComponent( (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); @@ -919,7 +949,7 @@ Element.isParent = function(child, element) { return Element.isParent(child.parentNode, element); } -Element.findChildren = function(element, only, recursive, tagName) { +Element.findChildren = function(element, only, recursive, tagName) { if(!element.hasChildNodes()) return null; tagName = tagName.toUpperCase(); if(only) only = [only].flatten(); diff --git a/groups/public/javascripts/effects.js b/groups/public/javascripts/effects.js index 3b02eda2b..f030b5dbe 100644 --- a/groups/public/javascripts/effects.js +++ b/groups/public/javascripts/effects.js @@ -1,4 +1,4 @@ -// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) @@ -11,17 +11,17 @@ // returns self (or first argument) if not convertable String.prototype.parseColor = function() { var color = '#'; - if(this.slice(0,4) == 'rgb(') { + 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(); + 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)); -} + return (color.length==7 ? color : (arguments[0] || this)); +}; /*--------------------------------------------------------------------------*/ @@ -30,7 +30,7 @@ Element.collectTextNodes = function(element) { return (node.nodeType==3 ? node.nodeValue : (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); }).flatten().join(''); -} +}; Element.collectTextNodesIgnoreClass = function(element, className) { return $A($(element).childNodes).collect( function(node) { @@ -38,47 +38,18 @@ Element.collectTextNodesIgnoreClass = function(element, className) { ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? Element.collectTextNodesIgnoreClass(node, className) : '')); }).flatten().join(''); -} +}; Element.setContentZoom = function(element, percent) { element = $(element); element.setStyle({fontSize: (percent/100) + 'em'}); - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + if (Prototype.Browser.WebKit) window.scrollBy(0,0); return element; -} - -Element.getOpacity = function(element){ - element = $(element); - var opacity; - if (opacity = element.getStyle('opacity')) - return parseFloat(opacity); - if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) - if(opacity[1]) return parseFloat(opacity[1]) / 100; - return 1.0; -} +}; -Element.setOpacity = function(element, value){ - element= $(element); - if (value == 1){ - element.setStyle({ opacity: - (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? - 0.999999 : 1.0 }); - if(/MSIE/.test(navigator.userAgent) && !window.opera) - element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); - } else { - if(value < 0.00001) value = 0; - element.setStyle({opacity: value}); - if(/MSIE/.test(navigator.userAgent) && !window.opera) - element.setStyle( - { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + - 'alpha(opacity='+value*100+')' }); - } - return element; -} - -Element.getInlineOpacity = function(element){ +Element.getInlineOpacity = function(element){ return $(element).style.opacity || ''; -} +}; Element.forceRerendering = function(element) { try { @@ -91,31 +62,63 @@ Element.forceRerendering = function(element) { /*--------------------------------------------------------------------------*/ -Array.prototype.call = function() { - var args = arguments; - this.each(function(f){ f.apply(this, args) }); -} - -/*--------------------------------------------------------------------------*/ - var Effect = { _elementDoesNotExistError: { name: 'ElementDoesNotExistError', message: 'The specified DOM element does not exist, but is required for this effect to operate' }, + Transitions: { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return pos > 1 ? 1 : pos; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + ((pos % (1/pulses)) * pulses).round() == 0 ? + ((pos * pulses * 2) - (pos * pulses * 2).floor()) : + 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor()) + ); + }, + spring: function(pos) { + return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } + }, + DefaultOptions: { + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' + }, tagifyText: function(element) { - if(typeof Builder == 'undefined') - throw("Effect.tagifyText requires including script.aculo.us' builder.js library"); - var tagifyStyle = 'position:relative'; - if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1'; + if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; element = $(element); $A(element.childNodes).each( function(child) { - if(child.nodeType==3) { + if (child.nodeType==3) { child.nodeValue.toArray().each( function(character) { element.insertBefore( - Builder.node('span',{style: tagifyStyle}, + new Element('span', {style: tagifyStyle}).update( character == ' ' ? String.fromCharCode(160) : character), child); }); @@ -125,8 +128,8 @@ var Effect = { }, multiple: function(element, effect) { var elements; - if(((typeof element == 'object') || - (typeof element == 'function')) && + if (((typeof element == 'object') || + Object.isFunction(element)) && (element.length)) elements = element; else @@ -135,7 +138,7 @@ var Effect = { var options = Object.extend({ speed: 0.1, delay: 0.0 - }, arguments[2] || {}); + }, arguments[2] || { }); var masterDelay = options.delay; $A(elements).each( function(element, index) { @@ -152,53 +155,20 @@ var Effect = { effect = (effect || 'appear').toLowerCase(); var options = Object.extend({ queue: { position:'end', scope:(element.id || 'global'), limit: 1 } - }, arguments[2] || {}); + }, arguments[2] || { }); Effect[element.visible() ? Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); } }; -var Effect2 = Effect; // deprecated - -/* ------------- transitions ------------- */ - -Effect.Transitions = { - linear: Prototype.K, - sinoidal: function(pos) { - return (-Math.cos(pos*Math.PI)/2) + 0.5; - }, - reverse: function(pos) { - return 1-pos; - }, - flicker: function(pos) { - return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; - }, - wobble: function(pos) { - return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; - }, - pulse: function(pos, pulses) { - pulses = pulses || 5; - return ( - Math.round((pos % (1/pulses)) * pulses) == 0 ? - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : - 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) - ); - }, - none: function(pos) { - return 0; - }, - full: function(pos) { - return 1; - } -}; +Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; /* ------------- core effects ------------- */ -Effect.ScopedQueue = Class.create(); -Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { +Effect.ScopedQueue = Class.create(Enumerable, { initialize: function() { this.effects = []; - this.interval = null; + this.interval = null; }, _each: function(iterator) { this.effects._each(iterator); @@ -206,7 +176,7 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { add: function(effect) { var timestamp = new Date().getTime(); - var position = (typeof effect.options.queue == 'string') ? + var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position; switch(position) { @@ -229,115 +199,111 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { effect.startOn += timestamp; effect.finishOn += timestamp; - if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) this.effects.push(effect); - if(!this.interval) - this.interval = setInterval(this.loop.bind(this), 40); + if (!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); }, remove: function(effect) { this.effects = this.effects.reject(function(e) { return e==effect }); - if(this.effects.length == 0) { + if (this.effects.length == 0) { clearInterval(this.interval); this.interval = null; } }, loop: function() { var timePos = new Date().getTime(); - this.effects.invoke('loop', timePos); + for(var i=0, len=this.effects.length;i<len;i++) + this.effects[i] && this.effects[i].loop(timePos); } }); Effect.Queues = { instances: $H(), get: function(queueName) { - if(typeof queueName != 'string') return queueName; + if (!Object.isString(queueName)) return queueName; - if(!this.instances[queueName]) - this.instances[queueName] = new Effect.ScopedQueue(); - - return this.instances[queueName]; + return this.instances.get(queueName) || + this.instances.set(queueName, new Effect.ScopedQueue()); } -} +}; Effect.Queue = Effect.Queues.get('global'); -Effect.DefaultOptions = { - transition: Effect.Transitions.sinoidal, - duration: 1.0, // seconds - fps: 25.0, // max. 25fps due to Effect.Queue implementation - sync: false, // true for combining - from: 0.0, - to: 1.0, - delay: 0.0, - queue: 'parallel' -} - -Effect.Base = function() {}; -Effect.Base.prototype = { +Effect.Base = Class.create({ position: null, start: function(options) { - this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + function codeForEvent(options,eventName){ + return ( + (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') + + (options[eventName] ? 'this.options.'+eventName+'(this);' : '') + ); + } + if (options && options.transition === false) options.transition = Effect.Transitions.linear; + this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { }); this.currentFrame = 0; this.state = 'idle'; this.startOn = this.options.delay*1000; - this.finishOn = this.startOn + (this.options.duration*1000); + this.finishOn = this.startOn+(this.options.duration*1000); + this.fromToDelta = this.options.to-this.options.from; + this.totalTime = this.finishOn-this.startOn; + this.totalFrames = this.options.fps*this.options.duration; + + eval('this.render = function(pos){ '+ + 'if (this.state=="idle"){this.state="running";'+ + codeForEvent(this.options,'beforeSetup')+ + (this.setup ? 'this.setup();':'')+ + codeForEvent(this.options,'afterSetup')+ + '};if (this.state=="running"){'+ + 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+ + 'this.position=pos;'+ + codeForEvent(this.options,'beforeUpdate')+ + (this.update ? 'this.update(pos);':'')+ + codeForEvent(this.options,'afterUpdate')+ + '}}'); + this.event('beforeStart'); - if(!this.options.sync) - Effect.Queues.get(typeof this.options.queue == 'string' ? + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).add(this); }, loop: function(timePos) { - if(timePos >= this.startOn) { - if(timePos >= this.finishOn) { + if (timePos >= this.startOn) { + if (timePos >= this.finishOn) { this.render(1.0); this.cancel(); this.event('beforeFinish'); - if(this.finish) this.finish(); + 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) { + var pos = (timePos - this.startOn) / this.totalTime, + frame = (pos * this.totalFrames).round(); + if (frame > this.currentFrame) { this.render(pos); this.currentFrame = frame; } } }, - render: function(pos) { - if(this.state == 'idle') { - this.state = 'running'; - this.event('beforeSetup'); - if(this.setup) this.setup(); - this.event('afterSetup'); - } - if(this.state == 'running') { - if(this.options.transition) pos = this.options.transition(pos); - pos *= (this.options.to-this.options.from); - pos += this.options.from; - this.position = pos; - this.event('beforeUpdate'); - if(this.update) this.update(pos); - this.event('afterUpdate'); - } - }, cancel: function() { - if(!this.options.sync) - Effect.Queues.get(typeof this.options.queue == 'string' ? + if (!this.options.sync) + Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).remove(this); this.state = 'finished'; }, event: function(eventName) { - if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); - if(this.options[eventName]) this.options[eventName](this); + if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if (this.options[eventName]) this.options[eventName](this); }, inspect: function() { - return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>'; + var data = $H(); + for(property in this) + if (!Object.isFunction(this[property])) data.set(property, this[property]); + return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>'; } -} +}); -Effect.Parallel = Class.create(); -Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { +Effect.Parallel = Class.create(Effect.Base, { initialize: function(effects) { this.effects = effects || []; this.start(arguments[1]); @@ -350,35 +316,45 @@ Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { effect.render(1.0); effect.cancel(); effect.event('beforeFinish'); - if(effect.finish) effect.finish(position); + if (effect.finish) effect.finish(position); effect.event('afterFinish'); }); } }); -Effect.Event = Class.create(); -Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), { +Effect.Tween = Class.create(Effect.Base, { + initialize: function(object, from, to) { + object = Object.isString(object) ? $(object) : object; + var args = $A(arguments), method = args.last(), + options = args.length == 5 ? args[3] : null; + this.method = Object.isFunction(method) ? method.bind(object) : + Object.isFunction(object[method]) ? object[method].bind(object) : + function(value) { object[method] = value }; + this.start(Object.extend({ from: from, to: to }, options || { })); + }, + update: function(position) { + this.method(position); + } +}); + +Effect.Event = Class.create(Effect.Base, { initialize: function() { - var options = Object.extend({ - duration: 0 - }, arguments[0] || {}); - this.start(options); + this.start(Object.extend({ duration: 0 }, arguments[0] || { })); }, update: Prototype.emptyFunction }); -Effect.Opacity = Class.create(); -Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { +Effect.Opacity = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); - if(!this.element) throw(Effect._elementDoesNotExistError); + if (!this.element) throw(Effect._elementDoesNotExistError); // make this work on IE on elements without 'layout' - if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout)) + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) this.element.setStyle({zoom: 1}); var options = Object.extend({ from: this.element.getOpacity() || 0.0, to: 1.0 - }, arguments[1] || {}); + }, arguments[1] || { }); this.start(options); }, update: function(position) { @@ -386,36 +362,30 @@ Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { } }); -Effect.Move = Class.create(); -Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { +Effect.Move = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); - if(!this.element) throw(Effect._elementDoesNotExistError); + if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ x: 0, y: 0, mode: 'relative' - }, arguments[1] || {}); + }, arguments[1] || { }); this.start(options); }, setup: function() { - // Bug in Opera: Opera returns the "real" position of a static element or - // relative element that does not have top/left explicitly set. - // ==> Always set top and left for position relative elements in your stylesheets - // (to 0 if you do not need them) this.element.makePositioned(); this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); this.originalTop = parseFloat(this.element.getStyle('top') || '0'); - if(this.options.mode == 'absolute') { - // absolute movement, so we need to calc deltaX and deltaY + if (this.options.mode == 'absolute') { this.options.x = this.options.x - this.originalLeft; this.options.y = this.options.y - this.originalTop; } }, update: function(position) { this.element.setStyle({ - left: Math.round(this.options.x * position + this.originalLeft) + 'px', - top: Math.round(this.options.y * position + this.originalTop) + 'px' + left: (this.options.x * position + this.originalLeft).round() + 'px', + top: (this.options.y * position + this.originalTop).round() + 'px' }); } }); @@ -423,30 +393,29 @@ Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { // for backwards compatibility Effect.MoveBy = function(element, toTop, toLeft) { return new Effect.Move(element, - Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); + Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); }; -Effect.Scale = Class.create(); -Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { +Effect.Scale = Class.create(Effect.Base, { initialize: function(element, percent) { this.element = $(element); - if(!this.element) throw(Effect._elementDoesNotExistError); + if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ scaleX: true, scaleY: true, scaleContent: true, scaleFromCenter: false, - scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleMode: 'box', // 'box' or 'contents' or { } with provided values scaleFrom: 100.0, scaleTo: percent - }, arguments[2] || {}); + }, arguments[2] || { }); this.start(options); }, setup: function() { this.restoreAfterFinish = this.options.restoreAfterFinish || false; this.elementPositioning = this.element.getStyle('position'); - this.originalStyle = {}; + this.originalStyle = { }; ['top','left','width','height','fontSize'].each( function(k) { this.originalStyle[k] = this.element.style[k]; }.bind(this)); @@ -456,7 +425,7 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { var fontSize = this.element.getStyle('font-size') || '100%'; ['em','px','%','pt'].each( function(fontSizeType) { - if(fontSize.indexOf(fontSizeType)>0) { + if (fontSize.indexOf(fontSizeType)>0) { this.fontSize = parseFloat(fontSize); this.fontSizeType = fontSizeType; } @@ -465,60 +434,61 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; this.dims = null; - if(this.options.scaleMode=='box') + if (this.options.scaleMode=='box') this.dims = [this.element.offsetHeight, this.element.offsetWidth]; - if(/^content/.test(this.options.scaleMode)) + if (/^content/.test(this.options.scaleMode)) this.dims = [this.element.scrollHeight, this.element.scrollWidth]; - if(!this.dims) + 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) + if (this.options.scaleContent && this.fontSize) this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); }, finish: function(position) { - if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); }, setDimensions: function(height, width) { - var d = {}; - if(this.options.scaleX) d.width = Math.round(width) + 'px'; - if(this.options.scaleY) d.height = Math.round(height) + 'px'; - if(this.options.scaleFromCenter) { + var d = { }; + if (this.options.scaleX) d.width = width.round() + 'px'; + if (this.options.scaleY) d.height = height.round() + 'px'; + if (this.options.scaleFromCenter) { var topd = (height - this.dims[0])/2; var leftd = (width - this.dims[1])/2; - if(this.elementPositioning == 'absolute') { - if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; - if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + 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'; + if (this.options.scaleY) d.top = -topd + 'px'; + if (this.options.scaleX) d.left = -leftd + 'px'; } } this.element.setStyle(d); } }); -Effect.Highlight = Class.create(); -Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { +Effect.Highlight = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); - if(!this.element) throw(Effect._elementDoesNotExistError); - var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + if (!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); this.start(options); }, setup: function() { // Prevent executing on elements not in the layout flow - if(this.element.getStyle('display')=='none') { this.cancel(); return; } + if (this.element.getStyle('display')=='none') { this.cancel(); return; } // Disable background image during the effect - this.oldStyle = { - backgroundImage: this.element.getStyle('background-image') }; - this.element.setStyle({backgroundImage: 'none'}); - if(!this.options.endcolor) + this.oldStyle = { }; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if (!this.options.endcolor) this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); - if(!this.options.restorecolor) + if (!this.options.restorecolor) this.options.restorecolor = this.element.getStyle('background-color'); // init color calculations this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); @@ -526,7 +496,7 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), }, update: function(position) { this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ - return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); }, finish: function() { this.element.setStyle(Object.extend(this.oldStyle, { @@ -535,30 +505,21 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), } }); -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)); - } -}); +Effect.ScrollTo = function(element) { + var options = arguments[1] || { }, + scrollOffsets = document.viewport.getScrollOffsets(), + elementOffsets = $(element).cumulativeOffset(), + max = (window.height || document.body.scrollHeight) - document.viewport.getHeight(); + + if (options.offset) elementOffsets[1] += options.offset; + + return new Effect.Tween(null, + scrollOffsets.top, + elementOffsets[1] > max ? max : elementOffsets[1], + options, + function(p){ scrollTo(scrollOffsets.left, p.round()) } + ); +}; /* ------------- combination effects ------------- */ @@ -566,14 +527,15 @@ Effect.Fade = function(element) { element = $(element); var oldOpacity = element.getInlineOpacity(); var options = Object.extend({ - from: element.getOpacity() || 1.0, - to: 0.0, - afterFinishInternal: function(effect) { - if(effect.options.to!=0) return; - effect.element.hide().setStyle({opacity: oldOpacity}); - }}, arguments[1] || {}); + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if (effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + } + }, arguments[1] || { }); return new Effect.Opacity(element,options); -} +}; Effect.Appear = function(element) { element = $(element); @@ -586,9 +548,9 @@ Effect.Appear = function(element) { }, beforeSetup: function(effect) { effect.element.setOpacity(effect.options.from).show(); - }}, arguments[1] || {}); + }}, arguments[1] || { }); return new Effect.Opacity(element,options); -} +}; Effect.Puff = function(element) { element = $(element); @@ -610,9 +572,9 @@ Effect.Puff = function(element) { }, afterFinishInternal: function(effect) { effect.effects[0].element.hide().setStyle(oldStyle); } - }, arguments[1] || {}) + }, arguments[1] || { }) ); -} +}; Effect.BlindUp = function(element) { element = $(element); @@ -624,9 +586,9 @@ Effect.BlindUp = function(element) { afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } - }, arguments[1] || {}) + }, arguments[1] || { }) ); -} +}; Effect.BlindDown = function(element) { element = $(element); @@ -643,8 +605,8 @@ Effect.BlindDown = function(element) { afterFinishInternal: function(effect) { effect.element.undoClipping(); } - }, arguments[1] || {})); -} + }, arguments[1] || { })); +}; Effect.SwitchOff = function(element) { element = $(element); @@ -665,8 +627,8 @@ Effect.SwitchOff = function(element) { } }) } - }, arguments[1] || {})); -} + }, arguments[1] || { })); +}; Effect.DropOut = function(element) { element = $(element); @@ -685,29 +647,35 @@ Effect.DropOut = function(element) { afterFinishInternal: function(effect) { effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); } - }, arguments[1] || {})); -} + }, arguments[1] || { })); +}; Effect.Shake = function(element) { element = $(element); + var options = Object.extend({ + distance: 20, + duration: 0.5 + }, arguments[1] || {}); + var distance = parseFloat(options.distance); + var split = parseFloat(options.duration) / 10.0; var oldStyle = { top: element.getStyle('top'), left: element.getStyle('left') }; - return new Effect.Move(element, - { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + return new Effect.Move(element, + { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { new Effect.Move(effect.element, - { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, - { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, - { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, - { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { new Effect.Move(effect.element, - { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { effect.element.undoPositioned().setStyle(oldStyle); }}) }}) }}) }}) }}) }}); -} +}; Effect.SlideDown = function(element) { element = $(element).cleanWhitespace(); @@ -723,7 +691,7 @@ Effect.SlideDown = function(element) { afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); - if(window.opera) effect.element.setStyle({top: ''}); + if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().setStyle({height: '0px'}).show(); }, afterUpdateInternal: function(effect) { @@ -733,23 +701,25 @@ Effect.SlideDown = function(element) { afterFinishInternal: function(effect) { effect.element.undoClipping().undoPositioned(); effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } - }, arguments[1] || {}) + }, arguments[1] || { }) ); -} +}; Effect.SlideUp = function(element) { element = $(element).cleanWhitespace(); var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); return new Effect.Scale(element, window.opera ? 0 : 1, Object.extend({ scaleContent: false, scaleX: false, scaleMode: 'box', scaleFrom: 100, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, - beforeStartInternal: function(effect) { + afterSetup: function(effect) { effect.element.makePositioned(); effect.element.down().makePositioned(); - if(window.opera) effect.element.setStyle({top: ''}); + if (window.opera) effect.element.setStyle({top: ''}); effect.element.makeClipping().show(); }, afterUpdateInternal: function(effect) { @@ -757,12 +727,12 @@ Effect.SlideUp = function(element) { (effect.dims[0] - effect.element.clientHeight) + 'px' }); }, afterFinishInternal: function(effect) { - effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom}); - effect.element.down().undoPositioned(); + effect.element.hide().undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } - }, arguments[1] || {}) + }, arguments[1] || { }) ); -} +}; // Bug in opera makes the TD containing this element expand for a instance after finish Effect.Squish = function(element) { @@ -775,7 +745,7 @@ Effect.Squish = function(element) { effect.element.hide().undoClipping(); } }); -} +}; Effect.Grow = function(element) { element = $(element); @@ -784,7 +754,7 @@ Effect.Grow = function(element) { moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.full - }, arguments[1] || {}); + }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, @@ -849,7 +819,7 @@ Effect.Grow = function(element) { ) } }); -} +}; Effect.Shrink = function(element) { element = $(element); @@ -858,7 +828,7 @@ Effect.Shrink = function(element) { moveTransition: Effect.Transitions.sinoidal, scaleTransition: Effect.Transitions.sinoidal, opacityTransition: Effect.Transitions.none - }, arguments[1] || {}); + }, arguments[1] || { }); var oldStyle = { top: element.style.top, left: element.style.left, @@ -903,11 +873,11 @@ Effect.Shrink = function(element) { effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); -} +}; Effect.Pulsate = function(element) { element = $(element); - var options = arguments[1] || {}; + var options = arguments[1] || { }; var oldOpacity = element.getInlineOpacity(); var transition = options.transition || Effect.Transitions.sinoidal; var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; @@ -916,7 +886,7 @@ Effect.Pulsate = function(element) { Object.extend(Object.extend({ duration: 2.0, from: 0, afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } }, options), {transition: reverser})); -} +}; Effect.Fold = function(element) { element = $(element); @@ -936,37 +906,71 @@ Effect.Fold = function(element) { afterFinishInternal: function(effect) { effect.element.hide().undoClipping().setStyle(oldStyle); } }); - }}, arguments[1] || {})); + }}, arguments[1] || { })); }; -Effect.Morph = Class.create(); -Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { +Effect.Morph = Class.create(Effect.Base, { initialize: function(element) { this.element = $(element); - if(!this.element) throw(Effect._elementDoesNotExistError); + if (!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ - style: '' - }, arguments[1] || {}); + style: { } + }, arguments[1] || { }); + + if (!Object.isString(options.style)) this.style = $H(options.style); + else { + if (options.style.include(':')) + this.style = options.style.parseStyle(); + else { + this.element.addClassName(options.style); + this.style = $H(this.element.getStyles()); + this.element.removeClassName(options.style); + var css = this.element.getStyles(); + this.style = this.style.reject(function(style) { + return style.value == css[style.key]; + }); + options.afterFinishInternal = function(effect) { + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + effect.element.style[transform.style] = ''; + }); + } + } + } this.start(options); }, + setup: function(){ function parseColor(color){ - if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; color = color.parseColor(); return $R(0,2).map(function(i){ return parseInt( color.slice(i*2+1,i*2+3), 16 ) }); } - this.transforms = this.options.style.parseStyle().map(function(property){ - var originalValue = this.element.getStyle(property[0]); - return $H({ - style: property[0], - originalValue: property[1].unit=='color' ? - parseColor(originalValue) : parseFloat(originalValue || 0), - targetValue: property[1].unit=='color' ? - parseColor(property[1].value) : property[1].value, - unit: property[1].unit - }); + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if (value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if (property == 'opacity') { + value = parseFloat(value); + if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if (Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; }.bind(this)).reject(function(transform){ return ( (transform.originalValue == transform.targetValue) || @@ -978,32 +982,35 @@ Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { }); }, update: function(position) { - var style = $H(), value = null; - this.transforms.each(function(transform){ - value = transform.unit=='color' ? - $R(0,2).inject('#',function(m,v,i){ - return m+(Math.round(transform.originalValue[i]+ - (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : - transform.originalValue + Math.round( - ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit; - style[transform.style] = value; - }); - this.element.setStyle(style); + var style = { }, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + (transform.originalValue + + (transform.targetValue - transform.originalValue) * position).toFixed(3) + + (transform.unit === null ? '' : transform.unit); + this.element.setStyle(style, true); } }); -Effect.Transform = Class.create(); -Object.extend(Effect.Transform.prototype, { +Effect.Transform = Class.create({ initialize: function(tracks){ this.tracks = []; - this.options = arguments[1] || {}; + this.options = arguments[1] || { }; this.addTracks(tracks); }, addTracks: function(tracks){ tracks.each(function(track){ - var data = $H(track).values().first(); + track = $H(track); + var data = track.values().first(); this.tracks.push($H({ - ids: $H(track).keys().first(), + ids: track.keys().first(), effect: Effect.Morph, options: { style: data } })); @@ -1013,76 +1020,101 @@ Object.extend(Effect.Transform.prototype, { play: function(){ return new Effect.Parallel( this.tracks.map(function(track){ - var elements = [$(track.ids) || $$(track.ids)].flatten(); - return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) }); + var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); + var elements = [$(ids) || $$(ids)].flatten(); + return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); }).flatten(), this.options ); } }); -Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', - 'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', - 'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth', - 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor', - 'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content', - 'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction', - 'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', - 'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight', - 'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight', - 'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity', - 'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY', - 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore', - 'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes', - 'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress', - 'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top', - 'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows', - 'width', 'wordSpacing', 'zIndex']; +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; +String.__parseStyleElement = document.createElement('div'); String.prototype.parseStyle = function(){ - var element = Element.extend(document.createElement('div')); - element.innerHTML = '<div style="' + this + '"></div>'; - var style = element.down().style, styleRules = $H(); + var style, styleRules = $H(); + if (Prototype.Browser.WebKit) + style = new Element('div',{style:this}).style; + else { + String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>'; + style = String.__parseStyleElement.childNodes[0].style; + } Element.CSS_PROPERTIES.each(function(property){ - if(style[property]) styleRules[property] = style[property]; + if (style[property]) styleRules.set(property, style[property]); }); - var result = $H(); - - styleRules.each(function(pair){ - var property = pair[0], value = pair[1], unit = null; - - if(value.parseColor('#zzzzzz') != '#zzzzzz') { - value = value.parseColor(); - unit = 'color'; - } else if(Element.CSS_LENGTH.test(value)) - var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/), - value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null; - - result[property.underscore().dasherize()] = $H({ value:value, unit:unit }); - }.bind(this)); - - return result; + if (Prototype.Browser.IE && this.include('opacity')) + styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); + + return styleRules; }; -Element.morph = function(element, style) { - new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {})); - return element; +if (document.defaultView && document.defaultView.getComputedStyle) { + Element.getStyles = function(element) { + var css = document.defaultView.getComputedStyle($(element), null); + return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { + styles[property] = css[property]; + return styles; + }); + }; +} else { + Element.getStyles = function(element) { + element = $(element); + var css = element.currentStyle, styles; + styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) { + hash.set(property, css[property]); + return hash; + }); + if (!styles.opacity) styles.set('opacity', element.getOpacity()); + return styles; + }; }; -['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', - 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( - function(f) { Element.Methods[f] = Element[f]; } +Effect.Methods = { + morph: function(element, style) { + element = $(element); + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); + return element; + }, + visualEffect: function(element, effect, options) { + element = $(element) + var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[klass](element, options); + return element; + }, + highlight: function(element, options) { + element = $(element); + new Effect.Highlight(element, options); + return element; + } +}; + +$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ + 'pulsate shake puff squish switchOff dropOut').each( + function(effect) { + Effect.Methods[effect] = function(element, options){ + element = $(element); + Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); + return element; + } + } ); -Element.Methods.visualEffect = function(element, effect, options) { - s = effect.gsub(/_/, '-').camelize(); - effect_class = s.charAt(0).toUpperCase() + s.substring(1); - new Effect[effect_class](element, options); - return $(element); -}; +$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( + function(f) { Effect.Methods[f] = Element[f]; } +); -Element.addMethods();
\ No newline at end of file +Element.addMethods(Effect.Methods); diff --git a/groups/public/javascripts/jstoolbar/jstoolbar.js b/groups/public/javascripts/jstoolbar/jstoolbar.js index be982d4b9..64c460217 100644 --- a/groups/public/javascripts/jstoolbar/jstoolbar.js +++ b/groups/public/javascripts/jstoolbar/jstoolbar.js @@ -498,6 +498,37 @@ jsToolBar.prototype.elements.ol = { } } +// spacer +jsToolBar.prototype.elements.space3 = {type: 'space'} + +// bq +jsToolBar.prototype.elements.bq = { + type: 'button', + title: 'Quote', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); + }); + } + } +} + +// unbq +jsToolBar.prototype.elements.unbq = { + type: 'button', + title: 'Unquote', + fn: { + wiki: function() { + this.encloseLineSelection('','',function(str) { + str = str.replace(/\r/g,''); + return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); + }); + } + } +} + // pre jsToolBar.prototype.elements.pre = { type: 'button', @@ -508,7 +539,7 @@ jsToolBar.prototype.elements.pre = { } // spacer -jsToolBar.prototype.elements.space3 = {type: 'space'} +jsToolBar.prototype.elements.space4 = {type: 'space'} // wiki page jsToolBar.prototype.elements.link = { diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js index 8a59a8162..f2c0dbff5 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Záhlavà 2'; jsToolBar.strings['Heading 3'] = 'Záhlavà 3'; jsToolBar.strings['Unordered list'] = 'Seznam'; jsToolBar.strings['Ordered list'] = 'Uspořádaný seznam'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'PÅ™edformátovaný text'; jsToolBar.strings['Wiki link'] = 'Vložit odkaz na Wiki stránku'; jsToolBar.strings['Image'] = 'Vložit obrázek'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js index 9996acaf3..6ccc8ead2 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Overskrift 2'; jsToolBar.strings['Heading 3'] = 'Overskrift 3'; jsToolBar.strings['Unordered list'] = 'Unummereret list'; jsToolBar.strings['Ordered list'] = 'Nummereret list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatteret tekst'; jsToolBar.strings['Wiki link'] = 'Link til en Wiki side'; jsToolBar.strings['Image'] = 'Billede'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js index e2ba3fc1c..ce686860f 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Überschrift 2. Ordnung'; jsToolBar.strings['Heading 3'] = 'Überschrift 3. Ordnung'; jsToolBar.strings['Unordered list'] = 'Aufzählungsliste'; jsToolBar.strings['Ordered list'] = 'Nummerierte Liste'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Präformatierter Text'; jsToolBar.strings['Wiki link'] = 'Verweis (Link) zu einer Wiki-Seite'; jsToolBar.strings['Image'] = 'Grafik'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js index 357d25951..c2229b281 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Otsikko 2'; jsToolBar.strings['Heading 3'] = 'Otsikko 3'; jsToolBar.strings['Unordered list'] = 'Järjestämätön lista'; jsToolBar.strings['Ordered list'] = 'Järjestetty lista'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Ennaltamuotoiltu teksti'; jsToolBar.strings['Wiki link'] = 'Linkki Wiki sivulle'; jsToolBar.strings['Image'] = 'Kuva'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js index 3cbc67863..c52a783bc 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Titre niveau 2'; jsToolBar.strings['Heading 3'] = 'Titre niveau 3'; jsToolBar.strings['Unordered list'] = 'Liste à puces'; jsToolBar.strings['Ordered list'] = 'Liste numérotée'; +jsToolBar.strings['Quote'] = 'Citer'; +jsToolBar.strings['Unquote'] = 'Supprimer citation'; jsToolBar.strings['Preformatted text'] = 'Texte préformaté'; jsToolBar.strings['Wiki link'] = 'Lien vers une page Wiki'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js new file mode 100644 index 000000000..c31ba00c0 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Félkövér'; +jsToolBar.strings['Italic'] = 'DÅ‘lt'; +jsToolBar.strings['Underline'] = 'Aláhúzott'; +jsToolBar.strings['Deleted'] = 'Törölt'; +jsToolBar.strings['Code'] = 'Kód sorok'; +jsToolBar.strings['Heading 1'] = 'Fejléc 1'; +jsToolBar.strings['Heading 2'] = 'Fejléc 2'; +jsToolBar.strings['Heading 3'] = 'Fejléc 3'; +jsToolBar.strings['Unordered list'] = 'Felsorolás'; +jsToolBar.strings['Ordered list'] = 'Számozott lista'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'ElÅ‘reformázott szöveg'; +jsToolBar.strings['Wiki link'] = 'Link egy Wiki oldalra'; +jsToolBar.strings['Image'] = 'Kép'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js index fc4d987de..c9413dac2 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = '見出㗠2'; jsToolBar.strings['Heading 3'] = '見出㗠3'; jsToolBar.strings['Unordered list'] = 'é †ä¸åŒãƒªã‚¹ãƒˆ'; jsToolBar.strings['Ordered list'] = '番å·ã¤ãリスト'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = '整形済ã¿ãƒ†ã‚スト'; jsToolBar.strings['Wiki link'] = 'Wiki ページã¸ã®ãƒªãƒ³ã‚¯'; jsToolBar.strings['Image'] = 'ç”»åƒ'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js index f0a7c5d90..8af364c8d 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Nenumeruotas sÄ…raÅ¡as'; jsToolBar.strings['Ordered list'] = 'Numeruotas sÄ…raÅ¡as'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatuotas tekstas'; jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį'; jsToolBar.strings['Image'] = 'Paveikslas'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js index cf6e19ff9..799597343 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Overskrift 2'; jsToolBar.strings['Heading 3'] = 'Overskrift 3'; jsToolBar.strings['Unordered list'] = 'Punktliste'; jsToolBar.strings['Ordered list'] = 'Nummerert liste'; +jsToolBar.strings['Quote'] = 'Sitat'; +jsToolBar.strings['Unquote'] = 'Avslutt sitat'; jsToolBar.strings['Preformatted text'] = 'Preformatert tekst'; jsToolBar.strings['Wiki link'] = 'Lenke til Wiki-side'; jsToolBar.strings['Image'] = 'Bilde'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js index cd36a4b55..5035524ab 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js @@ -1,14 +1,18 @@ +// Translated by: Alexandre da Silva <simpsomboy@gmail.com> + jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Code'] = 'Inline Code'; -jsToolBar.strings['Heading 1'] = 'Heading 1'; -jsToolBar.strings['Heading 2'] = 'Heading 2'; -jsToolBar.strings['Heading 3'] = 'Heading 3'; -jsToolBar.strings['Unordered list'] = 'Unordered list'; -jsToolBar.strings['Ordered list'] = 'Ordered list'; -jsToolBar.strings['Preformatted text'] = 'Preformatted text'; -jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; -jsToolBar.strings['Image'] = 'Image'; +jsToolBar.strings['Strong'] = 'Negrito'; +jsToolBar.strings['Italic'] = 'Itálico'; +jsToolBar.strings['Underline'] = 'Sublinhado'; +jsToolBar.strings['Deleted'] = 'ExcluÃdo'; +jsToolBar.strings['Code'] = 'Código Inline'; +jsToolBar.strings['Heading 1'] = 'Cabeçalho 1'; +jsToolBar.strings['Heading 2'] = 'Cabeçalho 2'; +jsToolBar.strings['Heading 3'] = 'Cabeçalho 3'; +jsToolBar.strings['Unordered list'] = 'Lista não ordenada'; +jsToolBar.strings['Ordered list'] = 'Lista ordenada'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'Texto pré-formatado'; +jsToolBar.strings['Wiki link'] = 'Link para uma página Wiki'; +jsToolBar.strings['Image'] = 'Imagem'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js index 6370a3e2d..a6d8c4fad 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Заголовок 2'; jsToolBar.strings['Heading 3'] = 'Заголовок 3'; jsToolBar.strings['Unordered list'] = 'Маркированный ÑпиÑок'; jsToolBar.strings['Ordered list'] = 'Ðумерованный ÑпиÑок'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Заранее форматированный текÑÑ‚'; jsToolBar.strings['Wiki link'] = 'СÑылка на Ñтраницу в Wiki'; jsToolBar.strings['Image'] = 'Ð’Ñтавка изображениÑ'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js new file mode 100644 index 000000000..d87164226 --- /dev/null +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'หนา'; +jsToolBar.strings['Italic'] = 'เà¸à¸µà¸¢à¸‡'; +jsToolBar.strings['Underline'] = 'ขีดเส้นใต้'; +jsToolBar.strings['Deleted'] = 'ขีดฆ่า'; +jsToolBar.strings['Code'] = 'โค๊ดโปรà¹à¸à¸£à¸¡'; +jsToolBar.strings['Heading 1'] = 'หัวข้ภ1'; +jsToolBar.strings['Heading 2'] = 'หัวข้ภ2'; +jsToolBar.strings['Heading 3'] = 'หัวข้ภ3'; +jsToolBar.strings['Unordered list'] = 'รายà¸à¸²à¸£'; +jsToolBar.strings['Ordered list'] = 'ลำดับเลข'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; +jsToolBar.strings['Preformatted text'] = 'รูปà¹à¸šà¸šà¸‚้à¸à¸„วามคงที่'; +jsToolBar.strings['Wiki link'] = 'เชื่à¸à¸¡à¹‚ยงไปหน้า Wiki à¸à¸·à¹ˆà¸™'; +jsToolBar.strings['Image'] = 'รูปภาพ'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js index cd36a4b55..2d68498f9 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; jsToolBar.strings['Unordered list'] = 'Unordered list'; jsToolBar.strings['Ordered list'] = 'Ordered list'; +jsToolBar.strings['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatted text'; jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; jsToolBar.strings['Image'] = 'Image'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js index 1e46e2470..86599c5a2 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js @@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = '標題 2'; jsToolBar.strings['Heading 3'] = '標題 3'; jsToolBar.strings['Unordered list'] = 'é …ç›®æ¸…å–®'; jsToolBar.strings['Ordered list'] = '編號清單'; -jsToolBar.strings['Preformatted text'] = 'æ ¼å¼åŒ–æ–‡å—'; +jsToolBar.strings['Quote'] = '引文'; +jsToolBar.strings['Unquote'] = 'å–æ¶ˆå¼•æ–‡'; +jsToolBar.strings['Preformatted text'] = 'å·²æ ¼å¼æ–‡å—'; jsToolBar.strings['Wiki link'] = '連çµè‡³ Wiki é é¢'; jsToolBar.strings['Image'] = '圖片'; diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js index cd36a4b55..a9b6ba230 100644 --- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js +++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js @@ -1,14 +1,16 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Code'] = 'Inline Code'; -jsToolBar.strings['Heading 1'] = 'Heading 1'; -jsToolBar.strings['Heading 2'] = 'Heading 2'; -jsToolBar.strings['Heading 3'] = 'Heading 3'; -jsToolBar.strings['Unordered list'] = 'Unordered list'; -jsToolBar.strings['Ordered list'] = 'Ordered list'; -jsToolBar.strings['Preformatted text'] = 'Preformatted text'; -jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; -jsToolBar.strings['Image'] = 'Image'; +jsToolBar.strings['Strong'] = '粗体'; +jsToolBar.strings['Italic'] = '斜体'; +jsToolBar.strings['Underline'] = '下划线'; +jsToolBar.strings['Deleted'] = 'åˆ é™¤çº¿'; +jsToolBar.strings['Code'] = '程åºä»£ç '; +jsToolBar.strings['Heading 1'] = 'æ ‡é¢˜ 1'; +jsToolBar.strings['Heading 2'] = 'æ ‡é¢˜ 2'; +jsToolBar.strings['Heading 3'] = 'æ ‡é¢˜ 3'; +jsToolBar.strings['Unordered list'] = 'æ— åºåˆ—表'; +jsToolBar.strings['Ordered list'] = '排åºåˆ—表'; +jsToolBar.strings['Quote'] = '引用'; +jsToolBar.strings['Unquote'] = 'åˆ é™¤å¼•ç”¨'; +jsToolBar.strings['Preformatted text'] = 'æ ¼å¼åŒ–文本'; +jsToolBar.strings['Wiki link'] = '连接到 Wiki 页é¢'; +jsToolBar.strings['Image'] = '图片'; diff --git a/groups/public/javascripts/prototype.js b/groups/public/javascripts/prototype.js index 2735d10dc..546f9fe44 100644 --- a/groups/public/javascripts/prototype.js +++ b/groups/public/javascripts/prototype.js @@ -1,43 +1,114 @@ -/* Prototype JavaScript framework, version 1.5.0 +/* Prototype JavaScript framework, version 1.6.0.1 * (c) 2005-2007 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. - * For details, see the Prototype web site: http://prototype.conio.net/ + * For details, see the Prototype web site: http://www.prototypejs.org/ * -/*--------------------------------------------------------------------------*/ + *--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.5.0', + Version: '1.6.0.1', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + BrowserFeatures: { - XPath: !!document.evaluate + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div').__proto__ && + document.createElement('div').__proto__ !== + document.createElement('form').__proto__ }, - ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', - emptyFunction: function() {}, + ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, K: function(x) { return x } -} +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + +/* Based on Alex Arnell's inheritance implementation. */ var Class = { create: function() { - return function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { this.initialize.apply(this, arguments); } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value, value = Object.extend((function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method), { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + this.prototype[property] = value; + } + + return this; } -} +}; -var Abstract = new Object(); +var Abstract = { }; Object.extend = function(destination, source) { - for (var property in source) { + for (var property in source) destination[property] = source[property]; - } return destination; -} +}; Object.extend(Object, { inspect: function(object) { try { - if (object === undefined) return 'undefined'; + if (Object.isUndefined(object)) return 'undefined'; if (object === null) return 'null'; return object.inspect ? object.inspect() : object.toString(); } catch (e) { @@ -46,6 +117,37 @@ Object.extend(Object, { } }, + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (!Object.isUndefined(value)) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + keys: function(object) { var keys = []; for (var property in object) @@ -61,41 +163,101 @@ Object.extend(Object, { }, clone: function(object) { - return Object.extend({}, object); + return Object.extend({ }, object); + }, + + isElement: function(object) { + return object && object.nodeType == 1; + }, + + isArray: function(object) { + return object && object.constructor === Array; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; } }); -Function.prototype.bind = function() { - var __method = this, args = $A(arguments), object = args.shift(); - return function() { - return __method.apply(object, args.concat($A(arguments))); - } -} +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip"); + return names.length == 1 && !names[0] ? [] : names; + }, -Function.prototype.bindAsEventListener = function(object) { - var __method = this, args = $A(arguments), object = args.shift(); - return function(event) { - return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments))); - } -} + bind: function() { + if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, -Object.extend(Number.prototype, { - toColorPart: function() { - var digits = this.toString(16); - if (this < 16) return '0' + digits; - return digits; + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } }, - succ: function() { - return this + 1; + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } }, - times: function(iterator) { - $R(0, this, true).each(iterator); - return this; + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; } }); +Function.prototype.defer = Function.prototype.delay.curry(0.01); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + var Try = { these: function() { var returnValue; @@ -105,17 +267,22 @@ var Try = { try { returnValue = lambda(); break; - } catch (e) {} + } catch (e) { } } return returnValue; } -} +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; /*--------------------------------------------------------------------------*/ -var PeriodicalExecuter = Class.create(); -PeriodicalExecuter.prototype = { +var PeriodicalExecuter = Class.create({ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; @@ -128,6 +295,10 @@ PeriodicalExecuter.prototype = { this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }, + execute: function() { + this.callback(this); + }, + stop: function() { if (!this.timer) return; clearInterval(this.timer); @@ -138,16 +309,26 @@ PeriodicalExecuter.prototype = { if (!this.currentlyExecuting) { try { this.currentlyExecuting = true; - this.callback(this); + this.execute(); } finally { this.currentlyExecuting = false; } } } -} -String.interpret = function(value){ - return value == null ? '' : String(value); -} +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); Object.extend(String.prototype, { gsub: function(pattern, replacement) { @@ -168,7 +349,7 @@ Object.extend(String.prototype, { sub: function(pattern, replacement, count) { replacement = this.gsub.prepareReplacement(replacement); - count = count === undefined ? 1 : count; + count = Object.isUndefined(count) ? 1 : count; return this.gsub(pattern, function(match) { if (--count < 0) return match[0]; @@ -178,14 +359,14 @@ Object.extend(String.prototype, { scan: function(pattern, iterator) { this.gsub(pattern, iterator); - return this; + return String(this); }, truncate: function(length, truncation) { length = length || 30; - truncation = truncation === undefined ? '...' : truncation; + truncation = Object.isUndefined(truncation) ? '...' : truncation; return this.length > length ? - this.slice(0, length - truncation.length) + truncation : this; + this.slice(0, length - truncation.length) + truncation : String(this); }, strip: function() { @@ -213,35 +394,34 @@ Object.extend(String.prototype, { }, escapeHTML: function() { - var div = document.createElement('div'); - var text = document.createTextNode(this); - div.appendChild(text); - return div.innerHTML; + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; }, unescapeHTML: function() { - var div = document.createElement('div'); + var div = new Element('div'); div.innerHTML = this.stripTags(); return div.childNodes[0] ? (div.childNodes.length > 1 ? - $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) : + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : div.childNodes[0].nodeValue) : ''; }, toQueryParams: function(separator) { var match = this.strip().match(/([^?#]*)(#.*)?$/); - if (!match) return {}; + if (!match) return { }; - return match[1].split(separator || '&').inject({}, function(hash, pair) { + return match[1].split(separator || '&').inject({ }, function(hash, pair) { if ((pair = pair.split('='))[0]) { - var name = decodeURIComponent(pair[0]); - var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); - if (hash[name] !== undefined) { - if (hash[name].constructor != Array) - hash[name] = [hash[name]]; - if (value) hash[name].push(value); + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); } - else hash[name] = value; + else hash[key] = value; } return hash; }); @@ -256,6 +436,10 @@ Object.extend(String.prototype, { String.fromCharCode(this.charCodeAt(this.length - 1) + 1); }, + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + camelize: function() { var parts = this.split('-'), len = parts.length; if (len == 1) return parts[0]; @@ -270,7 +454,7 @@ Object.extend(String.prototype, { return camelized; }, - capitalize: function(){ + capitalize: function() { return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); }, @@ -283,52 +467,131 @@ Object.extend(String.prototype, { }, inspect: function(useDoubleQuotes) { - var escapedString = this.replace(/\\/g, '\\\\'); - if (useDoubleQuotes) - return '"' + escapedString.replace(/"/g, '\\"') + '"'; - else - return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this; + if (str.blank()) return false; + str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + }, + unescapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } }); String.prototype.gsub.prepareReplacement = function(replacement) { - if (typeof replacement == 'function') return replacement; + if (Object.isFunction(replacement)) return replacement; var template = new Template(replacement); return function(match) { return template.evaluate(match) }; -} +}; String.prototype.parseQuery = String.prototype.toQueryParams; -var Template = Class.create(); -Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; -Template.prototype = { +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create({ initialize: function(template, pattern) { this.template = template.toString(); - this.pattern = pattern || Template.Pattern; + this.pattern = pattern || Template.Pattern; }, evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + return this.template.gsub(this.pattern, function(match) { - var before = match[1]; + if (object == null) return ''; + + var before = match[1] || ''; if (before == '\\') return match[2]; - return before + String.interpret(object[match[3]]); - }); + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }.bind(this)); } -} +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; -var $break = new Object(); -var $continue = new Object(); +var $break = { }; var Enumerable = { - each: function(iterator) { + each: function(iterator, context) { var index = 0; + iterator = iterator.bind(context); try { this._each(function(value) { - try { - iterator(value, index++); - } catch (e) { - if (e != $continue) throw e; - } + iterator(value, index++); }); } catch (e) { if (e != $break) throw e; @@ -336,40 +599,45 @@ var Enumerable = { return this; }, - eachSlice: function(number, iterator) { + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var index = -number, slices = [], array = this.toArray(); while ((index += number) < array.length) slices.push(array.slice(index, index+number)); - return slices.map(iterator); + return slices.collect(iterator, context); }, - all: function(iterator) { + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result = true; this.each(function(value, index) { - result = result && !!(iterator || Prototype.K)(value, index); + result = result && !!iterator(value, index); if (!result) throw $break; }); return result; }, - any: function(iterator) { + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result = false; this.each(function(value, index) { - if (result = !!(iterator || Prototype.K)(value, index)) + if (result = !!iterator(value, index)) throw $break; }); return result; }, - collect: function(iterator) { + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var results = []; this.each(function(value, index) { - results.push((iterator || Prototype.K)(value, index)); + results.push(iterator(value, index)); }); return results; }, - detect: function(iterator) { + detect: function(iterator, context) { + iterator = iterator.bind(context); var result; this.each(function(value, index) { if (iterator(value, index)) { @@ -380,7 +648,8 @@ var Enumerable = { return result; }, - findAll: function(iterator) { + findAll: function(iterator, context) { + iterator = iterator.bind(context); var results = []; this.each(function(value, index) { if (iterator(value, index)) @@ -389,17 +658,24 @@ var Enumerable = { return results; }, - grep: function(pattern, iterator) { + grep: function(filter, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + this.each(function(value, index) { - var stringValue = value.toString(); - if (stringValue.match(pattern)) - results.push((iterator || Prototype.K)(value, index)); - }) + if (filter.match(value)) + results.push(iterator(value, index)); + }); return results; }, include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + var found = false; this.each(function(value) { if (value == object) { @@ -411,14 +687,15 @@ var Enumerable = { }, inGroupsOf: function(number, fillWith) { - fillWith = fillWith === undefined ? null : fillWith; + fillWith = Object.isUndefined(fillWith) ? null : fillWith; return this.eachSlice(number, function(slice) { while(slice.length < number) slice.push(fillWith); return slice; }); }, - inject: function(memo, iterator) { + inject: function(memo, iterator, context) { + iterator = iterator.bind(context); this.each(function(value, index) { memo = iterator(memo, value, index); }); @@ -432,30 +709,33 @@ var Enumerable = { }); }, - max: function(iterator) { + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result; this.each(function(value, index) { - value = (iterator || Prototype.K)(value, index); - if (result == undefined || value >= result) + value = iterator(value, index); + if (result == null || value >= result) result = value; }); return result; }, - min: function(iterator) { + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var result; this.each(function(value, index) { - value = (iterator || Prototype.K)(value, index); - if (result == undefined || value < result) + value = iterator(value, index); + if (result == null || value < result) result = value; }); return result; }, - partition: function(iterator) { + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; var trues = [], falses = []; this.each(function(value, index) { - ((iterator || Prototype.K)(value, index) ? + (iterator(value, index) ? trues : falses).push(value); }); return [trues, falses]; @@ -463,13 +743,14 @@ var Enumerable = { pluck: function(property) { var results = []; - this.each(function(value, index) { + this.each(function(value) { results.push(value[property]); }); return results; }, - reject: function(iterator) { + reject: function(iterator, context) { + iterator = iterator.bind(context); var results = []; this.each(function(value, index) { if (!iterator(value, index)) @@ -478,7 +759,8 @@ var Enumerable = { return results; }, - sortBy: function(iterator) { + sortBy: function(iterator, context) { + iterator = iterator.bind(context); return this.map(function(value, index) { return {value: value, criteria: iterator(value, index)}; }).sort(function(left, right) { @@ -493,7 +775,7 @@ var Enumerable = { zip: function() { var iterator = Prototype.K, args = $A(arguments); - if (typeof args.last() == 'function') + if (Object.isFunction(args.last())) iterator = args.pop(); var collections = [this].concat(args).map($A); @@ -509,31 +791,42 @@ var Enumerable = { inspect: function() { return '#<Enumerable:' + this.toArray().inspect() + '>'; } -} +}; Object.extend(Enumerable, { map: Enumerable.collect, find: Enumerable.detect, select: Enumerable.findAll, + filter: Enumerable.findAll, member: Enumerable.include, - entries: Enumerable.toArray + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any }); -var $A = Array.from = function(iterable) { +function $A(iterable) { if (!iterable) return []; - if (iterable.toArray) { - return iterable.toArray(); - } else { - var results = []; - for (var i = 0, length = iterable.length; i < length; i++) - results.push(iterable[i]); + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + function $A(iterable) { + if (!iterable) return []; + if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && + iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; return results; } } +Array.from = $A; + Object.extend(Array.prototype, Enumerable); -if (!Array.prototype._reverse) - Array.prototype._reverse = Array.prototype.reverse; +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; Object.extend(Array.prototype, { _each: function(iterator) { @@ -562,7 +855,7 @@ Object.extend(Array.prototype, { flatten: function() { return this.inject([], function(array, value) { - return array.concat(value && value.constructor == Array ? + return array.concat(Object.isArray(value) ? value.flatten() : [value]); }); }, @@ -574,12 +867,6 @@ Object.extend(Array.prototype, { }); }, - indexOf: function(object) { - for (var i = 0, length = this.length; i < length; i++) - if (this[i] == object) return i; - return -1; - }, - reverse: function(inline) { return (inline !== false ? this : this.toArray())._reverse(); }, @@ -588,9 +875,17 @@ Object.extend(Array.prototype, { return this.length > 1 ? this : this[0]; }, - uniq: function() { - return this.inject([], function(array, value) { - return array.include(value) ? array : array.concat([value]); + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); }); }, @@ -604,125 +899,187 @@ Object.extend(Array.prototype, { inspect: function() { return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (!Object.isUndefined(value)) results.push(value); + }); + return '[' + results.join(', ') + ']'; } }); +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + Array.prototype.toArray = Array.prototype.clone; -function $w(string){ +function $w(string) { + if (!Object.isString(string)) return []; string = string.strip(); return string ? string.split(/\s+/) : []; } -if(window.opera){ - Array.prototype.concat = function(){ +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { var array = []; - for(var i = 0, length = this.length; i < length; i++) array.push(this[i]); - for(var i = 0, length = arguments.length; i < length; i++) { - if(arguments[i].constructor == Array) { - for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) array.push(arguments[i][j]); } else { array.push(arguments[i]); } } return array; - } + }; } -var Hash = function(obj) { - Object.extend(this, obj || {}); -}; - -Object.extend(Hash, { - toQueryString: function(obj) { - var parts = []; - - this.prototype._each.call(obj, function(pair) { - if (!pair.key) return; - - if (pair.value && pair.value.constructor == Array) { - var values = pair.value.compact(); - if (values.length < 2) pair.value = values.reduce(); - else { - key = encodeURIComponent(pair.key); - values.each(function(value) { - value = value != undefined ? encodeURIComponent(value) : ''; - parts.push(key + '=' + encodeURIComponent(value)); - }); - return; - } - } - if (pair.value == undefined) pair[1] = ''; - parts.push(pair.map(encodeURIComponent).join('=')); - }); - - return parts.join('&'); - } -}); - -Object.extend(Hash.prototype, Enumerable); -Object.extend(Hash.prototype, { - _each: function(iterator) { - for (var key in this) { - var value = this[key]; - if (value && value == Hash.prototype[key]) continue; - - var pair = [key, value]; - pair.key = key; - pair.value = value; - iterator(pair); - } - }, - - keys: function() { - return this.pluck('key'); - }, - - values: function() { - return this.pluck('value'); +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); }, - merge: function(hash) { - return $H(hash).inject(this, function(mergedHash, pair) { - mergedHash[pair.key] = pair.value; - return mergedHash; - }); + succ: function() { + return this + 1; }, - remove: function() { - var result; - for(var i = 0, length = arguments.length; i < length; i++) { - var value = this[arguments[i]]; - if (value !== undefined){ - if (result === undefined) result = value; - else { - if (result.constructor != Array) result = [result]; - result.push(value) - } - } - delete this[arguments[i]]; - } - return result; + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; }, - toQueryString: function() { - return Hash.toQueryString(this); + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; }, - inspect: function() { - return '#<Hash:{' + this.map(function(pair) { - return pair.map(Object.inspect).join(': '); - }).join(', ') + '}>'; + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; } }); +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); function $H(object) { - if (object && object.constructor == Hash) return object; return new Hash(object); }; -ObjectRange = Class.create(); -Object.extend(ObjectRange.prototype, Enumerable); -Object.extend(ObjectRange.prototype, { + +var Hash = Class.create(Enumerable, (function() { + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: function(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { initialize: function(start, end, exclusive) { this.start = start; this.end = end; @@ -748,7 +1105,7 @@ Object.extend(ObjectRange.prototype, { var $R = function(start, end, exclusive) { return new ObjectRange(start, end, exclusive); -} +}; var Ajax = { getTransport: function() { @@ -760,7 +1117,7 @@ var Ajax = { }, activeRequestCount: 0 -} +}; Ajax.Responders = { responders: [], @@ -780,10 +1137,10 @@ Ajax.Responders = { dispatch: function(callback, request, transport, json) { this.each(function(responder) { - if (typeof responder[callback] == 'function') { + if (Object.isFunction(responder[callback])) { try { responder[callback].apply(responder, [request, transport, json]); - } catch (e) {} + } catch (e) { } } }); } @@ -792,49 +1149,45 @@ Ajax.Responders = { Object.extend(Ajax.Responders, Enumerable); Ajax.Responders.register({ - onCreate: function() { - Ajax.activeRequestCount++; - }, - onComplete: function() { - Ajax.activeRequestCount--; - } + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } }); -Ajax.Base = function() {}; -Ajax.Base.prototype = { - setOptions: function(options) { +Ajax.Base = Class.create({ + initialize: function(options) { this.options = { method: 'post', asynchronous: true, contentType: 'application/x-www-form-urlencoded', encoding: 'UTF-8', - parameters: '' - } - Object.extend(this.options, options || {}); + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); this.options.method = this.options.method.toLowerCase(); - if (typeof this.options.parameters == 'string') + + if (Object.isString(this.options.parameters)) this.options.parameters = this.options.parameters.toQueryParams(); + else if (Object.isHash(this.options.parameters)) + this.options.parameters = this.options.parameters.toObject(); } -} - -Ajax.Request = Class.create(); -Ajax.Request.Events = - ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; +}); -Ajax.Request.prototype = Object.extend(new Ajax.Base(), { +Ajax.Request = Class.create(Ajax.Base, { _complete: false, - initialize: function(url, options) { + initialize: function($super, url, options) { + $super(options); this.transport = Ajax.getTransport(); - this.setOptions(options); this.request(url); }, request: function(url) { this.url = url; this.method = this.options.method; - var params = this.options.parameters; + var params = Object.clone(this.options.parameters); if (!['get', 'post'].include(this.method)) { // simulate other verbs over post @@ -842,28 +1195,31 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { this.method = 'post'; } - params = Hash.toQueryString(params); - if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_=' + this.parameters = params; - // when GET, append parameters to URL - if (this.method == 'get' && params) - this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params; + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } try { - Ajax.Responders.dispatch('onCreate', this, this.transport); + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); - if (this.options.asynchronous) - setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); this.transport.onreadystatechange = this.onStateChange.bind(this); this.setRequestHeaders(); - var body = this.method == 'post' ? (this.options.postBody || params) : null; - - this.transport.send(body); + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); /* Force Firefox to handle ready state 4 for synchronous requests */ if (!this.options.asynchronous && this.transport.overrideMimeType) @@ -905,7 +1261,7 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { if (typeof this.options.requestHeaders == 'object') { var extras = this.options.requestHeaders; - if (typeof extras.push == 'function') + if (Object.isFunction(extras.push)) for (var i = 0, length = extras.length; i < length; i += 2) headers[extras[i]] = extras[i+1]; else @@ -917,32 +1273,39 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { }, success: function() { - return !this.transport.status - || (this.transport.status >= 200 && this.transport.status < 300); + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } }, respondToReadyState: function(readyState) { - var state = Ajax.Request.Events[readyState]; - var transport = this.transport, json = this.evalJSON(); + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); if (state == 'Complete') { try { this._complete = true; - (this.options['on' + this.transport.status] + (this.options['on' + response.status] || this.options['on' + (this.success() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(transport, json); + || Prototype.emptyFunction)(response, response.headerJSON); } catch (e) { this.dispatchException(e); } - if ((this.getHeader('Content-type') || 'text/javascript').strip(). - match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) - this.evalResponse(); + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); } try { - (this.options['on' + state] || Prototype.emptyFunction)(transport, json); - Ajax.Responders.dispatch('on' + state, this, transport, json); + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); } catch (e) { this.dispatchException(e); } @@ -959,16 +1322,9 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { } catch (e) { return null } }, - evalJSON: function() { - try { - var json = this.getHeader('X-JSON'); - return json ? eval('(' + json + ')') : null; - } catch (e) { return null } - }, - evalResponse: function() { try { - return eval(this.transport.responseText); + return eval((this.transport.responseText || '').unfilterJSON()); } catch (e) { this.dispatchException(e); } @@ -980,57 +1336,126 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { } }); -Ajax.Updater = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = Object.isUndefined(xml) ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, -Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { - initialize: function(container, url, options) { + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json')) || + this.responseText.blank()) + return null; + try { + return this.responseText.evalJSON(options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { this.container = { success: (container.success || container), failure: (container.failure || (container.success ? null : container)) - } - - this.transport = Ajax.getTransport(); - this.setOptions(options); + }; - var onComplete = this.options.onComplete || Prototype.emptyFunction; - this.options.onComplete = (function(transport, param) { - this.updateContent(); - onComplete(transport, param); + options = Object.clone(options); + var onComplete = options.onComplete; + options.onComplete = (function(response, json) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, json); }).bind(this); - this.request(url); + $super(url, options); }, - updateContent: function() { - var receiver = this.container[this.success() ? 'success' : 'failure']; - var response = this.transport.responseText; + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; - if (!this.options.evalScripts) response = response.stripScripts(); + if (!options.evalScripts) responseText = responseText.stripScripts(); if (receiver = $(receiver)) { - if (this.options.insertion) - new this.options.insertion(receiver, response); - else - receiver.update(response); - } - - if (this.success()) { - if (this.onComplete) - setTimeout(this.onComplete.bind(this), 10); + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); } } }); -Ajax.PeriodicalUpdater = Class.create(); -Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { - initialize: function(container, url, options) { - this.setOptions(options); +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); this.onComplete = this.options.onComplete; this.frequency = (this.options.frequency || 2); this.decay = (this.options.decay || 1); - this.updater = {}; + this.updater = { }; this.container = container; this.url = url; @@ -1048,15 +1473,14 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, - updateComplete: function(request) { + updateComplete: function(response) { if (this.options.decay) { - this.decay = (request.responseText == this.lastText ? + this.decay = (response.responseText == this.lastText ? this.decay * this.options.decay : 1); - this.lastText = request.responseText; + this.lastText = response.responseText; } - this.timer = setTimeout(this.onTimerEvent.bind(this), - this.decay * this.frequency * 1000); + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); }, onTimerEvent: function() { @@ -1069,7 +1493,7 @@ function $(element) { elements.push($(arguments[i])); return elements; } - if (typeof element == 'string') + if (Object.isString(element)) element = document.getElementById(element); return Element.extend(element); } @@ -1080,63 +1504,51 @@ if (Prototype.BrowserFeatures.XPath) { var query = document.evaluate(expression, $(parentElement) || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (var i = 0, length = query.snapshotLength; i < length; i++) - results.push(query.snapshotItem(i)); + results.push(Element.extend(query.snapshotItem(i))); return results; }; } -document.getElementsByClassName = function(className, parentElement) { - if (Prototype.BrowserFeatures.XPath) { - var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; - return document._getElementsByXPath(q, parentElement); - } else { - var children = ($(parentElement) || document.body).getElementsByTagName('*'); - var elements = [], child; - for (var i = 0, length = children.length; i < length; i++) { - child = children[i]; - if (Element.hasClassName(child, className)) - elements.push(Element.extend(child)); - } - return elements; - } -}; - /*--------------------------------------------------------------------------*/ -if (!window.Element) - var Element = new Object(); - -Element.extend = function(element) { - if (!element || _nativeExtensions || element.nodeType == 3) return element; - - if (!element._extended && element.tagName && element != window) { - var methods = Object.clone(Element.Methods), cache = Element.extend.cache; - - if (element.tagName == 'FORM') - Object.extend(methods, Form.Methods); - if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName)) - Object.extend(methods, Form.Element.Methods); - - Object.extend(methods, Element.Methods.Simulated); +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} - for (var property in methods) { - var value = methods[property]; - if (typeof value == 'function' && !(property in element)) - element[property] = cache.findOrStore(value); +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); } - } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); +}).call(window); - element._extended = true; - return element; -}; - -Element.extend.cache = { - findOrStore: function(value) { - return this[value] = this[value] || function() { - return value.apply(null, [this].concat($A(arguments))); - } - } -}; +Element.cache = { }; Element.Methods = { visible: function(element) { @@ -1165,28 +1577,74 @@ Element.Methods = { return element; }, - update: function(element, html) { - html = typeof html == 'undefined' ? '' : html.toString(); - $(element).innerHTML = html.stripScripts(); - setTimeout(function() {html.evalScripts()}, 10); + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); return element; }, - replace: function(element, html) { + replace: function(element, content) { element = $(element); - html = typeof html == 'undefined' ? '' : html.toString(); - if (element.outerHTML) { - element.outerHTML = html.stripScripts(); - } else { + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); var range = element.ownerDocument.createRange(); - range.selectNodeContents(element); - element.parentNode.replaceChild( - range.createContextualFragment(html.stripScripts()), element); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, t, range; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + t = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + t.insert(element, content); + continue; + } + + content = Object.toHTML(content); + + range = element.ownerDocument.createRange(); + t.initializeRange(element, range); + t.insert(element, range.createContextualFragment(content.stripScripts())); + + content.evalScripts.bind(content).defer(); } - setTimeout(function() {html.evalScripts()}, 10); + return element; }, + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + inspect: function(element) { element = $(element); var result = '<' + element.tagName.toLowerCase(); @@ -1212,7 +1670,13 @@ Element.Methods = { }, descendants: function(element) { - return $A($(element).getElementsByTagName('*')); + return $(element).getElementsBySelector("*"); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); }, immediateDescendants: function(element) { @@ -1236,48 +1700,96 @@ Element.Methods = { }, match: function(element, selector) { - if (typeof selector == 'string') + if (Object.isString(selector)) selector = new Selector(selector); return selector.match($(element)); }, up: function(element, expression, index) { - return Selector.findElement($(element).ancestors(), expression, index); + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return expression ? Selector.findElement(ancestors, expression, index) : + ancestors[index || 0]; }, down: function(element, expression, index) { - return Selector.findElement($(element).descendants(), expression, index); + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + var descendants = element.descendants(); + return expression ? Selector.findElement(descendants, expression, index) : + descendants[index || 0]; }, previous: function(element, expression, index) { - return Selector.findElement($(element).previousSiblings(), expression, index); + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return expression ? Selector.findElement(previousSiblings, expression, index) : + previousSiblings[index || 0]; }, next: function(element, expression, index) { - return Selector.findElement($(element).nextSiblings(), expression, index); + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return expression ? Selector.findElement(nextSiblings, expression, index) : + nextSiblings[index || 0]; }, - getElementsBySelector: function() { + select: function() { var args = $A(arguments), element = $(args.shift()); return Selector.findChildElements(element, args); }, - getElementsByClassName: function(element, className) { - return document.getElementsByClassName(className, element); + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; }, readAttribute: function(element, name) { element = $(element); - if (document.all && !window.opera) { - var t = Element._attributeTranslations; + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; if (t.values[name]) return t.values[name](element, name); - if (t.names[name]) name = t.names[name]; - var attribute = element.attributes[name]; - if(attribute) return attribute.nodeValue; + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } } return element.getAttribute(name); }, + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = Object.isUndefined(value) ? true : value; + + for (var attr in attributes) { + name = t.names[attr] || attr; + value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + getHeight: function(element) { return $(element).getDimensions().height; }, @@ -1293,39 +1805,28 @@ Element.Methods = { hasClassName: function(element, className) { if (!(element = $(element))) return; var elementClassName = element.className; - if (elementClassName.length == 0) return false; - if (elementClassName == className || - elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) - return true; - return false; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }, addClassName: function(element, className) { if (!(element = $(element))) return; - Element.classNames(element).add(className); + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; - Element.classNames(element).remove(className); + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); return element; }, toggleClassName: function(element, className) { if (!(element = $(element))) return; - Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className); - return element; - }, - - observe: function() { - Event.observe.apply(Event, arguments); - return $A(arguments).first(); - }, - - stopObserving: function() { - Event.stopObserving.apply(Event, arguments); - return $A(arguments).first(); + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); }, // removes whitespace-only text node children @@ -1342,74 +1843,76 @@ Element.Methods = { }, empty: function(element) { - return $(element).innerHTML.match(/^\s*$/); + return $(element).innerHTML.blank(); }, descendantOf: function(element, ancestor) { element = $(element), ancestor = $(ancestor); + var originalAncestor = ancestor; + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (element.sourceIndex && !Prototype.Browser.Opera) { + var e = element.sourceIndex, a = ancestor.sourceIndex, + nextAncestor = ancestor.nextSibling; + if (!nextAncestor) { + do { ancestor = ancestor.parentNode; } + while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); + } + if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + } + while (element = element.parentNode) - if (element == ancestor) return true; + if (element == originalAncestor) return true; return false; }, scrollTo: function(element) { element = $(element); - var pos = Position.cumulativeOffset(element); + var pos = element.cumulativeOffset(); window.scrollTo(pos[0], pos[1]); return element; }, getStyle: function(element, style) { element = $(element); - if (['float','cssFloat'].include(style)) - style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat'); - style = style.camelize(); + style = style == 'float' ? 'cssFloat' : style.camelize(); var value = element.style[style]; if (!value) { - if (document.defaultView && document.defaultView.getComputedStyle) { - var css = document.defaultView.getComputedStyle(element, null); - value = css ? css[style] : null; - } else if (element.currentStyle) { - value = element.currentStyle[style]; - } + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, - if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none')) - value = element['offset'+style.capitalize()] + 'px'; + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, - if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) - if (Element.getStyle(element, 'position') == 'static') value = 'auto'; - if(style == 'opacity') { - if(value) return parseFloat(value); - if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) - if(value[1]) return parseFloat(value[1]) / 100; - return 1.0; + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; } - return value == 'auto' ? null : value; + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; }, - setStyle: function(element, style) { + setOpacity: function(element, value) { element = $(element); - for (var name in style) { - var value = style[name]; - if(name == 'opacity') { - if (value == 1) { - value = (/Gecko/.test(navigator.userAgent) && - !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0; - if(/MSIE/.test(navigator.userAgent) && !window.opera) - element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); - } else if(value == '') { - if(/MSIE/.test(navigator.userAgent) && !window.opera) - element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); - } else { - if(value < 0.00001) value = 0; - if(/MSIE/.test(navigator.userAgent) && !window.opera) - element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + - 'alpha(opacity='+value*100+')'; - } - } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat'; - element.style[name.camelize()] = value; - } + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; return element; }, @@ -1468,8 +1971,8 @@ Element.Methods = { makeClipping: function(element) { element = $(element); if (element._overflow) return element; - element._overflow = element.style.overflow || 'auto'; - if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') element.style.overflow = 'hidden'; return element; }, @@ -1480,393 +1983,1398 @@ Element.Methods = { element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; element._overflow = null; return element; - } -}; + }, -Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf}); - -Element._attributeTranslations = {}; - -Element._attributeTranslations.names = { - colspan: "colSpan", - rowspan: "rowSpan", - valign: "vAlign", - datetime: "dateTime", - accesskey: "accessKey", - tabindex: "tabIndex", - enctype: "encType", - maxlength: "maxLength", - readonly: "readOnly", - longdesc: "longDesc" -}; + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. -Element._attributeTranslations.values = { - _getAttr: function(element, attribute) { - return element.getAttribute(attribute, 2); + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); }, - _flag: function(element, attribute) { - return $(element).hasAttribute(attribute) ? attribute : null; + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); }, - style: function(element) { - return element.style.cssText.toLowerCase(); + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || element.tagName == 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); }, - title: function(element) { - var node = element.getAttributeNode('title'); - return node.specified ? node.nodeValue : null; + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; } }; -Object.extend(Element._attributeTranslations.values, { - href: Element._attributeTranslations.values._getAttr, - src: Element._attributeTranslations.values._getAttr, - disabled: Element._attributeTranslations.values._flag, - checked: Element._attributeTranslations.values._flag, - readonly: Element._attributeTranslations.values._flag, - multiple: Element._attributeTranslations.values._flag +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants }); -Element.Methods.Simulated = { - hasAttribute: function(element, attribute) { - var t = Element._attributeTranslations; - attribute = t.names[attribute] || attribute; - return $(element).getAttributeNode(attribute).specified; +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } } }; -// IE is missing .innerHTML support for TABLE-related elements -if (document.all && !window.opera){ - Element.Methods.update = function(element, html) { + +if (!document.createRange || Prototype.Browser.Opera) { + Element.Methods.insert = function(element, insertions) { element = $(element); - html = typeof html == 'undefined' ? '' : html.toString(); - var tagName = element.tagName.toUpperCase(); - if (['THEAD','TBODY','TR','TD'].include(tagName)) { - var div = document.createElement('div'); - switch (tagName) { - case 'THEAD': - case 'TBODY': - div.innerHTML = '<table><tbody>' + html.stripScripts() + '</tbody></table>'; - depth = 2; - break; - case 'TR': - div.innerHTML = '<table><tbody><tr>' + html.stripScripts() + '</tr></tbody></table>'; - depth = 3; - break; - case 'TD': - div.innerHTML = '<table><tbody><tr><td>' + html.stripScripts() + '</td></tr></tbody></table>'; - depth = 4; + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = { bottom: insertions }; + + var t = Element._insertionTranslations, content, position, pos, tagName; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + pos = t[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + pos.insert(element, content); + continue; } - $A(element.childNodes).each(function(node){ - element.removeChild(node) - }); - depth.times(function(){ div = div.firstChild }); - $A(div.childNodes).each( - function(node){ element.appendChild(node) }); - } else { - element.innerHTML = html.stripScripts(); + content = Object.toHTML(content); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + if (t.tags[tagName]) { + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + if (position == 'top' || position == 'after') fragments.reverse(); + fragments.each(pos.insert.curry(element)); + } + else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); + + content.evalScripts.bind(content).defer(); } - setTimeout(function() {html.evalScripts()}, 10); + return element; - } -}; + }; +} -Object.extend(Element, Element.Methods); +if (Prototype.Browser.Opera) { + Element.Methods.getStyle = Element.Methods.getStyle.wrap( + function(proceed, element, style) { + switch (style) { + case 'left': case 'top': case 'right': case 'bottom': + if (proceed(element, 'position') === 'static') return null; + case 'height': case 'width': + // returns '0px' for hidden elements; we want it to return null + if (!Element.visible(element)) return null; + + // returns the border-box dimensions rather than the content-box + // dimensions, so we subtract padding and borders from the value + var dim = parseInt(proceed(element, style), 10); + + if (dim !== element['offset' + style.capitalize()]) + return dim + 'px'; + + var properties; + if (style === 'height') { + properties = ['border-top-width', 'padding-top', + 'padding-bottom', 'border-bottom-width']; + } + else { + properties = ['border-left-width', 'padding-left', + 'padding-right', 'border-right-width']; + } + return properties.inject(dim, function(memo, property) { + var val = proceed(element, property); + return val === null ? memo : memo - parseInt(val, 10); + }) + 'px'; + default: return proceed(element, style); + } + } + ); -var _nativeExtensions = false; + Element.Methods.readAttribute = Element.Methods.readAttribute.wrap( + function(proceed, element, attribute) { + if (attribute === 'title') return element.title; + return proceed(element, attribute); + } + ); +} -if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { - var className = 'HTML' + tag + 'Element'; - if(window[className]) return; - var klass = window[className] = {}; - klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; +else if (Prototype.Browser.IE) { + $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position != 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); }); -Element.addMethods = function(methods) { - Object.extend(Element.Methods, methods || {}); + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; - function copy(methods, destination, onlyIfAbsent) { - onlyIfAbsent = onlyIfAbsent || false; - var cache = Element.extend.cache; - for (var property in methods) { - var value = methods[property]; - if (!onlyIfAbsent || !(property in destination)) - destination[property] = cache.findOrStore(value); + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; } - } - if (typeof HTMLElement != 'undefined') { - copy(Element.Methods, HTMLElement.prototype); - copy(Element.Methods.Simulated, HTMLElement.prototype, true); - copy(Form.Methods, HTMLFormElement.prototype); - [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) { - copy(Form.Element.Methods, klass.prototype); + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.clone(Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv }); - _nativeExtensions = true; - } + })(Element._attributeTranslations.read.values); } -var Toggle = new Object(); -Toggle.display = Element.toggle; +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} -/*--------------------------------------------------------------------------*/ +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Element#cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); -Abstract.Insertion = function(adjacency) { - this.adjacency = adjacency; + return Element._returnOffset(valueL, valueT); + }; } -Abstract.Insertion.prototype = { - initialize: function(element, content) { - this.element = $(element); - this.content = content.stripScripts(); +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); - if (this.adjacency && this.element.insertAdjacentHTML) { - try { - this.element.insertAdjacentHTML(this.adjacency, this.content); - } catch (e) { - var tagName = this.element.tagName.toUpperCase(); - if (['TBODY', 'TR'].include(tagName)) { - this.insertContent(this.contentFromAnonymousTable()._reverse()); - } else { - throw e; - } - } - } else { - this.range = this.element.ownerDocument.createRange(); - if (this.initializeRange) this.initializeRange(); - this.insertContent([this.range.createContextualFragment(this.content)]); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if (document.createElement('div').outerHTML) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; } - setTimeout(function() {content.evalScripts()}, 10); + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: { + adjacency: 'beforeBegin', + insert: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + initializeRange: function(element, range) { + range.setStartBefore(element); + } }, + top: { + adjacency: 'afterBegin', + insert: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + initializeRange: function(element, range) { + range.selectNodeContents(element); + range.collapse(true); + } + }, + bottom: { + adjacency: 'beforeEnd', + insert: function(element, node) { + element.appendChild(node); + } + }, + after: { + adjacency: 'afterEnd', + insert: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + initializeRange: function(element, range) { + range.setStartAfter(element); + } + }, + tags: { + TABLE: ['<table>', '</table>', 1], + TBODY: ['<table><tbody>', '</tbody></table>', 2], + TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3], + TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4], + SELECT: ['<select>', '</select>', 1] + } +}; - contentFromAnonymousTable: function() { - var div = document.createElement('div'); - div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>'; - return $A(div.childNodes[0].childNodes[0].childNodes); +(function() { + this.bottom.initializeRange = this.top.initializeRange; + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return node && node.specified; } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; } -var Insertion = new Object(); +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; -Insertion.Before = Class.create(); -Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { - initializeRange: function() { - this.range.setStartBefore(this.element); - }, + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName, property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; - insertContent: function(fragments) { - fragments.each((function(fragment) { - this.element.parentNode.insertBefore(fragment, this.element); - }).bind(this)); + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); } -}); -Insertion.Top = Class.create(); -Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { - initializeRange: function() { - this.range.selectNodeContents(this.element); - this.range.collapse(true); - }, + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } - insertContent: function(fragments) { - fragments.reverse(false).each((function(fragment) { - this.element.insertBefore(fragment, this.element.firstChild); - }).bind(this)); + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); } -}); -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); - }, + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } - insertContent: function(fragments) { - fragments.each((function(fragment) { - this.element.appendChild(fragment); - }).bind(this)); + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } } -}); -Insertion.After = Class.create(); -Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { - initializeRange: function() { - this.range.setStartAfter(this.element); - }, + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } - insertContent: function(fragments) { - fragments.each((function(fragment) { - this.element.parentNode.insertBefore(fragment, - this.element.nextSibling); - }).bind(this)); + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); } -}); -/*--------------------------------------------------------------------------*/ + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } -Element.ClassNames = Class.create(); -Element.ClassNames.prototype = { - initialize: function(element) { - this.element = $(element); - }, + Object.extend(Element, Element.Methods); + delete Element.ByTag; - _each: function(iterator) { - this.element.className.split(/\s+/).select(function(name) { - return name.length > 0; - })._each(iterator); - }, + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; - set: function(className) { - this.element.className = className; +document.viewport = { + getDimensions: function() { + var dimensions = { }; + var B = Prototype.Browser; + $w('width height').each(function(d) { + var D = d.capitalize(); + dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] : + (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D]; + }); + return dimensions; }, - add: function(classNameToAdd) { - if (this.include(classNameToAdd)) return; - this.set($A(this).concat(classNameToAdd).join(' ')); + getWidth: function() { + return this.getDimensions().width; }, - remove: function(classNameToRemove) { - if (!this.include(classNameToRemove)) return; - this.set($A(this).without(classNameToRemove).join(' ')); + getHeight: function() { + return this.getDimensions().height; }, - toString: function() { - return $A(this).join(' '); + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); } }; +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ -Object.extend(Element.ClassNames.prototype, Enumerable); -var Selector = Class.create(); -Selector.prototype = { +var Selector = Class.create({ initialize: function(expression) { - this.params = {classNames: []}; - this.expression = expression.toString().strip(); - this.parseExpression(); + this.expression = expression.strip(); this.compileMatcher(); }, - parseExpression: function() { - function abort(message) { throw 'Parse error in selector: ' + message; } + shouldUseXPath: function() { + if (!Prototype.BrowserFeatures.XPath) return false; - if (this.expression == '') abort('empty expression'); + var e = this.expression; - var params = this.params, expr = this.expression, match, modifier, clause, rest; - while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { - params.attributes = params.attributes || []; - params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); - expr = match[1]; - } + // Safari 3 chokes on :*-of-type and :empty + if (Prototype.Browser.WebKit && + (e.include("-of-type") || e.include(":empty"))) + return false; + + // XPath can't do namespaced attributes, nor can it read + // the "checked" property from DOM nodes + if ((/(\[[\w-]*?:|:checked)/).test(this.expression)) + return false; - if (expr == '*') return this.params.wildcard = true; + return true; + }, + + compileMatcher: function() { + if (this.shouldUseXPath()) + return this.compileXPathMatcher(); - while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { - modifier = match[1], clause = match[2], rest = match[3]; - switch (modifier) { - case '#': params.id = clause; break; - case '.': params.classNames.push(clause); break; - case '': - case undefined: params.tagName = clause.toUpperCase(); break; - default: abort(expr.inspect()); + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } } - expr = rest; } - if (expr.length > 0) abort(expr.inspect()); + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; }, - buildMatchExpression: function() { - var params = this.params, conditions = [], clause; + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; - if (params.wildcard) - conditions.push('true'); - if (clause = params.id) - conditions.push('element.readAttribute("id") == ' + clause.inspect()); - if (clause = params.tagName) - conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); - if ((clause = params.classNames).length > 0) - for (var i = 0, length = clause.length; i < length; i++) - conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); - if (clause = params.attributes) { - clause.each(function(attribute) { - var value = 'element.readAttribute(' + attribute.name.inspect() + ')'; - var splitValueBy = function(delimiter) { - return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; - } + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } - switch (attribute.operator) { - case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; - case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; - case '|=': conditions.push( - splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() - ); break; - case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; - case '': - case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break; - default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; } - }); + } } - return conditions.join(' && '); + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; }, - compileMatcher: function() { - this.match = new Function('element', 'if (!element.tagName) return false; \ - element = $(element); \ - return ' + this.buildMatchExpression()); + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); }, - findElements: function(scope) { - var element; + match: function(element) { + this.tokens = []; - if (element = $(this.params.id)) - if (this.match(element)) - if (!scope || Element.childOf(element, scope)) - return [element]; + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; - scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } - var results = []; - for (var i = 0, length = scope.length; i < length; i++) - if (this.match(element = scope[i])) - results.push(Element.extend(element)); + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } - return results; + return match; }, toString: function() { return this.expression; + }, + + inspect: function() { + return "#<Selector:" + this.expression.inspect() + ">"; } -} +}); Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: function(m) { + m[1] = m[1].toLowerCase(); + return new Template("[@#{1}]").evaluate(m); + }, + attr: function(m) { + m[1] = m[1].toLowerCase(); + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + attrPresence: /^\[([\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return Selector.operators[matches[2]](nodeValue, matches[3]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._counted = true; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._counted) { + n._counted = true; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + tagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() == tagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._counted) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._counted) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + matchElements: function(elements, expression) { - var selector = new Selector(expression); - return elements.select(selector.match.bind(selector)).map(Element.extend); + var matches = new Selector(expression).findElements(), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._counted) results.push(element); + h.unmark(matches); + return results; }, findElement: function(elements, expression, index) { - if (typeof expression == 'number') index = expression, expression = false; + if (Object.isNumber(expression)) { + index = expression; expression = false; + } return Selector.matchElements(elements, expression || '*')[index || 0]; }, findChildElements: function(element, expressions) { - return expressions.map(function(expression) { - return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) { - var selector = new Selector(expr); - return results.inject([], function(elements, result) { - return elements.concat(selector.findElements(result || element)); - }); - }); - }).flatten(); + var exprs = expressions.join(','); + expressions = []; + exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; } }); +if (Prototype.Browser.IE) { + // IE returns comment nodes on getElementsByTagName("*"). + // Filter them out. + Selector.handlers.concat = function(a, b) { + for (var i = 0, node; node = b[i]; i++) + if (node.tagName !== "!") a.push(node); + return a; + }; +} + function $$() { return Selector.findChildElements(document, $A(arguments)); } @@ -1876,13 +3384,19 @@ var Form = { return form; }, - serializeElements: function(elements, getHash) { - var data = elements.inject({}, function(result, element) { + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (Object.isUndefined(options.hash)) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { if (!element.disabled && element.name) { - var key = element.name, value = $(element).getValue(); - if (value != undefined) { - if (result[key]) { - if (result[key].constructor != Array) result[key] = [result[key]]; + key = element.name; value = $(element).getValue(); + if (value != null && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; result[key].push(value); } else result[key] = value; @@ -1891,13 +3405,13 @@ var Form = { return result; }); - return getHash ? data : Hash.toQueryString(data); + return options.hash ? data : Object.toQueryString(data); } }; Form.Methods = { - serialize: function(form, getHash) { - return Form.serializeElements(Form.getElements(form), getHash); + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); }, getElements: function(form) { @@ -1928,25 +3442,26 @@ Form.Methods = { disable: function(form) { form = $(form); - form.getElements().each(function(element) { - element.blur(); - element.disabled = 'true'; - }); + Form.getElements(form).invoke('disable'); return form; }, enable: function(form) { form = $(form); - form.getElements().each(function(element) { - element.disabled = ''; - }); + Form.getElements(form).invoke('enable'); return form; }, findFirstElement: function(form) { - return $(form).getElements().find(function(element) { - return element.type != 'hidden' && !element.disabled && - ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); }); }, @@ -1954,10 +3469,26 @@ Form.Methods = { form = $(form); form.findFirstElement().activate(); return form; - } -} + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); -Object.extend(Form, Form.Methods); + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; /*--------------------------------------------------------------------------*/ @@ -1971,7 +3502,7 @@ Form.Element = { $(element).select(); return element; } -} +}; Form.Element.Methods = { serialize: function(element) { @@ -1979,9 +3510,9 @@ Form.Element.Methods = { if (!element.disabled && element.name) { var value = element.getValue(); if (value != undefined) { - var pair = {}; + var pair = { }; pair[element.name] = value; - return Hash.toQueryString(pair); + return Object.toQueryString(pair); } } return ''; @@ -1993,6 +3524,13 @@ Form.Element.Methods = { return Form.Element.Serializers[method](element); }, + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + clear: function(element) { $(element).value = ''; return element; @@ -2004,55 +3542,75 @@ Form.Element.Methods = { activate: function(element) { element = $(element); - element.focus(); - if (element.select && ( element.tagName.toLowerCase() != 'input' || - !['button', 'reset', 'submit'].include(element.type) ) ) - element.select(); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } return element; }, disable: function(element) { element = $(element); + element.blur(); element.disabled = true; return element; }, enable: function(element) { element = $(element); - element.blur(); element.disabled = false; return element; } -} +}; + +/*--------------------------------------------------------------------------*/ -Object.extend(Form.Element, Form.Element.Methods); var Field = Form.Element; -var $F = Form.Element.getValue; +var $F = Form.Element.Methods.getValue; /*--------------------------------------------------------------------------*/ Form.Element.Serializers = { - input: function(element) { + input: function(element, value) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': - return Form.Element.Serializers.inputSelector(element); + return Form.Element.Serializers.inputSelector(element, value); default: - return Form.Element.Serializers.textarea(element); + return Form.Element.Serializers.textarea(element, value); } }, - inputSelector: function(element) { - return element.checked ? element.value : null; + inputSelector: function(element, value) { + if (Object.isUndefined(value)) return element.checked ? element.value : null; + else element.checked = !!value; }, - textarea: function(element) { - return element.value; + textarea: function(element, value) { + if (Object.isUndefined(value)) return element.value; + else element.value = value; }, - select: function(element) { - return this[element.type == 'select-one' ? - 'selectOne' : 'selectMany'](element); + select: function(element, index) { + if (Object.isUndefined(index)) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, value, single = !Object.isArray(index); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + value = this.optionValue(opt); + if (single) { + if (value == index) { + opt.selected = true; + return; + } + } + else opt.selected = index.include(value); + } + } }, selectOne: function(element) { @@ -2075,45 +3633,34 @@ Form.Element.Serializers = { // extend element because hasAttribute may not be native return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; } -} +}; /*--------------------------------------------------------------------------*/ -Abstract.TimedObserver = function() {} -Abstract.TimedObserver.prototype = { - initialize: function(element, frequency, callback) { - this.frequency = frequency; +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, 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() { + execute: function() { var value = this.getValue(); - var changed = ('string' == typeof this.lastValue && 'string' == typeof value - ? this.lastValue != value : String(this.lastValue) != String(value)); - if (changed) { + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { this.callback(this.element, value); this.lastValue = value; } } -} +}); -Form.Element.Observer = Class.create(); -Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { +Form.Element.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); -Form.Observer = Class.create(); -Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { +Form.Observer = Class.create(Abstract.TimedObserver, { getValue: function() { return Form.serialize(this.element); } @@ -2121,8 +3668,7 @@ Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { /*--------------------------------------------------------------------------*/ -Abstract.EventObserver = function() {} -Abstract.EventObserver.prototype = { +Abstract.EventObserver = Class.create({ initialize: function(element, callback) { this.element = $(element); this.callback = callback; @@ -2143,7 +3689,7 @@ Abstract.EventObserver.prototype = { }, registerFormCallbacks: function() { - Form.getElements(this.element).each(this.registerCallback.bind(this)); + Form.getElements(this.element).each(this.registerCallback, this); }, registerCallback: function(element) { @@ -2159,24 +3705,20 @@ Abstract.EventObserver.prototype = { } } } -} +}); -Form.Element.EventObserver = Class.create(); -Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.Element.getValue(this.element); } }); -Form.EventObserver = Class.create(); -Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { +Form.EventObserver = Class.create(Abstract.EventObserver, { getValue: function() { return Form.serialize(this.element); } }); -if (!window.Event) { - var Event = new Object(); -} +if (!window.Event) var Event = { }; Object.extend(Event, { KEY_BACKSPACE: 8, @@ -2192,102 +3734,337 @@ Object.extend(Event, { KEY_END: 35, KEY_PAGEUP: 33, KEY_PAGEDOWN: 34, + KEY_INSERT: 45, - element: function(event) { - return event.target || event.srcElement; - }, + cache: { }, - isLeftClick: function(event) { - return (((event.which) && (event.which == 1)) || - ((event.button) && (event.button == 1))); - }, + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); - pointerX: function(event) { - return event.pageX || (event.clientX + - (document.documentElement.scrollLeft || document.body.scrollLeft)); - }, +Event.Methods = (function() { + var isButton; - pointerY: function(event) { - return event.pageY || (event.clientY + - (document.documentElement.scrollTop || document.body.scrollTop)); - }, + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } - stop: function(event) { - if (event.preventDefault) { + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + var node = Event.extend(event).target; + return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + if (!expression) return element; + var elements = [element].concat(element.ancestors()); + return Selector.findElement(elements, expression, 0); + }, + + pointer: function(event) { + return { + x: event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); event.preventDefault(); event.stopPropagation(); - } else { - event.returnValue = false; - event.cancelBubble = true; + event.stopped = 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; - }, +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; - observers: false, + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._eventID) return element._eventID; + arguments.callee.id = arguments.callee.id || 1; + return element._eventID = ++arguments.callee.id; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event) + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } - _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); + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + if (document.createEvent) { + var event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + var event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return Event.extend(event); } - }, + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize() +}); - unloadCache: function() { - if (!Event.observers) return; - for (var i = 0, length = Event.observers.length; i < length; i++) { - Event.stopObserving.apply(this, Event.observers[i]); - Event.observers[i][0] = null; +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer, fired = false; + + function fireContentLoadedEvent() { + if (fired) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + fired = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); } - Event.observers = false; - }, - observe: function(element, name, observer, useCapture) { - element = $(element); - useCapture = useCapture || false; + } else { + document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>"); + $("__onDOMContentLoaded").onreadystatechange = function() { + if (this.readyState == "complete") { + this.onreadystatechange = null; + fireContentLoadedEvent(); + } + }; + } +})(); +/*------------------------------- DEPRECATED -------------------------------*/ - if (name == 'keypress' && - (navigator.appVersion.match(/Konqueror|Safari|KHTML/) - || element.attachEvent)) - name = 'keydown'; +Hash.toQueryString = Object.toQueryString; - Event._observeAndCache(element, name, observer, useCapture); +var Toggle = { display: Element.toggle }; + +Element.Methods.childOf = Element.Methods.descendantOf; + +var Insertion = { + Before: function(element, content) { + return Element.insert(element, {before:content}); }, - stopObserving: function(element, name, observer, useCapture) { - element = $(element); - useCapture = useCapture || false; + Top: function(element, content) { + return Element.insert(element, {top:content}); + }, - if (name == 'keypress' && - (navigator.appVersion.match(/Konqueror|Safari|KHTML/) - || element.detachEvent)) - name = 'keydown'; + Bottom: function(element, content) { + return Element.insert(element, {bottom:content}); + }, - if (element.removeEventListener) { - element.removeEventListener(name, observer, useCapture); - } else if (element.detachEvent) { - try { - element.detachEvent('on' + name, observer); - } catch (e) {} - } + After: function(element, content) { + return Element.insert(element, {after:content}); } -}); +}; + +var $continue = new Error('"throw $continue" is deprecated, use "return" instead'); -/* prevent memory leaks in IE */ -if (navigator.appVersion.match(/\bMSIE\b/)) - Event.observe(window, 'unload', Event.unloadCache, false); +// This should be moved to script.aculo.us; notice the deprecated methods +// further below, that map to the newer Element methods. var Position = { // set to true if needed, warning: firefox performance problems // NOT neeeded for page scrolling, only if draggable contained in @@ -2307,59 +4084,13 @@ var Position = { || 0; }, - realOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } while (element); - return [valueL, valueT]; - }, - - cumulativeOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - return [valueL, valueT]; - }, - - positionedOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if(element.tagName=='BODY') break; - var p = Element.getStyle(element, 'position'); - if (p == 'relative' || p == 'absolute') break; - } - } while (element); - return [valueL, valueT]; - }, - - offsetParent: function(element) { - if (element.offsetParent) return element.offsetParent; - if (element == document.body) return element; - - while ((element = element.parentNode) && element != document.body) - if (Element.getStyle(element, 'position') != 'static') - return element; - - return document.body; - }, - // caches x/y coordinate pair to use with overlap within: function(element, x, y) { if (this.includeScrollOffsets) return this.withinIncludingScrolloffsets(element, x, y); this.xcomp = x; this.ycomp = y; - this.offset = this.cumulativeOffset(element); + this.offset = Element.cumulativeOffset(element); return (y >= this.offset[1] && y < this.offset[1] + element.offsetHeight && @@ -2368,11 +4099,11 @@ var Position = { }, withinIncludingScrolloffsets: function(element, x, y) { - var offsetcache = this.realOffset(element); + var offsetcache = Element.cumulativeScrollOffset(element); this.xcomp = x + offsetcache[0] - this.deltaX; this.ycomp = y + offsetcache[1] - this.deltaY; - this.offset = this.cumulativeOffset(element); + this.offset = Element.cumulativeOffset(element); return (this.ycomp >= this.offset[1] && this.ycomp < this.offset[1] + element.offsetHeight && @@ -2391,125 +4122,104 @@ var Position = { element.offsetWidth; }, - page: function(forElement) { - var valueT = 0, valueL = 0; + // Deprecation layer -- use newer Element methods now (1.5.2). - var element = forElement; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; + cumulativeOffset: Element.Methods.cumulativeOffset, - // Safari fix - if (element.offsetParent==document.body) - if (Element.getStyle(element,'position')=='absolute') break; + positionedOffset: Element.Methods.positionedOffset, - } while (element = element.offsetParent); - - element = forElement; - do { - if (!window.opera || element.tagName=='BODY') { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); + absolutize: function(element) { + Position.prepare(); + return Element.absolutize(element); + }, - return [valueL, valueT]; + relativize: function(element) { + Position.prepare(); + return Element.relativize(element); }, - clone: function(source, target) { - var options = Object.extend({ - setLeft: true, - setTop: true, - setWidth: true, - setHeight: true, - offsetTop: 0, - offsetLeft: 0 - }, arguments[2] || {}) + realOffset: Element.Methods.cumulativeScrollOffset, - // find page position of source - source = $(source); - var p = Position.page(source); + offsetParent: Element.Methods.getOffsetParent, - // 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); - } + page: Element.Methods.viewportOffset, - // correct by body offsets (fixes Safari) - if (parent == document.body) { - delta[0] -= document.body.offsetLeft; - delta[1] -= document.body.offsetTop; + clone: function(source, target, options) { + options = options || { }; + return Element.clonePosition(target, source, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){ + function iter(name) { + return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]"; + } + + instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ? + function(element, className) { + className = className.toString().strip(); + var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className); + return cond ? document._getElementsByXPath('.//*' + cond, element) : []; + } : function(element, className) { + className = className.toString().strip(); + var elements = [], classNames = (/\s/.test(className) ? $w(className) : null); + if (!classNames && !className) return elements; + + var nodes = $(element).getElementsByTagName('*'); + className = ' ' + className + ' '; + + for (var i = 0, child, cn; child = nodes[i]; i++) { + if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) || + (classNames && classNames.all(function(name) { + return !name.toString().blank() && cn.include(' ' + name + ' '); + })))) + elements.push(Element.extend(child)); } + return elements; + }; - // 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'; - }, + return function(className, parentElement) { + return $(parentElement || document.body).getElementsByClassName(className); + }; +}(Element.Methods); - 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.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, - 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; + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, - element.style.position = 'absolute'; - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.width = width + 'px'; - element.style.height = height + 'px'; + set: function(className) { + this.element.className = className; }, - relativize: function(element) { - element = $(element); - if (element.style.position == 'relative') return; - Position.prepare(); + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, - element.style.position = 'relative'; - var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); - var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.height = element._originalHeight; - element.style.width = element._originalWidth; + toString: function() { + return $A(this).join(' '); } -} - -// 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); +Object.extend(Element.ClassNames.prototype, Enumerable); - return [valueL, valueT]; - } -} +/*--------------------------------------------------------------------------*/ Element.addMethods();
\ No newline at end of file diff --git a/groups/public/stylesheets/application.css b/groups/public/stylesheets/application.css index 8169beb49..ab6c83c29 100644 --- a/groups/public/stylesheets/application.css +++ b/groups/public/stylesheets/application.css @@ -75,12 +75,12 @@ a, a:link, a:visited{ color: #2A5685; text-decoration: none; } a:hover, a:active{ color: #c61a1a; text-decoration: underline;} a img{ border: 0; } -a.issue.closed, .issue.closed a { text-decoration: line-through; } +a.issue.closed { text-decoration: line-through; } /***** Tables *****/ table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } -table.list td { overflow: hidden; vertical-align: top;} +table.list td { vertical-align: top; } table.list td.id { width: 2%; text-align: center;} table.list td.checkbox { width: 15px; padding: 0px;} @@ -97,6 +97,10 @@ tr.entry td.size { text-align: right; font-size: 90%; } tr.entry td.revision, tr.entry td.author { text-align: center; } tr.entry td.age { text-align: right; } +tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} +tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} +tr.entry.file td.filename a { margin-left: 16px; } + tr.changeset td.author { text-align: center; width: 15%; } tr.changeset td.committed_on { text-align: center; width: 15%; } @@ -160,6 +164,8 @@ input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} fieldset {border: 1px solid #e4e4e4; margin:0;} legend {color: #484848;} hr { width: 100%; height: 1px; background: #ccc; border: 0;} +blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} +blockquote blockquote { margin-left: 0;} textarea.wiki-edit { width: 99%; } li p {margin-top: 0;} div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} @@ -179,22 +185,32 @@ div#issue-changesets .changeset { padding: 4px;} div#issue-changesets .changeset { border-bottom: 1px solid #ddd; } div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} -div#activity dl { margin-left: 2em; } -div#activity dd { margin-bottom: 1em; padding-left: 18px; } -div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dl, #search-results { margin-left: 2em; } +div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } +div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt.me .time { border-bottom: 1px solid #999; } div#activity dt .time { color: #777; font-size: 80%; } -div#activity dd .description { font-style: italic; } -div#activity span.project:after { content: " -"; } -div#activity dt.issue { background-image: url(../images/ticket.png); } -div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); } -div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); } -div#activity dt.changeset { background-image: url(../images/changeset.png); } -div#activity dt.news { background-image: url(../images/news.png); } -div#activity dt.message { background-image: url(../images/message.png); } -div#activity dt.reply { background-image: url(../images/comments.png); } -div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); } -div#activity dt.attachment { background-image: url(../images/attachment.png); } -div#activity dt.document { background-image: url(../images/document.png); } +div#activity dd .description, #search-results dd .description { font-style: italic; } +div#activity span.project:after, #search-results span.project:after { content: " -"; } +div#activity dd span.description, #search-results dd span.description { display:block; } + +#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } +div#search-results-counts {float:right;} +div#search-results-counts ul { margin-top: 0.5em; } +div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } + +dt.issue { background-image: url(../images/ticket.png); } +dt.issue-edit { background-image: url(../images/ticket_edit.png); } +dt.issue-closed { background-image: url(../images/ticket_checked.png); } +dt.issue-note { background-image: url(../images/ticket_note.png); } +dt.changeset { background-image: url(../images/changeset.png); } +dt.news { background-image: url(../images/news.png); } +dt.message { background-image: url(../images/message.png); } +dt.reply { background-image: url(../images/comments.png); } +dt.wiki-page { background-image: url(../images/wiki_edit.png); } +dt.attachment { background-image: url(../images/attachment.png); } +dt.document { background-image: url(../images/document.png); } +dt.project { background-image: url(../images/projects.png); } div#roadmap fieldset.related-issues { margin-bottom: 1em; } div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; } @@ -212,6 +228,10 @@ table#time-report tbody tr.last-level { font-style: normal; color: #555; } table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; } table#time-report .hours-dec { font-size: 0.9em; } +ul.properties {padding:0; font-size: 0.9em; color: #777;} +ul.properties li {list-style-type:none;} +ul.properties li span {font-style:italic;} + .total-hours { font-size: 110%; font-weight: bold; } .total-hours span.hours-int { font-size: 120%; } @@ -230,6 +250,8 @@ height: 1%; clear:left; } +html>body .tabular p {overflow:hidden;} + .tabular label{ font-weight: bold; float: left; @@ -246,6 +268,8 @@ text-align: left; width: 200px; } +input#time_entry_comments { width: 90%;} + #preview fieldset {margin-top: 1em; background: url(../images/draft.png)} .tabular.settings p{ padding-left: 300px; } @@ -444,31 +468,35 @@ div.wiki pre { overflow-x: auto; } -div.wiki div.toc { +div.wiki ul.toc { background-color: #ffffdd; border: 1px solid #e4e4e4; padding: 4px; line-height: 1.2em; margin-bottom: 12px; margin-right: 12px; + margin-left: 0; display: table } -* html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */ +* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ -div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } -div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } +div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } +div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } +div.wiki ul.toc li { list-style-type:none;} +div.wiki ul.toc li.heading2 { margin-left: 6px; } +div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; } -div.wiki div.toc a { - display: block; +div.wiki ul.toc a { font-size: 0.9em; font-weight: normal; text-decoration: none; color: #606060; } -div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;} +div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} -div.wiki div.toc a.heading2 { margin-left: 6px; } -div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; } +a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } +a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } +h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } /***** My page layout *****/ .block-receiver { @@ -578,6 +606,7 @@ vertical-align: middle; .icon-checked { background-image: url(../images/true.png); } .icon-details { background-image: url(../images/zoom_in.png); } .icon-report { background-image: url(../images/report.png); } +.icon-comment { background-image: url(../images/comment.png); } .icon22-projects { background-image: url(../images/22x22/projects.png); } .icon22-users { background-image: url(../images/22x22/users.png); } diff --git a/groups/public/stylesheets/context_menu.css b/groups/public/stylesheets/context_menu.css index e5a83be0d..b3aa1aca0 100644 --- a/groups/public/stylesheets/context_menu.css +++ b/groups/public/stylesheets/context_menu.css @@ -1,4 +1,4 @@ -#context-menu { position: absolute; z-index: 10;} +#context-menu { position: absolute; z-index: 40; font-size: 0.9em;} #context-menu ul, #context-menu li, #context-menu a { display:block; @@ -20,7 +20,7 @@ #context-menu li { position:relative; padding:1px; - z-index:9; + z-index:39; } #context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; } #context-menu li.folder>ul { left:148px; } @@ -31,10 +31,10 @@ #context-menu a { border:1px solid white; - text-decoration:none; + text-decoration:none !important; background-repeat: no-repeat; background-position: 1px 50%; - padding: 2px 0px 2px 20px; + padding: 1px 0px 1px 20px; width:100%; /* IE */ } #context-menu li>a { width:auto; } /* others */ @@ -42,7 +42,7 @@ #context-menu li a.submenu { background:url("../images/sub.gif") right no-repeat; } #context-menu a:hover { border-color:gray; background-color:#eee; color:#2A5685; } #context-menu li.folder a:hover { background-color:#eee; } -#context-menu li.folder:hover { z-index:10; } +#context-menu li.folder:hover { z-index:40; } #context-menu ul ul, #context-menu li:hover ul ul { display:none; } #context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; } diff --git a/groups/public/stylesheets/jstoolbar.css b/groups/public/stylesheets/jstoolbar.css index c4ab55711..4e9d44b6c 100644 --- a/groups/public/stylesheets/jstoolbar.css +++ b/groups/public/stylesheets/jstoolbar.css @@ -84,6 +84,12 @@ .jstb_ol { background-image: url(../images/jstoolbar/bt_ol.png); } +.jstb_bq { + background-image: url(../images/jstoolbar/bt_bq.png); +} +.jstb_unbq { + background-image: url(../images/jstoolbar/bt_bq_remove.png); +} .jstb_pre { background-image: url(../images/jstoolbar/bt_pre.png); } diff --git a/groups/public/stylesheets/scm.css b/groups/public/stylesheets/scm.css index 66847af8c..d5a879bf1 100644 --- a/groups/public/stylesheets/scm.css +++ b/groups/public/stylesheets/scm.css @@ -1,14 +1,16 @@ table.filecontent { border: 1px solid #ccc; border-collapse: collapse; width:98%; } table.filecontent th { border: 1px solid #ccc; background-color: #eee; } -table.filecontent th.filename { background-color: #ddc; text-align: left; } -table.filecontent tr.spacing { border: 1px solid #d7d7d7; } +table.filecontent th.filename { background-color: #e4e4d4; text-align: left; padding: 0.2em;} +table.filecontent tr.spacing th { text-align:center; } +table.filecontent tr.spacing td { height: 0.4em; background: #EAF2F5;} table.filecontent th.line-num { border: 1px solid #d7d7d7; font-size: 0.8em; text-align: right; width: 2%; padding-right: 3px; + color: #999; } table.filecontent td.line-code pre { white-space: pre-wrap; /* CSS2.1 compliant */ diff --git a/groups/script/dbconsole b/groups/script/dbconsole new file mode 100644 index 000000000..caa60ce82 --- /dev/null +++ b/groups/script/dbconsole @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../config/boot' +require 'commands/dbconsole' diff --git a/groups/script/performance/request b/groups/script/performance/request new file mode 100644 index 000000000..ae3f38c74 --- /dev/null +++ b/groups/script/performance/request @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/performance/request' diff --git a/groups/script/process/inspector b/groups/script/process/inspector new file mode 100644 index 000000000..bf25ad86d --- /dev/null +++ b/groups/script/process/inspector @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../config/boot' +require 'commands/process/inspector' diff --git a/groups/test/fixtures/attachments.yml b/groups/test/fixtures/attachments.yml index 162d44720..ec57aa6dd 100644 --- a/groups/test/fixtures/attachments.yml +++ b/groups/test/fixtures/attachments.yml @@ -36,4 +36,53 @@ attachments_003: filename: logo.gif
description: This is a logo
author_id: 2
+attachments_004:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_source.rb
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 4
+ filesize: 153
+ filename: source.rb
+ author_id: 2
+ description: This is a Ruby source file
+ content_type: application/x-ruby
+attachments_005:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_changeset.diff
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 5
+ filesize: 687
+ filename: changeset.diff
+ author_id: 2
+ content_type: text/x-diff
+attachments_006:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_archive.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 6
+ filesize: 157
+ filename: archive.zip
+ author_id: 2
+ content_type: application/octet-stream
+attachments_007:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 4
+ downloads: 0
+ disk_filename: 060719210727_archive.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 7
+ filesize: 157
+ filename: archive.zip
+ author_id: 1
+ content_type: application/octet-stream
\ No newline at end of file diff --git a/groups/test/fixtures/changes.yml b/groups/test/fixtures/changes.yml index 30acbd02d..56d936296 100644 --- a/groups/test/fixtures/changes.yml +++ b/groups/test/fixtures/changes.yml @@ -13,4 +13,11 @@ changes_002: path: /test/some/path/elsewhere/in/the/repo
from_path:
from_revision:
+changes_003:
+ id: 3
+ changeset_id: 101
+ action: M
+ path: /test/some/path/in/the/repo
+ from_path:
+ from_revision:
\ No newline at end of file diff --git a/groups/test/fixtures/custom_fields.yml b/groups/test/fixtures/custom_fields.yml index 6be840fcc..1005edae4 100644 --- a/groups/test/fixtures/custom_fields.yml +++ b/groups/test/fixtures/custom_fields.yml @@ -4,9 +4,13 @@ custom_fields_001: min_length: 0
regexp: ""
is_for_all: true
+ is_filter: true
type: IssueCustomField
max_length: 0
- possible_values: MySQL|PostgreSQL|Oracle
+ possible_values:
+ - MySQL
+ - PostgreSQL
+ - Oracle
id: 1
is_required: false
field_format: list
@@ -29,9 +33,14 @@ custom_fields_003: min_length: 0
regexp: ""
is_for_all: false
+ is_filter: true
type: ProjectCustomField
max_length: 0
- possible_values: Stable|Beta|Alpha|Planning
+ possible_values:
+ - Stable
+ - Beta
+ - Alpha
+ - Planning
id: 3
is_required: true
field_format: list
diff --git a/groups/test/fixtures/enabled_modules.yml b/groups/test/fixtures/enabled_modules.yml index 8d1565534..da63bad5d 100644 --- a/groups/test/fixtures/enabled_modules.yml +++ b/groups/test/fixtures/enabled_modules.yml @@ -39,4 +39,8 @@ enabled_modules_010: name: wiki project_id: 3 id: 10 +enabled_modules_011: + name: issue_tracking + project_id: 2 + id: 11
\ No newline at end of file diff --git a/groups/test/fixtures/enumerations.yml b/groups/test/fixtures/enumerations.yml index c90a997ee..22a581ab9 100644 --- a/groups/test/fixtures/enumerations.yml +++ b/groups/test/fixtures/enumerations.yml @@ -19,6 +19,7 @@ enumerations_005: name: Normal
id: 5
opt: IPRI
+ is_default: true
enumerations_006:
name: High
id: 6
@@ -39,4 +40,9 @@ enumerations_010: name: Development
id: 10
opt: ACTI
+ is_default: true
+enumerations_011:
+ name: QA
+ id: 11
+ opt: ACTI
\ No newline at end of file diff --git a/groups/test/fixtures/files/060719210727_archive.zip b/groups/test/fixtures/files/060719210727_archive.zip Binary files differnew file mode 100644 index 000000000..5467885d4 --- /dev/null +++ b/groups/test/fixtures/files/060719210727_archive.zip diff --git a/groups/test/fixtures/files/060719210727_changeset.diff b/groups/test/fixtures/files/060719210727_changeset.diff new file mode 100644 index 000000000..af2c2068d --- /dev/null +++ b/groups/test/fixtures/files/060719210727_changeset.diff @@ -0,0 +1,13 @@ +Index: trunk/app/controllers/issues_controller.rb +=================================================================== +--- trunk/app/controllers/issues_controller.rb (r‚vision 1483) ++++ trunk/app/controllers/issues_controller.rb (r‚vision 1484) +@@ -149,7 +149,7 @@ + attach_files(@issue, params[:attachments]) + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') +- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project ++ redirect_to :controller => 'issues', :action => 'show', :id => @issue + return + end + end diff --git a/groups/test/fixtures/files/060719210727_source.rb b/groups/test/fixtures/files/060719210727_source.rb new file mode 100644 index 000000000..dccb59165 --- /dev/null +++ b/groups/test/fixtures/files/060719210727_source.rb @@ -0,0 +1,10 @@ +# The Greeter class +class Greeter + def initialize(name) + @name = name.capitalize + end + + def salute + puts "Hello #{@name}!" + end +end diff --git a/groups/test/fixtures/issue_categories.yml b/groups/test/fixtures/issue_categories.yml index 6c2a07b58..aa2f70351 100644 --- a/groups/test/fixtures/issue_categories.yml +++ b/groups/test/fixtures/issue_categories.yml @@ -9,3 +9,14 @@ issue_categories_002: project_id: 1
assigned_to_id:
id: 2
+issue_categories_003:
+ name: Stock management
+ project_id: 2
+ assigned_to_id:
+ id: 3
+issue_categories_004:
+ name: Printing
+ project_id: 2
+ assigned_to_id:
+ id: 4
+
\ No newline at end of file diff --git a/groups/test/fixtures/issues.yml b/groups/test/fixtures/issues.yml index 4f42d93c4..9d3287c6f 100644 --- a/groups/test/fixtures/issues.yml +++ b/groups/test/fixtures/issues.yml @@ -13,6 +13,8 @@ issues_001: assigned_to_id:
author_id: 2
status_id: 1
+ start_date: <%= 1.day.ago.to_date.to_s(:db) %>
+ due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
issues_002:
created_on: 2006-07-19 21:04:21 +02:00
project_id: 1
@@ -20,13 +22,15 @@ issues_002: priority_id: 5
subject: Add ingredients categories
id: 2
- fixed_version_id:
+ fixed_version_id: 2
category_id:
description: Ingredients of the recipe should be classified by categories
tracker_id: 2
assigned_to_id: 3
author_id: 2
status_id: 2
+ start_date: <%= 2.day.ago.to_date.to_s(:db) %>
+ due_date:
issues_003:
created_on: 2006-07-19 21:07:27 +02:00
project_id: 1
@@ -38,7 +42,7 @@ issues_003: category_id:
description: Error 281 is encountered when saving a recipe
tracker_id: 1
- assigned_to_id:
+ assigned_to_id: 3
author_id: 2
status_id: 1
start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
@@ -71,4 +75,20 @@ issues_005: assigned_to_id:
author_id: 2
status_id: 1
-
+issues_006:
+ created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ project_id: 5
+ updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Issue of a private subproject
+ id: 6
+ fixed_version_id:
+ category_id:
+ description: This is an issue of a private subproject of cookbook
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= Date.today.to_s(:db) %>
+ due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+
\ No newline at end of file diff --git a/groups/test/fixtures/mail_handler/add_note_to_issue.txt b/groups/test/fixtures/mail_handler/add_note_to_issue.txt deleted file mode 100644 index 4fc6b68fb..000000000 --- a/groups/test/fixtures/mail_handler/add_note_to_issue.txt +++ /dev/null @@ -1,14 +0,0 @@ -x-sender: <jsmith@somenet.foo>
-x-receiver: <redmine@somenet.foo>
-Received: from somenet.foo ([127.0.0.1]) by somenet.foo;
- Sun, 25 Feb 2007 09:57:56 GMT
-Date: Sun, 25 Feb 2007 10:57:56 +0100
-From: jsmith@somenet.foo
-To: redmine@somenet.foo
-Message-Id: <45e15df440c00_b90238570a27b@osiris.tmail>
-In-Reply-To: <45e15df440c29_b90238570a27b@osiris.tmail>
-Subject: [Cookbook - Feature #2]
-Mime-Version: 1.0
-Content-Type: text/plain; charset=utf-8
-
-Note added by mail
diff --git a/groups/test/fixtures/mail_handler/ticket_on_given_project.eml b/groups/test/fixtures/mail_handler/ticket_on_given_project.eml new file mode 100644 index 000000000..927dbc63e --- /dev/null +++ b/groups/test/fixtures/mail_handler/ticket_on_given_project.eml @@ -0,0 +1,42 @@ +Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+Project: onlinestore
+Status: Resolved
+
diff --git a/groups/test/fixtures/mail_handler/ticket_reply.eml b/groups/test/fixtures/mail_handler/ticket_reply.eml new file mode 100644 index 000000000..99fcfa0d1 --- /dev/null +++ b/groups/test/fixtures/mail_handler/ticket_reply.eml @@ -0,0 +1,73 @@ +Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
+Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+References: <485d0ad366c88_d7014663a025f@osiris.tmail>
+Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
+Date: Sat, 21 Jun 2008 18:41:39 +0200
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+This is reply
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/html;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
+<STYLE>BODY {
+ FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
+}
+BODY H1 {
+ FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
+sans-serif
+}
+A {
+ COLOR: #2a5685
+}
+A:link {
+ COLOR: #2a5685
+}
+A:visited {
+ COLOR: #2a5685
+}
+A:hover {
+ COLOR: #c61a1a
+}
+A:active {
+ COLOR: #c61a1a
+}
+HR {
+ BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
+WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
+}
+.footer {
+ FONT-SIZE: 0.8em; FONT-STYLE: italic
+}
+</STYLE>
+
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
+<BODY bgColor=3D#ffffff>
+<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
+size=3D2>This is=20
+reply</FONT></DIV></SPAN></BODY></HTML>
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0--
+
diff --git a/groups/test/fixtures/mail_handler/ticket_reply_with_status.eml b/groups/test/fixtures/mail_handler/ticket_reply_with_status.eml new file mode 100644 index 000000000..ab799198b --- /dev/null +++ b/groups/test/fixtures/mail_handler/ticket_reply_with_status.eml @@ -0,0 +1,75 @@ +Return-Path: <jsmith@somenet.foo> +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200 +Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris> +From: "John Smith" <jsmith@somenet.foo> +To: <redmine@somenet.foo> +References: <485d0ad366c88_d7014663a025f@osiris.tmail> +Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories +Date: Sat, 21 Jun 2008 18:41:39 +0200 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a multi-part message in MIME format. + +------=_NextPart_000_0067_01C8D3CE.711F9CC0 +Content-Type: text/plain; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +This is reply + +Status: Resolved +------=_NextPart_000_0067_01C8D3CE.711F9CC0 +Content-Type: text/html; + charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<HTML><HEAD> +<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8"> +<STYLE>BODY { + FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif +} +BODY H1 { + FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, = +sans-serif +} +A { + COLOR: #2a5685 +} +A:link { + COLOR: #2a5685 +} +A:visited { + COLOR: #2a5685 +} +A:hover { + COLOR: #c61a1a +} +A:active { + COLOR: #c61a1a +} +HR { + BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; = +WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px +} +.footer { + FONT-SIZE: 0.8em; FONT-STYLE: italic +} +</STYLE> + +<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD> +<BODY bgColor=3D#ffffff> +<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 = +size=3D2>This is=20 +reply Status: Resolved</FONT></DIV></SPAN></BODY></HTML> + +------=_NextPart_000_0067_01C8D3CE.711F9CC0-- + diff --git a/groups/test/fixtures/mail_handler/ticket_with_attachment.eml b/groups/test/fixtures/mail_handler/ticket_with_attachment.eml new file mode 100644 index 000000000..c85f6b4a2 --- /dev/null +++ b/groups/test/fixtures/mail_handler/ticket_with_attachment.eml @@ -0,0 +1,248 @@ +Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sat, 21 Jun 2008 15:53:25 +0200
+Message-ID: <002301c8d3a6$2cdf6950$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: Ticket created by email with attachment
+Date: Sat, 21 Jun 2008 15:53:25 +0200
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_001F_01C8D3B6.F05C5270"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_001F_01C8D3B6.F05C5270
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_001_0020_01C8D3B6.F05C5270"
+
+
+------=_NextPart_001_0020_01C8D3B6.F05C5270
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+This is a new ticket with attachments
+------=_NextPart_001_0020_01C8D3B6.F05C5270
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; =
+charset=3Diso-8859-1">
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR>
+<STYLE></STYLE>
+</HEAD>
+<BODY bgColor=3D#ffffff>
+<DIV><FONT face=3DArial size=3D2>This is a new ticket with=20
+attachments</FONT></DIV></BODY></HTML>
+
+------=_NextPart_001_0020_01C8D3B6.F05C5270--
+
+------=_NextPart_000_001F_01C8D3B6.F05C5270
+Content-Type: image/jpeg;
+ name="Paella.jpg"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="Paella.jpg"
+
+/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcU
+FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgo
+KCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCACmAMgDASIA
+AhEBAxEB/8QAHQAAAgMBAQEBAQAAAAAAAAAABQYABAcDCAIBCf/EADsQAAEDAwMCBQIDBQcFAQAA
+AAECAwQABREGEiExQQcTIlFhcYEUMpEVI0Kh0QhSYrHB4fAWJCUzQ3L/xAAaAQADAQEBAQAAAAAA
+AAAAAAADBAUCAQYA/8QAKhEAAgIBBAICAgIDAAMAAAAAAQIAAxEEEiExIkEFE1FhMnFCkaEjwdH/
+2gAMAwEAAhEDEQA/ACTUdSsdhRCNE54GTRaBaXHiBtNOVo0wEpSt8BKfmpWCZRPHcVbdZ3X1J9Jx
+Tla9OBpIU8Noo7Gjx4qdrCBkfxGupUSck13GJjeT1ObEdthOG04/zpX8SNXjR1njym46ZMmQ+llp
+pStuc9T9hRq/X22afhKl3iazEYHdxWCfgDqT9K83eKfiFG1RfIEi3tuC3W9KlNh0YLqyeuO3QV0D
+MznM9O2uai4QI8psYQ8gLA9virY615P034xX+zNNslLDsMKOG1J5HuAa3nQPiBZ9WtpUy4lmcE4U
+ypXP2rmMHmcI/EealD7te7ZZ2S7dLhGiN9cvOBP+dIF18btHw3C1DkSbi7nATGZJBPwTitTIyZp9
+SsCun9oJaEFUDTy0oyQFyXSOfoB/rQOL466huE9LIagxW1A48tkuKJxwBlQrm4YzNhGPE9Mmua8Y
+JrzsrXPiQ42y7+KtsZt4kpS8ltK0p91J5IzXGFr3xFef8pMqE4vJABZT6se3FDNyEZzNCh89Tfbv
+aoV2iKj3GO2+0eyh0+h7VkWq/CqTDUqXpp0uJHPkKOFj6HofvQRzxZ1bbwFTG7c+jO0lKeh+cGi8
+bxrebZZVMtjDqljKgw4Rt9uuea5vEIEceoL09ZnHQoyGy3KaOFhxO0j6g0J8QNPr3tzorHmsJSUv
+NgdQeprTIuqbfqdtD7MRxh7HO/H6ZHWlnW0e5tQnv2WgupAyEg8p9xUl7WGowpzKCoDXyJ5nvMdK
+Uuho4bSv057CqK2stIWrgEZp2kWtE+O5+MC0OKUchHFCbnaWVNeW1KU3tTtwtAUkj6jkfpXoK7gQ
+AZLsqYEmJ0mUBlLeCfeqHKl5PqJopNhriupQWyoqPpKeQfpTXYPDW+3ZlEhTTcVpXI8w+oj6Cmty
+qMxTazHAi1ZLG/PXuKClv3Ip7t2n4yI3lKZSsEc7hmicXwfu5ThN22fCUH+tXB4QX1KdzN6WVjth
+Q/1oDuG/yjCIV/xgWLouQFfiLK/5LqejbnKT9D1FStX05DRaYrTN8K232wEl1aMJV856VKF9hPc3
+9QPM32HEjxEjykBSh/ERSd4s61uGjLbBnQrcie2t4pfClEFKAM8Y704uvtsMrdfcQ20gZUtZAAHu
+SawHxt8V7PKt/wCytPp/aLrToW7JAPlNkAjAPfOfpQ0JY4E42B3Nf09ruwXvTQvjM9lmGkfvvOWE
+llXdKvn/ADrONZeNwU28zo2Ml1tHpXc5Y2spP+EHlR/5ivOzYkPPKdjMechRDjrCUHy1Ec9Aa1Lw
+l0VF10pcy4XJC0RlbTFTgKbHwnokfSibFXkzAJbiJ0tN81jc1yHXplzkEEqkPA7UjvtR2H1/SrOl
+rGu6NvP7Q8yhaWkDruVj/n616Lvl20n4Z2cpeS02tSfRHbAU69/t8nivOGoNXzNQSVRbFAbtsFal
+FESEjBOepUR1rBs3D8CFVMHjmXNYW+wWtsMrlMvyyOW4h3FB9irpn70lx7k9AeDttW4w70DgWd3+
+1NmlvDi7XpL0iShcWG0dqllO5SlHsB35NG7l4PSRG823z0YbGFqkDaFK+MZx7d6XOu09Z2M8MKHb
+OBM1vBuAkJcuUgyHXRu3KfDp+5ycVTaeU36kKUlYOQQcEVrehvC5l1Mh/VClISHFMttIVgL45VnH
+TkEH4rQbjpHTbyGWVQIzL7bYabc2AnaMfYnAxk0K35Smo7e/2IRdC7eXUwfT5m6pfbtC/wARIlLW
+VNu7yoN9MlQ9h3NO+n9Cwo8rzZU1Sm2Mlx9YLaUkHjaOv3Nc7zd7FoyY5D07HR56SfMl7961ZGNo
+9gKXrtd77dnkssoSwt7K9rZG8jHU44Tkc9q0rvbyvipnNgT9kTRLvqKy2JDgS/8AiH3hjecKXjv2
+/SkG8akmRyhqG+hKSQ4dpyofBxxV2w+Hkuda27pMW5tcSpWxati1HJGQTkYp70xoS2MW1pp+ImXN
+koJLi+UtfP1FAt1dFPHcPXQ9nPUy+/3pu4usrYZS16MOKCAkuLJypRxX5aG5ExX4VlfC/Vt98e3z
+WvL8M9NsNMtyFyVyGx6h5uPMPyMcV9Q9HQbbdWwzHQGFHKVhStw+uTQTr6tu1IQad85M46baVarV
+uVkJ/mDVCVqWUll59t4FxlW0ocOA4k+1P8uLGU35UgAhQ2kgdRWUeIMi2WyKqASFLJJbWchQI7Ul
+pWWyw5GSYZ1IXA4Ez7U12mR7q95jCWgTuCQeoPsaGqntylbCpIdxnaSM/wBK56lujtydZS4UkNIw
+CBzQO4RURywWnUupcQF7knoT1BHYg5r0lFY2DIwZKvYq5x1DjUo26WzJKEuIQoFSFDIP+9bzaL0x
++HZcZcQpC0ggewIrzYzNJQGpGVt+/cUw2PU8+0vqWEJnW8q/9KzgpHslXb6UV6yw4gBZg8z1NZbj
+Ek43LQDjkZFMLbkMcJW3+orKvDq86T1SUssrEef3iPq2rz8f3vtTZrtizaR0pOvD8XephOG2959a
+ycJH60HBBxDBhjMB+L9/RY7WpT7jam3kkNNJwSs+/NSss0Bpi4+Jmpfxl7kPOQ2k7iCfyI/hQOwz
+/vUroqrUnceZ8LnIG2Cdaa61Dq54i7SVJi5ymGwdjSf/ANe/86s6W0TLvkNySp5pcVjBUy0oAD5x
+1P1NbDbPALTQjp/aC5bj+OS27tH+VOmjPDqw6QEv9lNPFcpIQ4p5zeSB0A/WtNYoXCwK1nOWgjwk
+sFrg2wuJjtKl5IJUBwPakLxDXbNI6/alaGW6b87uL1vjJCmAogjcvHTrnb8DpVnxj1q1oOS7b9PP
+j9qSEErA58gHuf8AF7CsStOurpBjKZioQqS6sqU+vlayepPvQytu3cgz/fEPWaXfFjYEfLlo5+bM
+/aurr+X33vW6lIJUD/dyen2p80zboMNG6NBEGOygJLy04cdAGRjjn5NYRD1NcjMMme8XpST6Q4Mp
+H0HStstF4kO2lMS5vAlTfq9O04PQZ+KifILaqg3PnPodS5o0S3I0q4x2T3Kr+obzH1HsjuFFpeUU
+B5s5Snck4ST0z0p502w5HZW86qW5lXLbpSeMfHFZH4gpFutbDlrmNtujlxvzc705HAHfB5qknVSI
+VliuWK7STcHVBL7Ticc8c8f70IaMaipWq4z+oo6jT2sr8ma3qCfBky48be4zvcAOB6gR/CMd6EXF
+m9EPKhx3Vx92EJdADmOmQKJ2y5xVpiJlW+OzPSj1LbSBtURyoGjFzWqPbHljClFBLbiBnHHUmpeT
+WdqiPISuDM/e0bark4YzkEJkJ9RebGF7u+T/AKVeg6DbVdXHJ6U/hi35KAlRGU44zj/WrtpdfSlt
+D7m54jKznr/WnOAVKa9Y7cGtDVWodhaH1WnVlD7cZxPhq3NMobbeBeZQnalKlZ47cUQDSGtvlqwn
+GEp7AVQdbddWQHkp2dOea6qWHQlPmJSscEE9aET/AJCK/X+JFxUtuKecHnKxx8VXRKiBSkuKII55
+PSvq4yUQmf3qspxwc8is71fqZMeKtTO0AHn3V8UaitrDgdmcdtoyZ215q1USShq0bZClghTYPqFL
+Vr0xH1otbt1XKZkpT6cccfOaF6SZkz7q7dZYWHjz0ykJp2Yvi4YaYVHdUXjs2eSUlR7HPt89KoW5
+p8af5D3OVLldz9GLmsNLR1WZiI+oJlRB5aHgBuKe2cdaxd5tVsuy0OJbdWwvkKGUq+or0PqiyXVy
+IJ7za1NlIJbz6m/fgdv61lN000qWJ09EWQ8++6lqM01k8geokY5p/wCK1RXK2Nn/AOz75PS1vStt
+Y594iCUnOauWi5SLXMDzIQ4g8ONOp3IcT7KHcVduWn7nbWg5OgSI6SopBcQUjPtzXK1RX1OqkMtb
+0xcPO9PSkHrzV0WKRkHM86a2BwZqFm0da9c2pdw0asM3JgBT9qdd2uNH+8y51x7A/rSjrXUmq129
+Om9TuyvKhu70NyUYd4GBlX8QofG1hcLbrBF/tZ/DvtqGEDhJQONpA6gjrXq61f8AS/jDo9mXNhNu
+nGxxPR2O5jkBXX+tY3bcFhPtoPAin4H6gsMTQgLEhtM7eoyGioBYI4Tx7Yx+pqUr668ILjZXDOtS
+XZsdvlMiGkJlND/GgYDg+Rg1KwUDHIM2r7Bgiei5NwiQo635cllllAypbiwAPvWO678c4UJuRH0y
+gSHkDBkrHpz2CR3+prHbXJ1L4o6matwkKaYP7xzkhthsdVEf8NLWrzbo94fh2RKjAjqLSHFnKniO
+Cs/X/KuLSAcN3OfYW5HUD3SXJutxfnTnVOyn1lbi1HJJNPnh9otyfbJF5lLabjpJQ0FjlZHUis9C
+lDOO9bdHkS4WkbXBlIMdaGUnyhwkjqFfU5pf5K566gqe+I98TpBqb9pnB/Q9wu7kdyOGUNNp3oWp
+Owq7+3P1r9uQmqllqS+S+ghClFWR+vtT/Z7goWGOopbjodwEltQOcdR16/WrcrTFmW4tyYZHmuDc
+dhwkDHSvNvq2BC2+up6PThdIzDvMypelJN2lI8+M9JKxsZS1/Cfcn2+tF9K6Oh6ZeW5fYS5VwKgl
+locpR3Cvk0+zJTdtioi2htDe5OVL/KAPcn3r5j3ZtdmkrKFTFJ3EDG7BAzgH9a+XX2sNi8CJXaZW
+c3GIN7u0u931+KwhaGGspKQMKcKepVV5UmU1DZZtzspMVKQXm3F5B+gHIH0zQCBImKuiJMeCuEH1
+YCfVkjv+bqSKr6t1U7a7uxEgurS0yMLBASc/arlenBULiSGtOSSY6WKJKXckJU2tplSt6FA7gfvW
+gxA/sUBggDGSayGya5ed8tkNqSlXVYOVVpEZydIablRFF6ORgjGFJPyKga3Tuj5Il2rVC6sKT1L9
+tiuPTnDI3eSfc/lqrqWOuHFK4qlF1HIX7j2NWIkyQ8XEApSUcD/Ea5TmZj2SggqUMKSrp9KUByQM
+T45U5mSS9UzJMtMZ93GFcqJ7UL8Q3UOOww24Bx6h3V8/Sqev0sx7u4IqkB5w8tJ4KFfNBXG3Fuo/
+FPqLxA3FXXHtXp9PQiBXXiTGZrmIjTo68qh+Y2ygPhYSAlXIBz1rYHp04RkNRnWDOA5KyEgDrgVh
+mmSmPcCfQpWCACnINFdRXOW3GQ4+60GgcJKDgr+R70lqdP8AZaAvuUK3woDY4mqyrjeFWppZZUXW
+lnzUlYCVp+K+LLeYEoLLG5lGdxQk4wcfyrOourlyIzbDhcKVNhHB7e9XYlxatbam0dVDOAOT96Rf
+TEDBHMMpU9dTQpVxiTWXGUqDy1n0hxCSAPvXnfWVtnWO9TI8lpLHnZOGxhKkE54+K1K1XhLj4S4j
+GOnxX5qiNZ7wlpd1Di30ZS0hKtu4kdCaN8fqG0luxhwYtrdOtqZXsTA1dTWh+B+unNG6tbTIWTap
+hDUhGeE56L+oP8qSbtBXDnyWSB+7WUnadwH3rgYT6IQmEpS0VbU5WNyj8DrXr/F1/ueXIZT1P6Hh
+aVoSpJBSoZBB4IqVjPgP4ii72eHZLsSJrCPKadP8YA4B+cfrUpMgg4jK8jMybw5vUfT/AIXatujD
+iRc5S24DX95KVAkn/P8ASstODk9asPSXvwZbUEoQpzhtIwkYHt9z1q3NZiO2uNMhFLbif3chkryc
+9lAHsabbAbP5i6DI/qctPSokW9w3p0cvsIcBLY7+2fituuVxYvDbAMZ2VIUkeX5I5x3Tgdqznwz0
+xbb/ADZQuy3w2y2FISycHJz3+MVtWnNLwNMb3G0SZDvlgb3DlWPgf86V5/5e+oOAc7l/9y18WLK/
+IdH/AHB+l23bLPLMl0RkyQS22r1eWQO/tR178NEju3GS8ZahyVIc7ewA4qpKKfxzTMOGHCsBZSob
+ueveitut+XGo8tpDacEp2DAP69ahNYHO4yo1rMxJgt22RLy0l5bYQ04jckLWfM+o7frVPUMpdg0a
+65EfXvaX5XOArnp9hTtGgRbcyhL6PPbaG1ClnJAPvWeeMl0FogwnWGYkqKHSFxnUkpSojgkD79aJ
+pQbblr9ZgNRcAhMzli9zZYfS27NkPBIKAFKVnnkn2pf1PaZbMNm4PpkDzeV+c0UEK+p6/WtX8H5M
+GXDm3OS22Jq3P/W2AlIHwOgFVPF+VBfjqKi4sEHBKSAVfFegXWsmo+pV4zJZ0wareTFbw71Y1Ab/
+AAjbcNh1Q/8Ae9yaYU33VESW5KdK1wucuMpwgj3FYq4S456E7VDjimGHqa6wYqIS5HmMq42LOQBT
+Wo0AYll5z+YCjV7MA+puVmuDkgh7evZt3bsdK46s1uiNZSY6iHwSj82CPnFC7PcbdbdOxkPTiqaB
+5iQlXCf61mV9uC79dn39oDIVztGAajafRK9pPoSrZezKAOzKclyXcLgue8VLUo7sHrUaVIfeCloG
+T0Uo9qstKdbcBLZUg9DiuzkbY4VDIBGQkdBVkuBxOrRtAwf7naKlyMoqQ4pRI9RHH2qtc1/i/KS+
+p3yWchtKwcIzX7HnoQv1nbgYUR7+9NESXCmR1xdjexxOXCTg9ODSzO1bBiJvCsCBFu3eahwltCnA
+O6ATj6082K2rlltyXGSsIGEhzPP1xQa1QJNngLmMuNPMrPKE5BwKuzrw6Yu6JJVGWkZSkHIXn274
+pe8m0+H+51G2DBlu4J/DzFKbWhICiS2EgH7H2FD3JTMuclt7B2ArBzgJPvQNF1lSUFoON5JyST1P
+tmgEu5yY0wgJ2uoUd27nPtRKdEzHk8xezVLUnHudtXsRYc4rt8pxZdKvMSpWcH60M07a03W5JZcW
+UtgFSj8Dt96orKnVKUQVK6nv966R5b0dCksLLe4gkp68dOatKjBNgPMiM4Z9xHE1fwCkQx4pqYdC
+vJcC1RwT0WkZH8s1KVPDm+Psa208ogAtysqWOqyo4JP2qUtanPM2jDEL+OWn49u8R5UK0MbGClDg
+bSOApYyQPvSzM0rKt9qiXCRs8uSSlCeQoHnII+1aJ/aAZWjxImL3FILTSwR/+RX7bhqJ561XC5Jj
+O20pSnyFYJWMZypJ6djWLdSa1BzxDUaYWnaOzH/RlmZ0nYWPJab9SQqS5t/eLV2+wzj7UfZmouM8
+MNtlsNoKlFZAV8H4FULPfmrmtyCtwJfQjKggFIVx2orHsbUZ1TzCktFwfvVKJJUB05968jqHaxyz
+y3t+sBeiJJTLSXA6hAWscFSTjke561yfkAlte4h88BIJwB3q5Hjx297RUpWfUD+YYqs5Gjx3HJJK
+ywRylIGM+/vShBMIrDMtpKiyVKcWtvaP3aRnn3HevOfi9eZM/UEiEv8A7eOHgkhfT0jg4+5r0JJu
+ENLad0plpWM9c8dqUtTaMtGoJS37gyXH3UANyEHH6iqXx99entD2CK31m1CqmZZomd+HjORbXte8
+hOVLSk4USeTRm4xrvqbTjseUGmozTmVPLH5fgfNNNhYtWmJardbw3tf59XqIwepNM2poyJVpdKEt
++SRuCR/EfemLdWou3oO/cJXVmsI08z3BiFp7UakMuonR0jk47+31oG7iTM/dkNoWvCdx/KCe9P8A
+dIzR1PAZfjtI3gx3QsAJHznFKOqbfbbXKSzbriZrwJ8390UJRjpgnrXpdNeLAM9kSDqKDWT+AYcu
+1ivcK2x1KdiyYSejrCgSnPZXehTLqou7cghKRkgd6Px9SWp2xsMT23HF7QgpaOCFDoaCxFee4UKC
+gCT14P3oKs5B+xccx+kIpG0wlaJKZLB9KglB5Uo9KsLeDj2GzjI+1AjmPLH4ZzCVEApPAIopGCFR
+1rSpW4naaFbWB5DqUabMnaYEuTGyc40le4deO1fMZam17krwAOua7yYjyZCiG8hZ65ya57WW3W2y
+lS3FDkFW0CmgdygdydZ4MT1HezzUy4iCwVKLKcFtSuD74r9uVtRJabLZ8obckpTlP60ItSLXOeDT
+KlR1spG9W7clw/ejN4mXa0MDYA9FLn7olIxtxyFCprVkWbU7/cY+0FNx6/UU70GYDBQw6FrUcAgH
+ke9Lq3FHkkk980xXedHuYWt6D5L4A2rQrCQO4xV+yaaiTrW5JL29GRgflUCOoJ5wPmqaOKUy/cl3
+Zufw6itbriuAJHloSVPNlvJ/hB61RCwVAKPHc1YubQZmvNpSlKUqIACtwH371Tzk/FOKAeR7ibEj
+g+o06QWy7riziG2pDf4lsJCjknnrUrv4TtIe1/ZQ50Q+Fk/TkfzxUpW7ggQ1a7xmbF/aGsKEX83N
+U4IU8wFJZWMbtvBwf04pOieITadOMxXmWRJR6CsD1HHTH2xWx/2irAu9aJTIjJJkQXgsYHJSrg/6
+V5os1rjsynVXOQY8uMsER1t8r+M9j0pSymu1P/J6j+ktatxtE23QtvmwYar3cX0JjyE+hhQ9ROeC
+a0CJJaLTe+Uhfm/l7/YUhWKUxfbKxCztdQkJStWdySf7o/rTHZLC7bW3g5M819Y2pLiPy/TmvLak
+AsSeCPUp7i1hB6h+Ytbnl+US2AfVx/nXyWg4kpeOQ4CPT2FVX0JacS6qWpASnC0qIINDLlKKGyGp
+QaLmADgYA74xzSY7zDpWW4Eq2e0N2yXMdmKS6twlCUO4IQj3+po86RGWzGjtNgO4AATwlPXNAmPK
+dLanH15K04SEE5x7GrsGWLnclJ9SHGuCrOCU+1E2s5zNfSE/7mJniFFciyHJ6XEktoIylWBjPPHv
+SnC1HKlFK25Kls7cBpSvy4PtWwXHSsCXIUqUt15Tg2qStfpx7kUIc0JZIqHlpGwqTgFJxgZzx809
+XfWE22DJgwQD49TGr0pN2nlL7i2JKjvC1DCc9qUtRR47sjLQWiYkYdbX0PyDWwax09bZpcZtpdbl
+FJO5aztJxkD46Vl83TclMT8SlDjh28lIJwfY/NXdDqK8Ag4iGsosYHK8QVKiRIztv/BqccWUhT6l
+jASruBVpEoKkOAYLhJO0D9KGIUoqQ2vucYPaidptb0i6lCMNt8lSlq/N8VRcDblz1J9Tbf4CEGYb
+rzbjiEBLqQQAtQAzUs7jrqnGFNJy0fUMcA/WjlutUySrLT0dLGw5C08hQ6fbNCrTBuVlubjjkJ58
+pJwU5Lef72B1pQMLFYZGY0bHQggS7KYUw35ivUlXU9xSfdCp5QWltSUp/iPfNaBLtv4KGiVOkYcf
+X5imS2dyE9uM8DvjrQc2hyYsg+WGSfSQKxRatfJMLepvXA7iilxtKmlMJcQ4nlSlKzn7U4wbou7Y
+RK9SGeUpzjJPciuLmi5ayDF8t3nsrHFfFx0lcbeSptYWhKUlS0EjBP8ADR2votx5DMSFF1eRjiGF
+OWuK4mO+y2lTyFIWpw5SCeivgZpNuCzBU4zEmBbTnUtq4UP+ZoxaNIXG6So5ebX5C3NillXQd/pV
+zWlmYtEJmEiARLz6XEerf78jrXy3VK4XO4mDsSzbwMYiQI8iQlx5tpa2kfmWBwK4BKVdDiicpq5t
+NGItl1DbbYdUgDgAjO40JZSpxwBA5zVBDnn1EnGD+5rn9n+1pXeZlzcQFIYbCEEjoo9x9galN/hp
+BFn06wwQA89+9cPfJ7fpUpG072zHql2Libtf225NukRX+WnWyhX0Iry9drM3ar2i4XN0h6BKS28r
+O5TiByleD8Yr0ldJyHWtyOD0UKzHW9taloXM8jzkhBbkN4yVt+4HunqPvQXBxkTqH1E2dck2u5wp
+9rUW0yiVPKCdwQgkYJx361pca9NSGG3C5kIR6nkD0g/Ws5uMMT4DJtFyZTCdSlAjlsJKTnHpP+hr
+hapk+yxP2fNW7+DeSrAIyN3uP0qJfQtij8/9lPTlkznmPNwdh3FgILzgcK/3bqSfUfZQpW1BMuNr
+hKeeQlCyrCWeu0DjdXL9oW2NAadjuLbdj4UFBQIWoe6Scg/NEo5cu81h+5JAQtvcgdE++Tmlvr+o
+5YZEbpvstyvRlPSGtFvNJjzox4JKHknHP0pq03c2GlTAp5j8Spw7d5CVEYHANL9xsrTbMibHUCUJ
+IKEt8JPvxSey4ZylLX/8yOSMbqIK67stXwIT0NxyZubSDKUX1lbawkAZ9u+KHXeez5ja3HwhpPxy
+D2HNZu1rG7W5zeqS0EgbUggHA+nvVaNqOXdr5HVNcQhCV71BKQNx7ZzxQxoW7PUIgGcmNs6SqW+W
+2hvdc53qRgkHgc0YsdpVGgluSGygrUdqQClJ+TXVu2sSSu4x3PxD20qDa14yccAe2KruPvNw23Lg
+z+HDytqh1Chjoo9utAJ9LC22h0CqMRc15omyXhCnLc0mLc0c7mcBKiBnCk/PuKy646YvkCU0qLuL
+iWylQUPyE9cH5/WtkRLs0VhTLzqW22sEqLm5xXPTjtV2bLt88sttrCSpQxsOSCPeqGn191ACnyH7
+k27RI/K8TFdFOOYcTcAWENqIcUpJBz23DvTqvWMRElm3uQiUpIQ08BgJV259qdFWjzorsd8RXQ7k
+KJHCh7E9yBWWatszVpmsKRuCRgJTn0g5P9KKt9WrtJYYM+q07IgQGWpsNN/lsTH5W7yF7H22+Nqc
+ZJz84r8sMda284IRztBHal19yRbslgltMjKVA01abvCmLamK6AprbtGeoo1ysKwF5Eao0TsxK9xu
+03BS6hS9gU4DzkUWj26G4osKbSpRysBQJGaE2W822NHDbyngM7s4wM/avmZqdhrelhorSoEbxknn
+5qVtctnEOdLZnkQvKjIhuNojNZyraQMYTx1PtXzeYMZtDS30IS4lQWhWMkH4+tIxvz8GT5iQt1Bz
+vSoHBPbNVjPvGo33HWnSEsgqTgcE9NtMJpWyGJwJ9dQVGOxAGt9QruazbYxQGMAOOjBUo9hn4pf0
+vYiu7AvEKQ0rcQOh9hX47bJMW5qjlrCyohKSoEgfOKboflWmIhhsb5S+Sfk16SsCmsLX1PLWoXsz
+Z2I6QZ3kBKc5dPGPapSw28qMn1q3PK/Mc9PipQ4YVMwyJt2oHV2uZuGVML/mKoKWlwbkHchQ4qkN
+ZaevsQxzcmQsj0byUkH71TgOvRVqbeG6Ks+l5PqSD9RXxBioihqTS8Vm7JlNyHGIqlZWWujDmQQr
+H9339q/bihUVLqVvh1ak7S6g8KHwO1OshQIIUAoHg96z7VdpkxIEw2chTDqTmOr/AOZ90Ht9KWv0
+7WkYMf0Oqr075sXIgLTkZl7Uy1zZCQhpsuDOOuQOa05NvYkS0J8h1UUDd5w5UOOAfisK026yJZj3
+YOR3i56XRzkn+EitUsN4uEvEeCpDCGlEOL67ldMikfk6HUg54Ef02pS9i6jEcLpcGUMLSW9iU43J
+6EjH+VZ9NuLDmQqCIsdxR7e30rQWNPKaebmOTVrdXysq5C+OhFfcm129Y/7ptghJ3JKU8j6VLqtS
+rvmNFNx4mNXGMy6jEQqeUF5V8D2oS63JalpaQdrhxjdyQK2O6Ls8SOGm0hO7ohKeVH2FIl205Pdd
+cmMskrICkNg+pIz0IqrptWGGDwP3M3VhFye4w2hmVGYaUmUUsrwcpOSn5xTpcpUJu1vOmQpwObUK
+S6njfnjjtzWOu6iu3luRnIhQGTtJHBB/pRq1u3G5hhKFlIVneVdz9+lKXaRgdzkCdRxYMg9S9qB+
+A/MS0tpYIVudaZTgOqwAPtUdjTkORXGmhHbKgltKVBJSMd+9Mtv/ABrcWRFLUdxATl0lGFlWOx7/
+AAaEOJhuLZipYdksr6BokraVnnd7VhbOl7xBfWwctnj8T9m39strVFa9aMggZKlK+lLGpXLhc47d
+smsKjlSgpJWg5A65B7dfrWk2vTdus8p+clS1vYyEurB2H+pqs9erVc32zJIbeZXtS2oZO8fH+tap
+sVH3VrnHucXftIeZf/0zdZDYbKlPlpJWVnkZ7D704WLRhTbkOzg6XVpxsB2+Wfr3p0hzIylPPtth
+KEr2uFQxuI7ChV61IhaTGay24okBST0J6GutrLLPACMJY6DxMze/Ldtdzcik7gnlJ+DVJF2KTlVO
+0O2M3WK8mQ0h5/HoIOFdepPalq5aTuapziQhptrPUkHA609VZW3i3cbHyRVfKU03RLishXIpfVqe
+Q2lyJC/dZWQpfzmqF5f/AGdcSw08hwJxnb3V7CqcNl5qWp6U2lKRnYnOefeqlOjQDcw4kX5D5g2Y
+Wn13GOKsQklxR8yU51UecUSt+5GX3vU8rue1CbeypxfnO/YUWB9jRGIHAiVNZc72lgLJVzzUrmg1
+KFiOjjqIwUpPKSR96KWnUl1tLoXCmOt+4CuD9qFlOe9fm3nrT5wexPN5I6msWHxHjzili+Nhlw4A
+faGBn5HSmicCI6X2loeiufkeb5Sf6GvPqknrTJpPVs2wPbMh+EvhxhzlKh9KA1XtYZbM9xj1Laos
+/K1ICHv74/1qnbryuwBtCIYQgDatbayQv5wehpnu8NiXaBebK6X7csgOIPK4yj/Cr49jSbJXwQel
+BesWLseGrsNTbkjx/wBWQ4FvYfdntLW8NwZC8qT9RQ9Gq3bo8ERlBDajgrJ/KPekB1ltLqZCAlK0
+HcCUgjP0NfIuy1Tg+yw2y4kEL8kYSv52nj9KSPxNQ/jyZRr+UYfyGJt+nm7Kje95pflEAFxR6H/C
+DQW+OSocpBjL/EFZOHmzyR7GkzSl9ZLr5uE2LFBOPLWlWSPccYFaxpS8WZlP4aEpDri8OKO4KBP+
+lTL9NZQ/kMxg21agBi3MXo9ulOvB1uC8p0j1LV0PH86JQ7QpiSh94mO3tUFBSeMn2zTsJjKFrde8
+g8DbsIJA78VzbuEd6MVLaSWFZSCUZI985pRnJjCviI2nbncJNzXDUhL7aSU5C8J2/OKcbTaodsU7
+K8hLL6zuUndkA/GaU7tM/ZUlQjBlu3bdzbkdHKTnkE+59qU77q+4zISmGY8lbyVH96hKjlPHHFGG
+me0+HAM7bcmMxv1V/wCQkLFvcdxzktd6RbNDC71lDgbS2dy3F9sHmh8PVF5ZQtEdteFDar0eof0o
+8q7abXHYNxdDEhgYUUnYpffkdxmqFelspGMZz+Io2qQ+51v9/wDw7KkwZflxlElIKgTnPJNcH7mz
+Asjbi1smU8QouE/PBH2pd1DreyOwnojMGPIK8+tLe3HGAfrSE9cVrjtJjFfozwv1bfpnj+VOaf40
+so3DETv+RReF5m53LUNis0Bp9ExK3QkAoQ5nPfisq1druXd3CmMVtsDITlXOPn3pcMGS/HW84VKd
+zwF9SKFKCs7T27U/pvjqaju7Mm6jW2uMdCE4tsukyI5cmY77sdtYSt4DICuoBNMFoWiapJcVhY6o
+V7138N9XK0/JWw42l+BIT5cmMv8AK6jv9COxpi1XpBtE2LctJvfi7bOBdbAI8xrH5krHYj370zaf
+R4gqCQwxzOCMJGE9K6A4rm20ttnDysuJ4OBxmq0uWllv08rNIjyOBPRsCg5GJLnODDZQg+s/yqUs
+zJKlqUVHJNSmkqGOZOt1TBvGfZIxkVwWsg1KlaEmT8DhxX7u3dqlStTka/D3Ur2nrylKkfiIEr9z
+IjK/K4g9fvR/xBsyLDqF+IwsrjqSl5rd1CFjcAfkZqVKHYIZOonyclpZz0oeygoUpWetSpWVmz1O
+c6Ol9o9lDoaBIkPMOZS4obTg4URUqUzWAeDE7SVPEYrXrSZb30ORGwhwDG4rUr/M0SXri+SpYcYu
+EiMMcJbVx9alSgtpad27aMw6ai0pjdKFz1nqJuSn/wAtIJIznj+lfQu11VueVdJm9weohwjNSpWj
+UigYAmfsck8wPPlPKz5jzyz33LJoOt1SieSB7VKlGQQDk5n2w35qwCaYLbEQEBwgY7CpUrlphaAC
+3MIkBKc0DuUUKC5CcJIPI96lSh18GH1AyINiI8x9CM4x3Fat4f6okWOY0qKkFv8AKpCgCFp75qVK
+xqfUY+MUENmMmv7bHbDV5tqPJjTFcsK6pVgE4+Kz68xy41vZUEKPvUqUovDyufKjmfrVmYbiHd6n
+cbis+/WpUqUcMZKdF44n/9k=
+
+------=_NextPart_000_001F_01C8D3B6.F05C5270--
+
diff --git a/groups/test/fixtures/mail_handler/ticket_with_attributes.eml b/groups/test/fixtures/mail_handler/ticket_with_attributes.eml new file mode 100644 index 000000000..118523496 --- /dev/null +++ b/groups/test/fixtures/mail_handler/ticket_with_attributes.eml @@ -0,0 +1,43 @@ +Return-Path: <jsmith@somenet.foo> +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" <jsmith@somenet.foo> +To: <redmine@somenet.foo> +Subject: New ticket on a given project +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature request +category: Stock management +priority: Urgent diff --git a/groups/test/fixtures/members.yml b/groups/test/fixtures/members.yml index 4e0a5a739..186c35506 100644 --- a/groups/test/fixtures/members.yml +++ b/groups/test/fixtures/members.yml @@ -34,7 +34,7 @@ members_005: project_id: 1
role_id: 3
principal_type: Group
- principal_id: 2 # Clients
+ principal_id: 2
members_006:
id: 6
created_on: 2008-01-19 19:35:36 +02:00
@@ -43,4 +43,12 @@ members_006: principal_type: User
principal_id: 7
inherited_from: 5
+members_007:
+ id: 7
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 5
+ role_id: 1
+ principal_type: User
+ principal_id: 2
+ inherited_from:
\ No newline at end of file diff --git a/groups/test/fixtures/projects.yml b/groups/test/fixtures/projects.yml index ad5cf4aa2..8e1b3fe1d 100644 --- a/groups/test/fixtures/projects.yml +++ b/groups/test/fixtures/projects.yml @@ -3,7 +3,7 @@ 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
+ projects_count: 3
id: 1
description: Recipes management application
homepage: http://ecookbook.somenet.foo/
@@ -43,3 +43,15 @@ projects_004: is_public: true
identifier: subproject2
parent_id: 1
+projects_005:
+ created_on: 2006-07-19 19:15:51 +02:00
+ name: Private child of eCookbook
+ updated_on: 2006-07-19 19:17:07 +02:00
+ projects_count: 0
+ id: 5
+ description: This is a private subproject of a public project
+ homepage: ""
+ is_public: false
+ identifier: private_child
+ parent_id: 1
+
\ No newline at end of file diff --git a/groups/test/fixtures/projects_trackers.yml b/groups/test/fixtures/projects_trackers.yml index 8eb7d85ab..31f7f943a 100644 --- a/groups/test/fixtures/projects_trackers.yml +++ b/groups/test/fixtures/projects_trackers.yml @@ -38,3 +38,7 @@ projects_trackers_010: projects_trackers_011: project_id: 4 tracker_id: 2 +projects_trackers_012: + project_id: 1 + tracker_id: 3 +
\ No newline at end of file diff --git a/groups/test/fixtures/repositories/filesystem_repository.tar.gz b/groups/test/fixtures/repositories/filesystem_repository.tar.gz Binary files differnew file mode 100644 index 000000000..7e8a4ac56 --- /dev/null +++ b/groups/test/fixtures/repositories/filesystem_repository.tar.gz diff --git a/groups/test/fixtures/roles.yml b/groups/test/fixtures/roles.yml index 95240e846..cf52db9e0 100644 --- a/groups/test/fixtures/roles.yml +++ b/groups/test/fixtures/roles.yml @@ -15,6 +15,8 @@ manager: - :add_issue_notes
- :move_issues
- :delete_issues
+ - :view_issue_watchers
+ - :add_issue_watchers
- :manage_public_queries
- :save_queries
- :view_gantt
@@ -29,6 +31,7 @@ manager: - :manage_documents
- :view_wiki_pages
- :edit_wiki_pages
+ - :protect_wiki_pages
- :delete_wiki_pages
- :rename_wiki_pages
- :add_messages
@@ -57,6 +60,7 @@ developer: - :add_issue_notes
- :move_issues
- :delete_issues
+ - :view_issue_watchers
- :save_queries
- :view_gantt
- :view_calendar
@@ -69,6 +73,7 @@ developer: - :manage_documents
- :view_wiki_pages
- :edit_wiki_pages
+ - :protect_wiki_pages
- :delete_wiki_pages
- :add_messages
- :manage_boards
@@ -93,6 +98,7 @@ reporter: - :manage_issue_relations
- :add_issue_notes
- :move_issues
+ - :view_issue_watchers
- :save_queries
- :view_gantt
- :view_calendar
diff --git a/groups/test/fixtures/versions.yml b/groups/test/fixtures/versions.yml index bf08660d5..62c5e6f99 100644 --- a/groups/test/fixtures/versions.yml +++ b/groups/test/fixtures/versions.yml @@ -14,7 +14,7 @@ versions_002: updated_on: 2006-07-19 21:00:33 +02:00
id: 2
description: Stable release
- effective_date: 2006-07-19
+ effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
versions_003:
created_on: 2006-07-19 21:00:33 +02:00
name: "2.0"
diff --git a/groups/test/fixtures/watchers.yml b/groups/test/fixtures/watchers.yml new file mode 100644 index 000000000..a8c482955 --- /dev/null +++ b/groups/test/fixtures/watchers.yml @@ -0,0 +1,6 @@ +--- +watchers_001: + watchable_type: Issue + watchable_id: 2 + user_id: 3 +
\ No newline at end of file diff --git a/groups/test/fixtures/wiki_contents.yml b/groups/test/fixtures/wiki_contents.yml index 5d6d3f1de..8c53d4d97 100644 --- a/groups/test/fixtures/wiki_contents.yml +++ b/groups/test/fixtures/wiki_contents.yml @@ -2,7 +2,7 @@ wiki_contents_001:
text: |-
h1. CookBook documentation
-
+ {{child_pages}}
Some updated [[documentation]] here with gzipped history
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
diff --git a/groups/test/fixtures/wiki_pages.yml b/groups/test/fixtures/wiki_pages.yml index f89832e44..e285441ff 100644 --- a/groups/test/fixtures/wiki_pages.yml +++ b/groups/test/fixtures/wiki_pages.yml @@ -4,19 +4,27 @@ wiki_pages_001: title: CookBook_documentation
id: 1
wiki_id: 1
+ protected: true
+ parent_id:
wiki_pages_002:
created_on: 2007-03-08 00:18:07 +01:00
title: Another_page
id: 2
wiki_id: 1
+ protected: false
+ parent_id:
wiki_pages_003:
created_on: 2007-03-08 00:18:07 +01:00
title: Start_page
id: 3
wiki_id: 2
+ protected: false
+ parent_id:
wiki_pages_004:
created_on: 2007-03-08 00:18:07 +01:00
title: Page_with_an_inline_image
id: 4
wiki_id: 1
+ protected: false
+ parent_id: 1
\ No newline at end of file diff --git a/groups/test/functional/account_controller_test.rb b/groups/test/functional/account_controller_test.rb index 666acf0dd..26218d177 100644 --- a/groups/test/functional/account_controller_test.rb +++ b/groups/test/functional/account_controller_test.rb @@ -44,6 +44,17 @@ class AccountControllerTest < Test::Unit::TestCase assert_nil assigns(:user) end + def test_login_should_redirect_to_back_url_param + # request.uri is "test.host" in test environment + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1' + assert_redirected_to '/issues/show/1' + end + + def test_login_should_not_redirect_to_another_host + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake' + assert_redirected_to '/my/page' + end + def test_login_with_wrong_password post :login, :username => 'admin', :password => 'bad' assert_response :success diff --git a/groups/test/functional/attachments_controller_test.rb b/groups/test/functional/attachments_controller_test.rb new file mode 100644 index 000000000..06a6343ba --- /dev/null +++ b/groups/test/functional/attachments_controller_test.rb @@ -0,0 +1,79 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'attachments_controller' + +# Re-raise errors caught by the controller. +class AttachmentsController; def rescue_action(e) raise e end; end + + +class AttachmentsControllerTest < Test::Unit::TestCase + fixtures :users, :projects, :issues, :attachments + + def setup + @controller = AttachmentsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files" + User.current = nil + end + + def test_routing + assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1') + assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext') + assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1') + assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext') + end + + def test_recognizes + assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1') + assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1') + assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext') + assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1') + assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext') + end + + def test_show_diff + get :show, :id => 5 + assert_response :success + assert_template 'diff' + end + + def test_show_text_file + get :show, :id => 4 + assert_response :success + assert_template 'file' + end + + def test_show_other + get :show, :id => 6 + assert_response :success + assert_equal 'application/octet-stream', @response.content_type + end + + def test_download_text_file + get :download, :id => 4 + assert_response :success + assert_equal 'application/x-ruby', @response.content_type + end + + def test_anonymous_on_private_private + get :download, :id => 7 + assert_redirected_to 'account/login' + end +end diff --git a/groups/test/functional/documents_controller_test.rb b/groups/test/functional/documents_controller_test.rb index f150a5b7a..7c1f0213a 100644 --- a/groups/test/functional/documents_controller_test.rb +++ b/groups/test/functional/documents_controller_test.rb @@ -40,6 +40,8 @@ class DocumentsControllerTest < Test::Unit::TestCase def test_new_with_one_attachment @request.session[:user_id] = 2 + set_tmp_attachments_directory + post :new, :project_id => 'ecookbook', :document => { :title => 'DocumentsControllerTest#test_post_new', :description => 'This is a new document', diff --git a/groups/test/functional/enumerations_controller.rb b/groups/test/functional/enumerations_controller.rb new file mode 100644 index 000000000..36ff86720 --- /dev/null +++ b/groups/test/functional/enumerations_controller.rb @@ -0,0 +1,61 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'enumerations_controller' + +# Re-raise errors caught by the controller. +class EnumerationsController; def rescue_action(e) raise e end; end + +class EnumerationsControllerTest < Test::Unit::TestCase + fixtures :enumerations, :issues, :users + + def setup + @controller = EnumerationsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'list' + end + + def test_destroy_enumeration_not_in_use + post :destroy, :id => 7 + assert_redirected_to :controller => 'enumerations', :action => 'index' + assert_nil Enumeration.find_by_id(7) + end + + def test_destroy_enumeration_in_use + post :destroy, :id => 4 + assert_response :success + assert_template 'destroy' + assert_not_nil Enumeration.find_by_id(4) + end + + def test_destroy_enumeration_in_use_with_reassignment + issue = Issue.find(:first, :conditions => {:priority_id => 4}) + post :destroy, :id => 4, :reassign_to_id => 6 + assert_redirected_to :controller => 'enumerations', :action => 'index' + assert_nil Enumeration.find_by_id(4) + # check that the issue was reassign + assert_equal 6, issue.reload.priority_id + end +end diff --git a/groups/test/functional/issues_controller_test.rb b/groups/test/functional/issues_controller_test.rb index 042a8f3f2..a248d8bde 100644 --- a/groups/test/functional/issues_controller_test.rb +++ b/groups/test/functional/issues_controller_test.rb @@ -38,7 +38,9 @@ class IssuesControllerTest < Test::Unit::TestCase :custom_fields, :custom_values, :custom_fields_trackers, - :time_entries + :time_entries, + :journals, + :journal_details def setup @controller = IssuesController.new @@ -53,13 +55,44 @@ class IssuesControllerTest < Test::Unit::TestCase assert_template 'index.rhtml' assert_not_nil assigns(:issues) assert_nil assigns(:project) + assert_tag :tag => 'a', :content => /Can't print recipes/ + assert_tag :tag => 'a', :content => /Subproject issue/ + # private projects hidden + assert_no_tag :tag => 'a', :content => /Issue of a private subproject/ + assert_no_tag :tag => 'a', :content => /Issue on project 2/ end def test_index_with_project + Setting.display_subprojects_issues = 0 get :index, :project_id => 1 assert_response :success assert_template 'index.rhtml' assert_not_nil assigns(:issues) + assert_tag :tag => 'a', :content => /Can't print recipes/ + assert_no_tag :tag => 'a', :content => /Subproject issue/ + end + + def test_index_with_project_and_subprojects + Setting.display_subprojects_issues = 1 + get :index, :project_id => 1 + assert_response :success + assert_template 'index.rhtml' + assert_not_nil assigns(:issues) + assert_tag :tag => 'a', :content => /Can't print recipes/ + assert_tag :tag => 'a', :content => /Subproject issue/ + assert_no_tag :tag => 'a', :content => /Issue of a private subproject/ + end + + def test_index_with_project_and_subprojects_should_show_private_subprojects + @request.session[:user_id] = 2 + Setting.display_subprojects_issues = 1 + get :index, :project_id => 1 + assert_response :success + assert_template 'index.rhtml' + assert_not_nil assigns(:issues) + assert_tag :tag => 'a', :content => /Can't print recipes/ + assert_tag :tag => 'a', :content => /Subproject issue/ + assert_tag :tag => 'a', :content => /Issue of a private subproject/ end def test_index_with_project_and_filter @@ -137,7 +170,7 @@ class IssuesControllerTest < Test::Unit::TestCase assert_response :success assert_template 'new' - assert_tag :tag => 'input', :attributes => { :name => 'custom_fields[2]', + assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]', :value => 'Default string' } end @@ -166,17 +199,20 @@ class IssuesControllerTest < Test::Unit::TestCase def test_post_new @request.session[:user_id] = 2 post :new, :project_id => 1, - :issue => {:tracker_id => 1, + :issue => {:tracker_id => 3, :subject => 'This is the test_new issue', :description => 'This is the description', - :priority_id => 5}, - :custom_fields => {'2' => 'Value for field 2'} + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'}} assert_redirected_to 'issues/show' issue = Issue.find_by_subject('This is the test_new issue') assert_not_nil issue assert_equal 2, issue.author_id - v = issue.custom_values.find_by_custom_field_id(2) + assert_equal 3, issue.tracker_id + assert_nil issue.estimated_hours + v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2}) assert_not_nil v assert_equal 'Value for field 2', v.value end @@ -191,6 +227,50 @@ class IssuesControllerTest < Test::Unit::TestCase assert_redirected_to 'issues/show' end + def test_post_new_with_required_custom_field_and_without_custom_fields_param + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + @request.session[:user_id] = 2 + post :new, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_template 'new' + issue = assigns(:issue) + assert_not_nil issue + assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values) + end + + def test_post_should_preserve_fields_values_on_validation_failure + @request.session[:user_id] = 2 + post :new, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + # empty description + :description => '', + :priority_id => 6, + :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}} + assert_response :success + assert_template 'new' + + assert_tag :input, :attributes => { :name => 'issue[subject]', + :value => 'This is the test_new issue' } + assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, + :child => { :tag => 'option', :attributes => { :selected => 'selected', + :value => '6' }, + :content => 'High' } + # Custom fields + assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' }, + :child => { :tag => 'option', :attributes => { :selected => 'selected', + :value => 'Oracle' }, + :content => 'Oracle' } + assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]', + :value => 'Value for field 2'} + end + def test_copy_issue @request.session[:user_id] = 2 get :new, :project_id => 1, :copy_from => 1 @@ -230,19 +310,43 @@ class IssuesControllerTest < Test::Unit::TestCase :content => 'Urgent', :attributes => { :selected => 'selected' } } end + + def test_reply_to_issue + @request.session[:user_id] = 2 + get :reply, :id => 1 + assert_response :success + assert_select_rjs :show, "update" + end + + def test_reply_to_note + @request.session[:user_id] = 2 + get :reply, :id => 1, :journal_id => 2 + assert_response :success + assert_select_rjs :show, "update" + end - def test_post_edit + def test_post_edit_without_custom_fields_param @request.session[:user_id] = 2 ActionMailer::Base.deliveries.clear issue = Issue.find(1) + assert_equal '125', issue.custom_value_for(2).value old_subject = issue.subject new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' - post :edit, :id => 1, :issue => {:subject => new_subject} + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 2) do + post :edit, :id => 1, :issue => {:subject => new_subject, + :priority_id => '6', + :category_id => '1' # no change + } + end + end assert_redirected_to 'issues/show/1' issue.reload assert_equal new_subject, issue.subject + # Make sure custom fields were not cleared + assert_equal '125', issue.custom_value_for(2).value mail = ActionMailer::Base.deliveries.last assert_kind_of TMail::Mail, mail @@ -250,14 +354,40 @@ class IssuesControllerTest < Test::Unit::TestCase assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}") end + def test_post_edit_with_custom_field_change + @request.session[:user_id] = 2 + issue = Issue.find(1) + assert_equal '125', issue.custom_value_for(2).value + + assert_difference('Journal.count') do + assert_difference('JournalDetail.count', 3) do + post :edit, :id => 1, :issue => {:subject => 'Custom field change', + :priority_id => '6', + :category_id => '1', # no change + :custom_field_values => { '2' => 'New custom value' } + } + end + end + assert_redirected_to 'issues/show/1' + issue.reload + assert_equal 'New custom value', issue.custom_value_for(2).value + + mail = ActionMailer::Base.deliveries.last + assert_kind_of TMail::Mail, mail + assert mail.body.include?("Searchable field changed from 125 to New custom value") + end + def test_post_edit_with_status_and_assignee_change issue = Issue.find(1) assert_equal 1, issue.status_id @request.session[:user_id] = 2 - post :edit, - :id => 1, - :issue => { :status_id => 2, :assigned_to_id => 3 }, - :notes => 'Assigned to dlopper' + assert_difference('TimeEntry.count', 0) do + post :edit, + :id => 1, + :issue => { :status_id => 2, :assigned_to_id => 3 }, + :notes => 'Assigned to dlopper', + :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } + end assert_redirected_to 'issues/show/1' issue.reload assert_equal 2, issue.status_id @@ -288,10 +418,12 @@ class IssuesControllerTest < Test::Unit::TestCase def test_post_edit_with_note_and_spent_time @request.session[:user_id] = 2 spent_hours_before = Issue.find(1).spent_hours - post :edit, - :id => 1, - :notes => '2.5 hours added', - :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } + assert_difference('TimeEntry.count') do + post :edit, + :id => 1, + :notes => '2.5 hours added', + :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } + end assert_redirected_to 'issues/show/1' issue = Issue.find(1) @@ -307,6 +439,8 @@ class IssuesControllerTest < Test::Unit::TestCase end def test_post_edit_with_attachment_only + set_tmp_attachments_directory + # anonymous user post :edit, :id => 1, @@ -398,10 +532,10 @@ class IssuesControllerTest < Test::Unit::TestCase :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5', :class => '' } assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&priority_id=8', :class => '' } assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3', + :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1', :class => '' } assert_tag :tag => 'a', :content => 'Copy', :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1', @@ -431,6 +565,12 @@ class IssuesControllerTest < Test::Unit::TestCase assert_tag :tag => 'a', :content => 'Edit', :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2', :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&priority_id=8', + :class => '' } + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1&ids%5B%5D=2', + :class => '' } assert_tag :tag => 'a', :content => 'Move', :attributes => { :href => '/issues/move?ids%5B%5D=1&ids%5B%5D=2', :class => 'icon-move' } diff --git a/groups/test/functional/mail_handler_controller_test.rb b/groups/test/functional/mail_handler_controller_test.rb new file mode 100644 index 000000000..6c5af23f0 --- /dev/null +++ b/groups/test/functional/mail_handler_controller_test.rb @@ -0,0 +1,53 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'mail_handler_controller' + +# Re-raise errors caught by the controller. +class MailHandlerController; def rescue_action(e) raise e end; end + +class MailHandlerControllerTest < Test::Unit::TestCase + fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :issue_statuses, :trackers, :enumerations + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' + + def setup + @controller = MailHandlerController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_should_create_issue + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + assert_response 201 + end + + def test_should_not_allow + # Disable API + Setting.mail_handler_api_enabled = 0 + Setting.mail_handler_api_key = 'secret' + + post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) + assert_response 403 + end +end diff --git a/groups/test/functional/messages_controller_test.rb b/groups/test/functional/messages_controller_test.rb index 1fe8d086a..b1b3ea942 100644 --- a/groups/test/functional/messages_controller_test.rb +++ b/groups/test/functional/messages_controller_test.rb @@ -40,6 +40,15 @@ class MessagesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:topic) end + def test_show_with_reply_permission + @request.session[:user_id] = 2 + get :show, :board_id => 1, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :div, :attributes => { :id => 'reply' }, + :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } } + end + def test_show_message_not_found get :show, :board_id => 1, :id => 99999 assert_response 404 @@ -108,4 +117,11 @@ class MessagesControllerTest < Test::Unit::TestCase assert_redirected_to 'boards/show' assert_nil Message.find_by_id(1) end + + def test_quote + @request.session[:user_id] = 2 + xhr :get, :quote, :board_id => 1, :id => 3 + assert_response :success + assert_select_rjs :show, 'reply' + end end diff --git a/groups/test/functional/projects_controller_test.rb b/groups/test/functional/projects_controller_test.rb index eb5795152..03773ccdb 100644 --- a/groups/test/functional/projects_controller_test.rb +++ b/groups/test/functional/projects_controller_test.rb @@ -29,24 +29,27 @@ class ProjectsControllerTest < Test::Unit::TestCase @controller = ProjectsController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new + @request.session[:user_id] = nil 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_template 'index' assert_not_nil assigns(:project_tree) # Root project as hash key - assert assigns(:project_tree).has_key?(Project.find(1)) + assert assigns(:project_tree).keys.include?(Project.find(1)) # Subproject in corresponding value assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) end
+ + def test_index_atom + get :index, :format => 'atom' + assert_response :success + assert_template 'common/feed.atom.rxml' + assert_select 'feed>title', :text => 'Redmine: Latest projects' + assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current)) + end def test_show_by_id
get :show, :id => 1
@@ -63,6 +66,21 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) end + def test_private_subprojects_hidden + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_no_tag :tag => 'a', :content => /Private child/ + end + + def test_private_subprojects_visible + @request.session[:user_id] = 2 # manager who is a member of the private subproject + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_tag :tag => 'a', :content => /Private child/ + end + def test_settings @request.session[:user_id] = 2 # manager get :settings, :id => 1 @@ -73,7 +91,7 @@ class ProjectsControllerTest < Test::Unit::TestCase def test_edit @request.session[:user_id] = 2 # manager post :edit, :id => 1, :project => {:name => 'Test changed name', - :custom_field_ids => ['']} + :issue_custom_field_ids => ['']} assert_redirected_to 'projects/settings/ecookbook' project = Project.find(1) assert_equal 'Test changed name', project.name @@ -135,22 +153,20 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'activity' assert_not_nil assigns(:events_by_day) - assert_not_nil assigns(:events) - - # subproject issue not included by default - assert !assigns(:events).include?(Issue.find(5)) assert_tag :tag => "h3", :content => /#{2.days.ago.to_date.day}/, :sibling => { :tag => "dl", :child => { :tag => "dt", - :attributes => { :class => 'issue-edit' }, + :attributes => { :class => /issue-edit/ }, :child => { :tag => "a", :content => /(#{IssueStatus.find(2).name})/, } } } - + end + + def test_previous_project_activity get :activity, :id => 1, :from => 3.days.ago.to_date assert_response :success assert_template 'activity' @@ -160,7 +176,7 @@ class ProjectsControllerTest < Test::Unit::TestCase :content => /#{3.day.ago.to_date.day}/, :sibling => { :tag => "dl", :child => { :tag => "dt", - :attributes => { :class => 'issue' }, + :attributes => { :class => /issue/ }, :child => { :tag => "a", :content => /#{Issue.find(1).subject}/, } @@ -168,53 +184,30 @@ class ProjectsControllerTest < Test::Unit::TestCase } end - def test_activity_with_subprojects - get :activity, :id => 1, :with_subprojects => 1 - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events) - - assert assigns(:events).include?(Issue.find(1)) - assert !assigns(:events).include?(Issue.find(4)) - # subproject issue - assert assigns(:events).include?(Issue.find(5)) - end - - def test_global_activity_anonymous + def test_global_activity get :activity assert_response :success assert_template 'activity' - assert_not_nil assigns(:events) + assert_not_nil assigns(:events_by_day) - assert assigns(:events).include?(Issue.find(1)) - # Issue of a private project - assert !assigns(:events).include?(Issue.find(4)) + assert_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(5).subject}/, + } + } + } end - def test_global_activity_logged_user - @request.session[:user_id] = 2 # manager - get :activity + def test_activity_atom_feed + get :activity, :format => 'atom' assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events) - - assert assigns(:events).include?(Issue.find(1)) - # Issue of a private project the user belongs to - assert assigns(:events).include?(Issue.find(4)) + assert_template 'common/feed.atom.rxml' end - - def test_global_activity_with_all_types - get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1 - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events) - - assert assigns(:events).include?(Issue.find(1)) - assert !assigns(:events).include?(Issue.find(4)) - assert assigns(:events).include?(Message.find(5)) - end - def test_calendar get :calendar, :id => 1 assert_response :success @@ -222,27 +215,56 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_not_nil assigns(:calendar) end - def test_calendar_with_subprojects + def test_calendar_with_subprojects_should_not_show_private_subprojects + get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + assert_no_tag :tag => 'a', :content => /#6/ + end + + def test_calendar_with_subprojects_should_show_private_subprojects + @request.session[:user_id] = 2 get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] assert_response :success assert_template 'calendar' assert_not_nil assigns(:calendar) + assert_tag :tag => 'a', :content => /#6/ end def test_gantt get :gantt, :id => 1 assert_response :success assert_template 'gantt.rhtml' - assert_not_nil assigns(:events) + events = assigns(:events) + assert_not_nil events + # Issue with start and due dates + i = Issue.find(1) + assert_not_nil i.due_date + assert events.include?(Issue.find(1)) + # Issue with without due date but targeted to a version with date + i = Issue.find(2) + assert_nil i.due_date + assert events.include?(i) end - def test_gantt_with_subprojects + def test_gantt_with_subprojects_should_not_show_private_subprojects get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] assert_response :success assert_template 'gantt.rhtml' assert_not_nil assigns(:events) + assert_no_tag :tag => 'a', :content => /#6/ end + def test_gantt_with_subprojects_should_show_private_subprojects + @request.session[:user_id] = 2 + get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] + assert_response :success + assert_template 'gantt.rhtml' + assert_not_nil assigns(:events) + assert_tag :tag => 'a', :content => /#6/ + end + def test_gantt_export_to_pdf get :gantt, :id => 1, :format => 'pdf' assert_response :success @@ -265,4 +287,33 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_redirected_to 'admin/projects' assert Project.find(1).active? end + + def test_project_menu + assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do + Redmine::MenuManager.map :project_menu do |menu| + menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo' + menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity + menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar + end + + get :show, :id => 1 + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo' } } + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar' }, + :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } } + + assert_tag :div, :attributes => { :id => 'main-menu' }, + :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' }, + :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } } + + # Remove the menu items + Redmine::MenuManager.map :project_menu do |menu| + menu.delete :foo + menu.delete :bar + menu.delete :hello + end + end + end end diff --git a/groups/test/functional/repositories_controller_test.rb b/groups/test/functional/repositories_controller_test.rb index 47455dc55..2892f3bd1 100644 --- a/groups/test/functional/repositories_controller_test.rb +++ b/groups/test/functional/repositories_controller_test.rb @@ -43,10 +43,10 @@ class RepositoriesControllerTest < Test::Unit::TestCase assert_response :success assert_template 'revision' assert_no_tag :tag => "div", :attributes => { :class => "contextual" }, - :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=0'} + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook/0'} } assert_tag :tag => "div", :attributes => { :class => "contextual" }, - :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=2'} + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook/2'} } end diff --git a/groups/test/functional/repositories_cvs_controller_test.rb b/groups/test/functional/repositories_cvs_controller_test.rb index e12bb53ac..2207d6ab6 100644 --- a/groups/test/functional/repositories_cvs_controller_test.rb +++ b/groups/test/functional/repositories_cvs_controller_test.rb @@ -25,7 +25,7 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase # No '..' in the repository path REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository' - REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/ + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? # CVS module MODULE_NAME = 'test' @@ -89,6 +89,19 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'] assert_response :success assert_template 'entry' + assert_no_tag :tag => 'td', :attributes => { :class => /line-code/}, + :content => /before_filter/ + end + + def test_entry_at_given_revision + # changesets must be loaded + Project.find(1).repository.fetch_changesets + get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :rev => 2 + assert_response :success + assert_template 'entry' + # this line was removed in r3 + assert_tag :tag => 'td', :attributes => { :class => /line-code/}, + :content => /before_filter/ end def test_entry_not_found diff --git a/groups/test/functional/repositories_git_controller_test.rb b/groups/test/functional/repositories_git_controller_test.rb index 339e22897..201a50677 100644 --- a/groups/test/functional/repositories_git_controller_test.rb +++ b/groups/test/functional/repositories_git_controller_test.rb @@ -26,7 +26,7 @@ class RepositoriesGitControllerTest < Test::Unit::TestCase # No '..' in the repository path REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository' - REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/ + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? def setup @controller = RepositoriesController.new diff --git a/groups/test/functional/repositories_subversion_controller_test.rb b/groups/test/functional/repositories_subversion_controller_test.rb index dd56947fc..35dfbb1a1 100644 --- a/groups/test/functional/repositories_subversion_controller_test.rb +++ b/groups/test/functional/repositories_subversion_controller_test.rb @@ -71,6 +71,19 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_not_nil assigns(:entries) assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name) end + + def test_changes + get :changes, :id => 1, :path => ['subversion_test', 'folder', 'helloworld.rb' ] + assert_response :success + assert_template 'changes' + # svn properties + assert_not_nil assigns(:properties) + assert_equal 'native', assigns(:properties)['svn:eol-style'] + assert_tag :ul, + :child => { :tag => 'li', + :child => { :tag => 'b', :content => 'svn:eol-style' }, + :child => { :tag => 'span', :content => 'native' } } + end def test_entry get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'] @@ -78,6 +91,15 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_template 'entry' end + def test_entry_at_given_revision + get :entry, :id => 1, :path => ['subversion_test', 'helloworld.rb'], :rev => 2 + assert_response :success + assert_template 'entry' + # this line was removed in r3 and file was moved in r6 + assert_tag :tag => 'td', :attributes => { :class => /line-code/}, + :content => /Here's the code/ + end + def test_entry_not_found get :entry, :id => 1, :path => ['subversion_test', 'zzz.c'] assert_tag :tag => 'div', :attributes => { :class => /error/ }, @@ -97,6 +119,37 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_equal 'folder', assigns(:entry).name end + def test_revision + get :revision, :id => 1, :rev => 2 + assert_response :success + assert_template 'revision' + assert_tag :tag => 'tr', + :child => { :tag => 'td', + # link to the entry at rev 2 + :child => { :tag => 'a', :attributes => {:href => 'repositories/entry/ecookbook/test/some/path/in/the/repo?rev=2'}, + :content => %r{/test/some/path/in/the/repo} } + }, + :child => { :tag => 'td', + # link to partial diff + :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/test/some/path/in/the/repo?rev=2' } } + } + end + + def test_revision_with_repository_pointing_to_a_subdirectory + r = Project.find(1).repository + # Changes repository url to a subdirectory + r.update_attribute :url, (r.url + '/test/some') + + get :revision, :id => 1, :rev => 2 + assert_response :success + assert_template 'revision' + assert_tag :tag => 'tr', + :child => { :tag => 'td', :content => %r{/test/some/path/in/the/repo} }, + :child => { :tag => 'td', + :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/path/in/the/repo?rev=2' } } + } + end + def test_diff get :diff, :id => 1, :rev => 3 assert_response :success diff --git a/groups/test/functional/search_controller_test.rb b/groups/test/functional/search_controller_test.rb index 49004c7e6..ce06ec298 100644 --- a/groups/test/functional/search_controller_test.rb +++ b/groups/test/functional/search_controller_test.rb @@ -5,7 +5,10 @@ require 'search_controller' class SearchController; def rescue_action(e) raise e end; end class SearchControllerTest < Test::Unit::TestCase - fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values + fixtures :projects, :enabled_modules, :roles, :users, + :issues, :trackers, :issue_statuses, + :custom_fields, :custom_values, + :repositories, :changesets def setup @controller = SearchController.new @@ -25,6 +28,31 @@ class SearchControllerTest < Test::Unit::TestCase assert assigns(:results).include?(Project.find(1)) end + def test_search_all_projects + get :index, :q => 'recipe subproject commit', :submit => 'Search' + assert_response :success + assert_template 'index' + + assert assigns(:results).include?(Issue.find(2)) + assert assigns(:results).include?(Issue.find(5)) + assert assigns(:results).include?(Changeset.find(101)) + assert_tag :dt, :attributes => { :class => /issue/ }, + :child => { :tag => 'a', :content => /Add ingredients categories/ }, + :sibling => { :tag => 'dd', :content => /should be classified by categories/ } + + assert assigns(:results_by_type).is_a?(Hash) + assert_equal 4, assigns(:results_by_type)['changesets'] + assert_tag :a, :content => 'Changesets (4)' + end + + def test_search_project_and_subprojects + get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :submit => 'Search' + assert_response :success + assert_template 'index' + assert assigns(:results).include?(Issue.find(1)) + assert assigns(:results).include?(Issue.find(5)) + end + def test_search_without_searchable_custom_fields CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" diff --git a/groups/test/functional/timelog_controller_test.rb b/groups/test/functional/timelog_controller_test.rb index e80a67728..7b4622daa 100644 --- a/groups/test/functional/timelog_controller_test.rb +++ b/groups/test/functional/timelog_controller_test.rb @@ -30,11 +30,22 @@ class TimelogControllerTest < Test::Unit::TestCase @response = ActionController::TestResponse.new end - def test_create + def test_get_edit + @request.session[:user_id] = 3 + get :edit, :project_id => 1 + assert_response :success + assert_template 'edit' + # Default activity selected + assert_tag :tag => 'option', :attributes => { :selected => 'selected' }, + :content => 'Development' + end + + def test_post_edit @request.session[:user_id] = 3 post :edit, :project_id => 1, :time_entry => {:comments => 'Some work on TimelogControllerTest', - :activity_id => '10', + # Not the default activity + :activity_id => '11', :spent_on => '2008-03-14', :issue_id => '1', :hours => '7.3'} @@ -43,6 +54,7 @@ class TimelogControllerTest < Test::Unit::TestCase i = Issue.find(1) t = TimeEntry.find_by_comments('Some work on TimelogControllerTest') assert_not_nil t + assert_equal 11, t.activity_id assert_equal 7.3, t.hours assert_equal 3, t.user_id assert_equal i, t.issue @@ -198,6 +210,14 @@ class TimelogControllerTest < Test::Unit::TestCase assert_equal '2007-04-22'.to_date, assigns(:to) end + def test_details_atom_feed + get :details, :project_id => 1, :format => 'atom' + assert_response :success + assert_equal 'application/atom+xml', @response.content_type + assert_not_nil assigns(:items) + assert assigns(:items).first.is_a?(TimeEntry) + end + def test_details_csv_export get :details, :project_id => 1, :format => 'csv' assert_response :success diff --git a/groups/test/functional/versions_controller_test.rb b/groups/test/functional/versions_controller_test.rb index 3477c5edd..3a118701a 100644 --- a/groups/test/functional/versions_controller_test.rb +++ b/groups/test/functional/versions_controller_test.rb @@ -22,7 +22,7 @@ require 'versions_controller' class VersionsController; def rescue_action(e) raise e end; end class VersionsControllerTest < Test::Unit::TestCase - fixtures :projects, :versions, :users, :roles, :members, :enabled_modules + fixtures :projects, :versions, :issues, :users, :roles, :members, :enabled_modules def setup @controller = VersionsController.new @@ -60,9 +60,9 @@ class VersionsControllerTest < Test::Unit::TestCase def test_destroy @request.session[:user_id] = 2 - post :destroy, :id => 2 + post :destroy, :id => 3 assert_redirected_to 'projects/settings/ecookbook' - assert_nil Version.find_by_id(2) + assert_nil Version.find_by_id(3) end def test_issue_status_by diff --git a/groups/test/functional/watchers_controller_test.rb b/groups/test/functional/watchers_controller_test.rb new file mode 100644 index 000000000..cd6539410 --- /dev/null +++ b/groups/test/functional/watchers_controller_test.rb @@ -0,0 +1,70 @@ +# Redmine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'watchers_controller' + +# Re-raise errors caught by the controller. +class WatchersController; def rescue_action(e) raise e end; end + +class WatchersControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :enabled_modules, + :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers + + def setup + @controller = WatchersController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_get_watch_should_be_invalid + @request.session[:user_id] = 3 + get :watch, :object_type => 'issue', :object_id => '1' + assert_response 405 + end + + def test_watch + @request.session[:user_id] = 3 + assert_difference('Watcher.count') do + xhr :post, :watch, :object_type => 'issue', :object_id => '1' + assert_response :success + assert_select_rjs :replace_html, 'watcher' + end + assert Issue.find(1).watched_by?(User.find(3)) + end + + def test_unwatch + @request.session[:user_id] = 3 + assert_difference('Watcher.count', -1) do + xhr :post, :unwatch, :object_type => 'issue', :object_id => '2' + assert_response :success + assert_select_rjs :replace_html, 'watcher' + end + assert !Issue.find(1).watched_by?(User.find(3)) + end + + def test_new_watcher + @request.session[:user_id] = 2 + assert_difference('Watcher.count') do + xhr :post, :new, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'} + assert_response :success + assert_select_rjs :replace_html, 'watchers' + end + assert Issue.find(2).watched_by?(User.find(4)) + end +end diff --git a/groups/test/functional/welcome_controller_test.rb b/groups/test/functional/welcome_controller_test.rb index 18146c6aa..df565a751 100644 --- a/groups/test/functional/welcome_controller_test.rb +++ b/groups/test/functional/welcome_controller_test.rb @@ -46,4 +46,18 @@ class WelcomeControllerTest < Test::Unit::TestCase get :index assert_equal :fr, @controller.current_language end + + def test_browser_language_alternate + Setting.default_language = 'en' + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-TW' + get :index + assert_equal :"zh-tw", @controller.current_language + end + + def test_browser_language_alternate_not_valid + Setting.default_language = 'en' + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr-CA' + get :index + assert_equal :fr, @controller.current_language + end end diff --git a/groups/test/functional/wiki_controller_test.rb b/groups/test/functional/wiki_controller_test.rb index bf31e6614..b5325357c 100644 --- a/groups/test/functional/wiki_controller_test.rb +++ b/groups/test/functional/wiki_controller_test.rb @@ -32,10 +32,16 @@ class WikiControllerTest < Test::Unit::TestCase end def test_show_start_page - get :index, :id => 1 + get :index, :id => 'ecookbook' assert_response :success assert_template 'show' assert_tag :tag => 'h1', :content => /CookBook documentation/ + + # child_pages macro + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } end def test_show_page_with_name @@ -86,14 +92,35 @@ class WikiControllerTest < Test::Unit::TestCase assert_tag :tag => 'strong', :content => /previewed text/ end + def test_preview_new_page + @request.session[:user_id] = 2 + xhr :post, :preview, :id => 1, :page => 'New page', + :content => { :text => 'h1. New page', + :comments => '', + :version => 0 } + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'h1', :content => /New page/ + end + def test_history get :history, :id => 1, :page => 'CookBook_documentation' assert_response :success assert_template 'history' assert_not_nil assigns(:versions) assert_equal 3, assigns(:versions).size + assert_select "input[type=submit][name=commit]" end - + + def test_history_with_one_version + get :history, :id => 1, :page => 'Another_page' + assert_response :success + assert_template 'history' + assert_not_nil assigns(:versions) + assert_equal 1, assigns(:versions).size + assert_select "input[type=submit][name=commit]", false + end + def test_diff get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1 assert_response :success @@ -152,12 +179,76 @@ class WikiControllerTest < Test::Unit::TestCase pages = assigns(:pages) assert_not_nil pages assert_equal Project.find(1).wiki.pages.size, pages.size - assert_tag :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, - :content => /CookBook documentation/ + + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, + :content => 'CookBook documentation' }, + :child => { :tag => 'ul', + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } } }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Another_page' }, + :content => 'Another page' } } end def test_not_found get :index, :id => 999 assert_response 404 end + + def test_protect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page') + assert !page.protected? + @request.session[:user_id] = 2 + post :protect, :id => 1, :page => page.title, :protected => '1' + assert_redirected_to 'wiki/ecookbook/Another_page' + assert page.reload.protected? + end + + def test_unprotect_page + page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation') + assert page.protected? + @request.session[:user_id] = 2 + post :protect, :id => 1, :page => page.title, :protected => '0' + assert_redirected_to 'wiki/ecookbook' + assert !page.reload.protected? + end + + def test_show_page_with_edit_link + @request.session[:user_id] = 2 + get :index, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' } + end + + def test_show_page_without_edit_link + @request.session[:user_id] = 4 + get :index, :id => 1 + assert_response :success + assert_template 'show' + assert_no_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' } + end + + def test_edit_unprotected_page + # Non members can edit unprotected wiki pages + @request.session[:user_id] = 4 + get :edit, :id => 1, :page => 'Another_page' + assert_response :success + assert_template 'edit' + end + + def test_edit_protected_page_by_nonmember + # Non members can't edit protected wiki pages + @request.session[:user_id] = 4 + get :edit, :id => 1, :page => 'CookBook_documentation' + assert_response 403 + end + + def test_edit_protected_page_by_member + @request.session[:user_id] = 2 + get :edit, :id => 1, :page => 'CookBook_documentation' + assert_response :success + assert_template 'edit' + end end diff --git a/groups/test/integration/account_test.rb b/groups/test/integration/account_test.rb index e9d665d19..c349200d3 100644 --- a/groups/test/integration/account_test.rb +++ b/groups/test/integration/account_test.rb @@ -17,6 +17,12 @@ require "#{File.dirname(__FILE__)}/../test_helper" +begin + require 'mocha' +rescue + # Won't run some tests +end + class AccountTest < ActionController::IntegrationTest fixtures :users @@ -67,8 +73,12 @@ class AccountTest < ActionController::IntegrationTest post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"}, :password => "newpass", :password_confirmation => "newpass" - assert_redirected_to 'account/login' - log_user('newuser', 'newpass') + assert_redirected_to 'my/account' + follow_redirect! + assert_response :success + assert_template 'my/account' + + assert User.find_by_login('newuser').active? end def test_register_with_manual_activation @@ -98,4 +108,46 @@ class AccountTest < ActionController::IntegrationTest assert_redirected_to 'account/login' log_user('newuser', 'newpass') end + + if Object.const_defined?(:Mocha) + + def test_onthefly_registration + # disable registration + Setting.self_registration = '0' + AuthSource.expects(:authenticate).returns([:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66]) + + post 'account/login', :username => 'foo', :password => 'bar' + assert_redirected_to 'my/page' + + user = User.find_by_login('foo') + assert user.is_a?(User) + assert_equal 66, user.auth_source_id + assert user.hashed_password.blank? + end + + def test_onthefly_registration_with_invalid_attributes + # disable registration + Setting.self_registration = '0' + AuthSource.expects(:authenticate).returns([:login => 'foo', :lastname => 'Smith', :auth_source_id => 66]) + + post 'account/login', :username => 'foo', :password => 'bar' + assert_response :success + assert_template 'account/register' + assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' } + assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' } + assert_no_tag :input, :attributes => { :name => 'user[login]' } + assert_no_tag :input, :attributes => { :name => 'user[password]' } + + post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'} + assert_redirected_to 'my/account' + + user = User.find_by_login('foo') + assert user.is_a?(User) + assert_equal 66, user.auth_source_id + assert user.hashed_password.blank? + end + + else + puts 'Mocha is missing. Skipping tests.' + end end diff --git a/groups/test/integration/admin_test.rb b/groups/test/integration/admin_test.rb index a424247cc..6e385873e 100644 --- a/groups/test/integration/admin_test.rb +++ b/groups/test/integration/admin_test.rb @@ -48,8 +48,9 @@ class AdminTest < ActionController::IntegrationTest post "projects/add", :project => { :name => "blog", :description => "weblog", :identifier => "blog", - :is_public => 1 }, - 'custom_fields[3]' => 'Beta' + :is_public => 1, + :custom_field_values => { '3' => 'Beta' } + } assert_redirected_to "admin/projects" assert_equal 'Successful creation.', flash[:notice] diff --git a/groups/test/integration/issues_test.rb b/groups/test/integration/issues_test.rb index b9e21719c..2ef933fc2 100644 --- a/groups/test/integration/issues_test.rb +++ b/groups/test/integration/issues_test.rb @@ -1,10 +1,30 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + require "#{File.dirname(__FILE__)}/../test_helper" class IssuesTest < ActionController::IntegrationTest fixtures :projects, :users, + :roles, + :members, :trackers, :projects_trackers, + :enabled_modules, :issue_statuses, :issues, :enumerations, @@ -47,6 +67,7 @@ class IssuesTest < ActionController::IntegrationTest # add then remove 2 attachments to an issue def test_issue_attachements log_user('jsmith', 'jsmith') + set_tmp_attachments_directory post 'issues/edit/1', :notes => 'Some notes', diff --git a/groups/test/test_helper.rb b/groups/test/test_helper.rb index 61670318a..f61b88d8c 100644 --- a/groups/test/test_helper.rb +++ b/groups/test/test_helper.rb @@ -57,21 +57,11 @@ class Test::Unit::TestCase def test_uploaded_file(name, mime) ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + "/files/#{name}", mime) end -end - - -# ActionController::TestUploadedFile bug -# see http://dev.rubyonrails.org/ticket/4635 -class String - def original_filename - "testfile.txt" - end - - def content_type - "text/plain" - end - def read - self.to_s + # Use a temporary directory for attachment related tests + def set_tmp_attachments_directory + Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test") + Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments") + Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments" end end diff --git a/groups/test/unit/activity_test.rb b/groups/test/unit/activity_test.rb new file mode 100644 index 000000000..ccda9f119 --- /dev/null +++ b/groups/test/unit/activity_test.rb @@ -0,0 +1,71 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class ActivityTest < Test::Unit::TestCase + fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + + def setup + @project = Project.find(1) + end + + def test_activity_without_subprojects + events = find_events(User.anonymous, :project => @project) + assert_not_nil events + + assert events.include?(Issue.find(1)) + assert !events.include?(Issue.find(4)) + # subproject issue + assert !events.include?(Issue.find(5)) + end + + def test_activity_with_subprojects + events = find_events(User.anonymous, :project => @project, :with_subprojects => 1) + assert_not_nil events + + assert events.include?(Issue.find(1)) + # subproject issue + assert events.include?(Issue.find(5)) + end + + def test_global_activity_anonymous + events = find_events(User.anonymous) + assert_not_nil events + + assert events.include?(Issue.find(1)) + assert events.include?(Message.find(5)) + # Issue of a private project + assert !events.include?(Issue.find(4)) + end + + def test_global_activity_logged_user + events = find_events(User.find(2)) # manager + assert_not_nil events + + assert events.include?(Issue.find(1)) + # Issue of a private project the user belongs to + assert events.include?(Issue.find(4)) + end + + private + + def find_events(user, options={}) + Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1) + end +end diff --git a/groups/test/unit/attachment_test.rb b/groups/test/unit/attachment_test.rb new file mode 100644 index 000000000..99f7c29f9 --- /dev/null +++ b/groups/test/unit/attachment_test.rb @@ -0,0 +1,32 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class AttachmentTest < Test::Unit::TestCase + + def setup + end + + def test_diskfilename + assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/ + assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1] + assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1] + assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1] + assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1] + end +end diff --git a/groups/test/unit/changeset_test.rb b/groups/test/unit/changeset_test.rb index bbfe6952d..6cc53d852 100644 --- a/groups/test/unit/changeset_test.rb +++ b/groups/test/unit/changeset_test.rb @@ -39,6 +39,17 @@ class ChangesetTest < Test::Unit::TestCase assert fixed.closed? assert_equal 90, fixed.done_ratio end + + def test_ref_keywords_any_line_start + Setting.commit_ref_keywords = '*' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => '#1 is the reason of this commit') + c.scan_comment_for_issue_ids + + assert_equal [1], c.issue_ids.sort + end def test_previous changeset = Changeset.find_by_revision('3') diff --git a/groups/test/unit/default_data_test.rb b/groups/test/unit/default_data_test.rb new file mode 100644 index 000000000..39616135e --- /dev/null +++ b/groups/test/unit/default_data_test.rb @@ -0,0 +1,45 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class DefaultDataTest < Test::Unit::TestCase + fixtures :roles + + def test_no_data + assert !Redmine::DefaultData::Loader::no_data? + Role.delete_all("builtin = 0") + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + assert Redmine::DefaultData::Loader::no_data? + end + + def test_load + GLoc.valid_languages.each do |lang| + begin + Role.delete_all("builtin = 0") + Tracker.delete_all + IssueStatus.delete_all + Enumeration.delete_all + assert Redmine::DefaultData::Loader::load(lang) + rescue ActiveRecord::RecordInvalid => e + assert false, ":#{lang} default data is invalid (#{e.message})." + end + end + end +end diff --git a/groups/test/unit/enumeration_test.rb b/groups/test/unit/enumeration_test.rb new file mode 100644 index 000000000..9b7bfd174 --- /dev/null +++ b/groups/test/unit/enumeration_test.rb @@ -0,0 +1,45 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class EnumerationTest < Test::Unit::TestCase + fixtures :enumerations, :issues + + def setup + end + + def test_objects_count + # low priority + assert_equal 5, Enumeration.find(4).objects_count + # urgent + assert_equal 0, Enumeration.find(7).objects_count + end + + def test_in_use + # low priority + assert Enumeration.find(4).in_use? + # urgent + assert !Enumeration.find(7).in_use? + end + + def test_destroy_with_reassign + Enumeration.find(4).destroy(Enumeration.find(6)) + assert_nil Issue.find(:first, :conditions => {:priority_id => 4}) + assert_equal 5, Enumeration.find(6).objects_count + end +end diff --git a/groups/test/unit/filesystem_adapter_test.rb b/groups/test/unit/filesystem_adapter_test.rb new file mode 100644 index 000000000..720d1e92c --- /dev/null +++ b/groups/test/unit/filesystem_adapter_test.rb @@ -0,0 +1,42 @@ + +require File.dirname(__FILE__) + '/../test_helper' + + +class FilesystemAdapterTest < Test::Unit::TestCase + + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository' + + if File.directory?(REPOSITORY_PATH) + def setup + @adapter = Redmine::Scm::Adapters::FilesystemAdapter.new(REPOSITORY_PATH) + end + + def test_entries + assert_equal 2, @adapter.entries.size + assert_equal ["dir", "test"], @adapter.entries.collect(&:name) + assert_equal ["dir", "test"], @adapter.entries(nil).collect(&:name) + assert_equal ["dir", "test"], @adapter.entries("/").collect(&:name) + ["dir", "/dir", "/dir/", "dir/"].each do |path| + assert_equal ["subdir", "dirfile"], @adapter.entries(path).collect(&:name) + end + # If y try to use "..", the path is ignored + ["/../","dir/../", "..", "../", "/..", "dir/.."].each do |path| + assert_equal ["dir", "test"], @adapter.entries(path).collect(&:name), ".. must be ignored in path argument" + end + end + + def test_cat + assert_equal "TEST CAT\n", @adapter.cat("test") + assert_equal "TEST CAT\n", @adapter.cat("/test") + # Revision number is ignored + assert_equal "TEST CAT\n", @adapter.cat("/test", 1) + end + + else + puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS." + def test_fake; assert true end + end + +end + + diff --git a/groups/test/unit/helpers/application_helper_test.rb b/groups/test/unit/helpers/application_helper_test.rb index fa2109131..452c0b535 100644 --- a/groups/test/unit/helpers/application_helper_test.rb +++ b/groups/test/unit/helpers/application_helper_test.rb @@ -20,7 +20,11 @@ require File.dirname(__FILE__) + '/../../test_helper' class ApplicationHelperTest < HelperTestCase include ApplicationHelper include ActionView::Helpers::TextHelper - fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules + fixtures :projects, :roles, :enabled_modules, + :repositories, :changesets, + :trackers, :issue_statuses, :issues, :versions, :documents, + :wikis, :wiki_pages, :wiki_contents, + :boards, :messages def setup super @@ -31,10 +35,14 @@ class ApplicationHelperTest < HelperTestCase 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>', 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>', 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.', + 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.', + 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).', 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.', 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>', 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&t=z&s=">http://foo.bar/page?p=1&t=z&s=</a>', - 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>' + 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>', + 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>', + 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>', } to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) } end @@ -58,7 +66,10 @@ class ApplicationHelperTest < HelperTestCase to_test = { 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>', 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>', - '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>' + '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>', + "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph", + # no multiline link text + "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test" } to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) } end @@ -76,7 +87,10 @@ class ApplicationHelperTest < HelperTestCase version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2}, :class => 'version') - source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => 'some/file'} + message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4} + + source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']} + source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']} to_test = { # tickets @@ -92,10 +106,20 @@ class ApplicationHelperTest < HelperTestCase 'version:"1.0"' => version_link, # source 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), + 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'), + 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'), 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'), + 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'), 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'), 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'), + # message + 'message#4' => link_to('Post 2', message_url, :class => 'message'), + 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'), # escaping '!#3.' => '#3.', '!r1' => 'r1', @@ -106,7 +130,9 @@ class ApplicationHelperTest < HelperTestCase '!version:"1.0"' => 'version:"1.0"', '!source:/some/file' => 'source:/some/file', # invalid expressions - 'source:' => 'source:' + 'source:' => 'source:', + # url hash + "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>', } @project = Project.find(1) to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) } @@ -116,6 +142,9 @@ class ApplicationHelperTest < HelperTestCase to_test = { '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>', '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>', + # link with anchor + '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>', + '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>', # page that doesn't exist '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>', '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>', @@ -125,6 +154,8 @@ class ApplicationHelperTest < HelperTestCase '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>', '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>', '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>', + # striked through link + '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>', # escaping '![[Another page|Page]]' => '[[Another page|Page]]', } @@ -141,17 +172,22 @@ class ApplicationHelperTest < HelperTestCase "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>", "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>", "<pre><div>content</div></pre>" => "<pre><div>content</div></pre>", + "HTML comment: <!-- no comments -->" => "<p>HTML comment: <!-- no comments --></p>", + "<!-- opening comment" => "<p><!-- opening comment</p>" + } + to_test.each { |text, result| assert_equal result, textilizable(text) } + end + + def test_allowed_html_tags + to_test = { + "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>", + "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting", } to_test.each { |text, result| assert_equal result, textilizable(text) } end def test_wiki_links_in_tables - to_test = {"|Cell 11|Cell 12|Cell 13|\n|Cell 21|Cell 22||\n|Cell 31||Cell 33|" => - '<tr><td>Cell 11</td><td>Cell 12</td><td>Cell 13</td></tr>' + - '<tr><td>Cell 21</td><td>Cell 22</td></tr>' + - '<tr><td>Cell 31</td><td>Cell 33</td></tr>', - - "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" => + to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" => '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' + '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' + '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>' @@ -160,6 +196,108 @@ class ApplicationHelperTest < HelperTestCase to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') } end + def test_text_formatting + to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>', + '(_text within parentheses_)' => '(<em>text within parentheses</em>)' + } + to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) } + end + + def test_wiki_horizontal_rule + assert_equal '<hr />', textilizable('---') + assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---') + end + + def test_table_of_content + raw = <<-RAW +{{toc}} + +h1. Title + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. + +h2. Subtitle + +Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor. + +h2. Subtitle with %{color:red}red text% + +h1. Another title + +RAW + + expected = '<ul class="toc">' + + '<li class="heading1"><a href="#Title">Title</a></li>' + + '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' + + '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' + + '<li class="heading1"><a href="#Another-title">Another title</a></li>' + + '</ul>' + + assert textilizable(raw).gsub("\n", "").include?(expected) + end + + def test_blockquote + # orig raw text + raw = <<-RAW +John said: +> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. +> Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor. +> * Donec odio lorem, +> * sagittis ac, +> * malesuada in, +> * adipiscing eu, dolor. +> +> >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus. +> Proin a tellus. Nam vel neque. + +He's right. +RAW + + # expected html + expected = <<-EXPECTED +<p>John said:</p> +<blockquote> +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero. +Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor. +<ul> + <li>Donec odio lorem,</li> + <li>sagittis ac,</li> + <li>malesuada in,</li> + <li>adipiscing eu, dolor.</li> +</ul> +<blockquote> +<p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p> +</blockquote> +<p>Proin a tellus. Nam vel neque.</p> +</blockquote> +<p>He's right.</p> +EXPECTED + + assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '') + end + + def test_table + raw = <<-RAW +This is a table with empty cells: + +|cell11|cell12|| +|cell21||cell23| +|cell31|cell32|cell33| +RAW + + expected = <<-EXPECTED +<p>This is a table with empty cells:</p> + +<table> + <tr><td>cell11</td><td>cell12</td><td></td></tr> + <tr><td>cell21</td><td></td><td>cell23</td></tr> + <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr> +</table> +EXPECTED + + assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '') + end + def test_macro_hello_world text = "{{hello_world}}" assert textilizable(text).match(/Hello world!/) diff --git a/groups/test/unit/issue_test.rb b/groups/test/unit/issue_test.rb index 36ba1fb45..12b4da336 100644 --- a/groups/test/unit/issue_test.rb +++ b/groups/test/unit/issue_test.rb @@ -18,7 +18,13 @@ require File.dirname(__FILE__) + '/../test_helper' class IssueTest < Test::Unit::TestCase - fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries + fixtures :projects, :users, :members, + :trackers, :projects_trackers, + :issue_statuses, :issue_categories, + :enumerations, + :issues, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, + :time_entries def test_create issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30') @@ -27,6 +33,76 @@ class IssueTest < Test::Unit::TestCase assert_equal 1.5, issue.estimated_hours end + def test_create_with_required_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field') + assert issue.available_custom_fields.include?(field) + # No value for the custom field + assert !issue.save + assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values) + # Blank value + issue.custom_field_values = { field.id => '' } + assert !issue.save + assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values) + # Invalid value + issue.custom_field_values = { field.id => 'SQLServer' } + assert !issue.save + assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values) + # Valid value + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + issue.reload + assert_equal 'PostgreSQL', issue.custom_value_for(field).value + end + + def test_update_issue_with_required_custom_field + field = IssueCustomField.find_by_name('Database') + field.update_attribute(:is_required, true) + + issue = Issue.find(1) + assert_nil issue.custom_value_for(field) + assert issue.available_custom_fields.include?(field) + # No change to custom values, issue can be saved + assert issue.save + # Blank value + issue.custom_field_values = { field.id => '' } + assert !issue.save + # Valid value + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + issue.reload + assert_equal 'PostgreSQL', issue.custom_value_for(field).value + end + + def test_should_not_update_attributes_if_custom_fields_validation_fails + issue = Issue.find(1) + field = IssueCustomField.find_by_name('Database') + assert issue.available_custom_fields.include?(field) + + issue.custom_field_values = { field.id => 'Invalid' } + issue.subject = 'Should be not be saved' + assert !issue.save + + issue.reload + assert_equal "Can't print recipes", issue.subject + end + + def test_should_not_recreate_custom_values_objects_on_update + field = IssueCustomField.find_by_name('Database') + + issue = Issue.find(1) + issue.custom_field_values = { field.id => 'PostgreSQL' } + assert issue.save + custom_value = issue.custom_value_for(field) + issue.reload + issue.custom_field_values = { field.id => 'MySQL' } + assert issue.save + issue.reload + assert_equal custom_value.id, issue.custom_value_for(field).id + end + def test_category_based_assignment issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1) assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to @@ -42,7 +118,7 @@ class IssueTest < Test::Unit::TestCase assert_equal orig.custom_values.first.value, issue.custom_values.first.value end - def test_close_duplicates + def test_should_close_duplicates # Create 3 issues issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test') assert issue1.save @@ -52,12 +128,12 @@ class IssueTest < Test::Unit::TestCase assert issue3.save # 2 is a dupe of 1 - IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES) + IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES) # And 3 is a dupe of 2 - IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES) + IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES) # And 3 is a dupe of 1 (circular duplicates) - IssueRelation.create(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES) - + IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES) + assert issue1.reload.duplicates.include?(issue2) # Closing issue 1 @@ -69,17 +145,46 @@ class IssueTest < Test::Unit::TestCase assert issue3.reload.closed? end - def test_move_to_another_project + def test_should_not_close_duplicated_issue + # Create 3 issues + issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test') + assert issue1.save + issue2 = issue1.clone + assert issue2.save + + # 2 is a dupe of 1 + IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES) + # 2 is a dup of 1 but 1 is not a duplicate of 2 + assert !issue2.reload.duplicates.include?(issue1) + + # Closing issue 2 + issue2.init_journal(User.find(:first), "Closing issue2") + issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true} + assert issue2.save + # 1 should not be also closed + assert !issue1.reload.closed? + end + + def test_move_to_another_project_with_same_category issue = Issue.find(1) assert issue.move_to(Project.find(2)) issue.reload assert_equal 2, issue.project_id - # Category removed - assert_nil issue.category + # Category changes + assert_equal 4, issue.category_id # Make sure time entries were move to the target project assert_equal 2, issue.time_entries.first.project_id end + def test_move_to_another_project_without_same_category + issue = Issue.find(2) + assert issue.move_to(Project.find(2)) + issue.reload + assert_equal 2, issue.project_id + # Category cleared + assert_nil issue.category_id + end + def test_issue_destroy Issue.find(1).destroy assert_nil Issue.find_by_id(1) diff --git a/groups/test/unit/mail_handler_test.rb b/groups/test/unit/mail_handler_test.rb index d0fc68de8..b3628e0d5 100644 --- a/groups/test/unit/mail_handler_test.rb +++ b/groups/test/unit/mail_handler_test.rb @@ -18,40 +18,111 @@ require File.dirname(__FILE__) + '/../test_helper' class MailHandlerTest < Test::Unit::TestCase - fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations + fixtures :users, :projects, + :enabled_modules, + :roles, + :members, + :issues, + :trackers, + :projects_trackers, + :enumerations, + :issue_categories + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' - FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' - CHARSET = "utf-8" - - include ActionMailer::Quoting - def setup - ActionMailer::Base.delivery_method = :test - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries = [] + ActionMailer::Base.deliveries.clear + end + + def test_add_issue + # This email contains: 'Project: onlinestore' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end - @expected = TMail::Mail.new - @expected.set_content_type "text", "plain", { "charset" => CHARSET } - @expected.mime_version = '1.0' + def test_add_issue_with_status + # This email contains: 'Project: onlinestore' and 'Status: Resolved' + issue = submit_email('ticket_on_given_project.eml') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal Project.find(2), issue.project + assert_equal IssueStatus.find_by_name("Resolved"), issue.status + end + + def test_add_issue_with_attributes_override + issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority') + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_equal 'Stock management', issue.category.to_s + assert_equal 'Urgent', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') end - def test_add_note_to_issue - raw = read_fixture("add_note_to_issue.txt").join - MailHandler.receive(raw) + def test_add_issue_with_partial_attributes_override + issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker']) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'New ticket on a given project', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'Feature request', issue.tracker.to_s + assert_nil issue.category + assert_equal 'High', issue.priority.to_s + assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') + end + + def test_add_issue_with_attachment_to_specific_project + issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) + assert issue.is_a?(Issue) + assert !issue.new_record? + issue.reload + assert_equal 'Ticket created by email with attachment', issue.subject + assert_equal User.find_by_login('jsmith'), issue.author + assert_equal Project.find(2), issue.project + assert_equal 'This is a new ticket with attachments', issue.description + # Attachment properties + assert_equal 1, issue.attachments.size + assert_equal 'Paella.jpg', issue.attachments.first.filename + assert_equal 'image/jpeg', issue.attachments.first.content_type + assert_equal 10790, issue.attachments.first.filesize + end + + def test_add_issue_note + journal = submit_email('ticket_reply.eml') + assert journal.is_a?(Journal) + assert_equal User.find_by_login('jsmith'), journal.user + assert_equal Issue.find(2), journal.journalized + assert_match /This is reply/, journal.notes + end - issue = Issue.find(2) - journal = issue.journals.find(:first, :order => "created_on DESC") - assert journal - assert_equal User.find_by_mail("jsmith@somenet.foo"), journal.user - assert_equal "Note added by mail", journal.notes + def test_add_issue_note_with_status_change + # This email contains: 'Status: Resolved' + journal = submit_email('ticket_reply_with_status.eml') + assert journal.is_a?(Journal) + issue = Issue.find(journal.issue.id) + assert_equal User.find_by_login('jsmith'), journal.user + assert_equal Issue.find(2), journal.journalized + assert_match /This is reply/, journal.notes + assert_equal IssueStatus.find_by_name("Resolved"), issue.status end private - def read_fixture(action) - IO.readlines("#{FIXTURES_PATH}/mail_handler/#{action}") - end - - def encode(subject) - quoted_printable(subject, CHARSET) - end + + def submit_email(filename, options={}) + raw = IO.read(File.join(FIXTURES_PATH, filename)) + MailHandler.receive(raw, options) + end end diff --git a/groups/test/unit/mailer_test.rb b/groups/test/unit/mailer_test.rb index 64648b94c..402624f5f 100644 --- a/groups/test/unit/mailer_test.rb +++ b/groups/test/unit/mailer_test.rb @@ -36,7 +36,7 @@ class MailerTest < Test::Unit::TestCase # link to a referenced ticket assert mail.body.include?('<a href="https://mydomain.foo/issues/show/2" class="issue" title="Add ingredients categories (Assigned)">#2</a>') # link to a changeset - assert mail.body.include?('<a href="https://mydomain.foo/repositories/revision/ecookbook?rev=2" class="changeset" title="This commit fixes #1, #2 and references #1 & #3">r2</a>') + assert mail.body.include?('<a href="https://mydomain.foo/repositories/revision/ecookbook/2" class="changeset" title="This commit fixes #1, #2 and references #1 & #3">r2</a>') end # test mailer methods for each language @@ -116,4 +116,13 @@ class MailerTest < Test::Unit::TestCase assert Mailer.deliver_register(token) end end + + def test_reminders + ActionMailer::Base.deliveries.clear + Mailer.reminders(:days => 42) + assert_equal 1, ActionMailer::Base.deliveries.size + mail = ActionMailer::Base.deliveries.last + assert mail.bcc.include?('dlopper@somenet.foo') + assert mail.body.include?('Bug #3: Error 281 when updating a recipe') + end end diff --git a/groups/test/unit/mercurial_adapter_test.rb b/groups/test/unit/mercurial_adapter_test.rb new file mode 100644 index 000000000..a2673ad42 --- /dev/null +++ b/groups/test/unit/mercurial_adapter_test.rb @@ -0,0 +1,53 @@ +require File.dirname(__FILE__) + '/../test_helper' +begin + require 'mocha' + + class MercurialAdapterTest < Test::Unit::TestCase + + TEMPLATES_DIR = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATES_DIR + TEMPLATE_NAME = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_NAME + TEMPLATE_EXTENSION = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_EXTENSION + + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' + + def test_hgversion + to_test = { "0.9.5" => [0,9,5], + "1.0" => [1,0], + "1e4ddc9ac9f7+20080325" => nil, + "1.0.1+20080525" => [1,0,1], + "1916e629a29d" => nil} + + to_test.each do |s, v| + test_hgversion_for(s, v) + end + end + + def test_template_path + to_test = { [0,9,5] => "0.9.5", + [1,0] => "1.0", + [] => "1.0", + [1,0,1] => "1.0"} + + to_test.each do |v, template| + test_template_path_for(v, template) + end + end + + private + + def test_hgversion_for(hgversion, version) + Redmine::Scm::Adapters::MercurialAdapter.expects(:hgversion_from_command_line).returns(hgversion) + adapter = Redmine::Scm::Adapters::MercurialAdapter + assert_equal version, adapter.hgversion + end + + def test_template_path_for(version, template) + adapter = Redmine::Scm::Adapters::MercurialAdapter + assert_equal "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{template}.#{TEMPLATE_EXTENSION}", adapter.template_path_for(version) + assert File.exist?(adapter.template_path_for(version)) + end + end + +rescue LoadError + def test_fake; assert(false, "Requires mocha to run those tests") end +end diff --git a/groups/test/unit/project_test.rb b/groups/test/unit/project_test.rb index 60106bc07..0bd28dbc9 100644 --- a/groups/test/unit/project_test.rb +++ b/groups/test/unit/project_test.rb @@ -101,7 +101,7 @@ class ProjectTest < Test::Unit::TestCase assert sub.save
assert_equal @ecookbook.id, sub.parent.id
@ecookbook.reload
- assert_equal 3, @ecookbook.children.size
+ assert_equal 4, @ecookbook.children.size
end
def test_subproject_invalid
@@ -118,6 +118,7 @@ class ProjectTest < Test::Unit::TestCase def test_rolled_up_trackers
parent = Project.find(1)
+ parent.trackers = Tracker.find([1,2])
child = parent.children.find(3)
assert_equal [1, 2], parent.tracker_ids
diff --git a/groups/test/unit/query_test.rb b/groups/test/unit/query_test.rb index d291018fb..c243dfbad 100644 --- a/groups/test/unit/query_test.rb +++ b/groups/test/unit/query_test.rb @@ -20,15 +20,109 @@ require File.dirname(__FILE__) + '/../test_helper' class QueryTest < Test::Unit::TestCase fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries + def test_custom_fields_for_all_projects_should_be_available_in_global_queries + query = Query.new(:project => nil, :name => '_') + assert query.available_filters.has_key?('cf_1') + assert !query.available_filters.has_key?('cf_3') + end + + def find_issues_with_query(query) + Issue.find :all, + :include => [ :assigned_to, :status, :tracker, :project, :priority ], + :conditions => query.statement + end + def test_query_with_multiple_custom_fields query = Query.find(1) assert query.valid? assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + issues = find_issues_with_query(query) assert_equal 1, issues.length assert_equal Issue.find(3), issues.first end + def test_operator_none + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '!*', ['']) + query.add_filter('cf_1', '!*', ['']) + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL") + assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''") + find_issues_with_query(query) + end + + def test_operator_none_for_integer + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('estimated_hours', '!*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| !i.estimated_hours} + end + + def test_operator_all + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('fixed_version_id', '*', ['']) + query.add_filter('cf_1', '*', ['']) + assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL") + assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''") + find_issues_with_query(query) + end + + def test_operator_greater_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('done_ratio', '>=', ['40']) + assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40") + find_issues_with_query(query) + end + + def test_operator_in_more_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '>t+', ['15']) + assert query.statement.include?("#{Issue.table_name}.due_date >=") + find_issues_with_query(query) + end + + def test_operator_in_less_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', '<t+', ['15']) + assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN") + find_issues_with_query(query) + end + + def test_operator_today + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', 't', ['']) + assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN") + find_issues_with_query(query) + end + + def test_operator_this_week_on_date + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', 'w', ['']) + assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN") + find_issues_with_query(query) + end + + def test_operator_this_week_on_datetime + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('created_on', 'w', ['']) + assert query.statement.include?("#{Issue.table_name}.created_on BETWEEN") + find_issues_with_query(query) + end + + def test_operator_contains + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('subject', '~', ['string']) + assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'") + find_issues_with_query(query) + end + + def test_operator_does_not_contains + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('subject', '!~', ['string']) + assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'") + find_issues_with_query(query) + end + def test_default_columns q = Query.new assert !q.columns.empty? @@ -42,6 +136,11 @@ class QueryTest < Test::Unit::TestCase assert q.has_column?(c) end + def test_label_for + q = Query.new + assert_equal 'assigned_to', q.label_for('assigned_to_id') + end + def test_editable_by admin = User.find(1) manager = User.find(2) diff --git a/groups/test/unit/repository_cvs_test.rb b/groups/test/unit/repository_cvs_test.rb index b14d9d964..6615f73bf 100644 --- a/groups/test/unit/repository_cvs_test.rb +++ b/groups/test/unit/repository_cvs_test.rb @@ -22,7 +22,7 @@ class RepositoryCvsTest < Test::Unit::TestCase # No '..' in the repository path REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository' - REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/ + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? # CVS module MODULE_NAME = 'test' diff --git a/groups/test/unit/repository_darcs_test.rb b/groups/test/unit/repository_darcs_test.rb index 1c8c1b8dd..ca8c267f2 100644 --- a/groups/test/unit/repository_darcs_test.rb +++ b/groups/test/unit/repository_darcs_test.rb @@ -48,6 +48,13 @@ class RepositoryDarcsTest < Test::Unit::TestCase @repository.fetch_changesets assert_equal 6, @repository.changesets.count end + + def test_cat + @repository.fetch_changesets + cat = @repository.cat("sources/welcome_controller.rb", 2) + assert_not_nil cat + assert cat.include?('class WelcomeController < ApplicationController') + end else puts "Darcs test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end diff --git a/groups/test/unit/repository_filesystem_test.rb b/groups/test/unit/repository_filesystem_test.rb new file mode 100644 index 000000000..6b643f96f --- /dev/null +++ b/groups/test/unit/repository_filesystem_test.rb @@ -0,0 +1,54 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class RepositoryFilesystemTest < Test::Unit::TestCase + fixtures :projects + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository' + + def setup + @project = Project.find(1) + Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem') + assert @repository = Repository::Filesystem.create(:project => @project, :url => REPOSITORY_PATH) + end + + if File.directory?(REPOSITORY_PATH) + def test_fetch_changesets + @repository.fetch_changesets + @repository.reload + + assert_equal 0, @repository.changesets.count + assert_equal 0, @repository.changes.count + end + + def test_entries + assert_equal 2, @repository.entries("", 2).size + assert_equal 2, @repository.entries("dir", 3).size + end + + def test_cat + assert_equal "TEST CAT\n", @repository.scm.cat("test") + end + + else + puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS." + def test_fake; assert true end + end +end diff --git a/groups/test/unit/repository_git_test.rb b/groups/test/unit/repository_git_test.rb index c7bd84a6e..8a6f1ddd0 100644 --- a/groups/test/unit/repository_git_test.rb +++ b/groups/test/unit/repository_git_test.rb @@ -22,7 +22,7 @@ class RepositoryGitTest < Test::Unit::TestCase # No '..' in the repository path REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository' - REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/ + REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin? def setup @project = Project.find(1) diff --git a/groups/test/unit/repository_mercurial_test.rb b/groups/test/unit/repository_mercurial_test.rb index 21ddf1e3a..0f993ac16 100644 --- a/groups/test/unit/repository_mercurial_test.rb +++ b/groups/test/unit/repository_mercurial_test.rb @@ -48,6 +48,26 @@ class RepositoryMercurialTest < Test::Unit::TestCase @repository.fetch_changesets assert_equal 6, @repository.changesets.count end + + def test_entries + assert_equal 2, @repository.entries("sources", 2).size + assert_equal 1, @repository.entries("sources", 3).size + end + + def test_locate_on_outdated_repository + # Change the working dir state + %x{hg -R #{REPOSITORY_PATH} up -r 0} + assert_equal 1, @repository.entries("images", 0).size + assert_equal 2, @repository.entries("images").size + assert_equal 2, @repository.entries("images", 2).size + end + + + def test_cat + assert @repository.scm.cat("sources/welcome_controller.rb", 2) + assert_nil @repository.scm.cat("sources/welcome_controller.rb") + end + else puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end diff --git a/groups/test/unit/repository_test.rb b/groups/test/unit/repository_test.rb index 7764ee04a..9ea9fdd45 100644 --- a/groups/test/unit/repository_test.rb +++ b/groups/test/unit/repository_test.rb @@ -45,6 +45,26 @@ class RepositoryTest < Test::Unit::TestCase assert_equal repository, project.repository
end + def test_destroy + changesets = Changeset.count(:all, :conditions => "repository_id = 10") + changes = Change.count(:all, :conditions => "repository_id = 10", :include => :changeset) + assert_difference 'Changeset.count', -changesets do + assert_difference 'Change.count', -changes do + Repository.find(10).destroy + end + end + end + + def test_should_not_create_with_disabled_scm + # disable Subversion + Setting.enabled_scm = ['Darcs', 'Git'] + repository = Repository::Subversion.new(:project => Project.find(3), :url => "svn://localhost") + assert !repository.save + assert_equal :activerecord_error_invalid, repository.errors.on(:type) + # re-enable Subversion for following tests + Setting.delete_all + end + def test_scan_changesets_for_issue_ids # choosing a status to apply to fix issues Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id diff --git a/groups/test/unit/role_test.rb b/groups/test/unit/role_test.rb index 5e0d16753..b98af2e36 100644 --- a/groups/test/unit/role_test.rb +++ b/groups/test/unit/role_test.rb @@ -26,7 +26,7 @@ class RoleTest < Test::Unit::TestCase target = Role.new(:name => 'Target') assert target.save - assert target.workflows.copy(source) + target.workflows.copy(source) target.reload assert_equal 90, target.workflows.size end diff --git a/groups/test/unit/search_test.rb b/groups/test/unit/search_test.rb new file mode 100644 index 000000000..1b32df733 --- /dev/null +++ b/groups/test/unit/search_test.rb @@ -0,0 +1,143 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class SearchTest < Test::Unit::TestCase + fixtures :users, + :members, + :projects, + :roles, + :enabled_modules, + :issues, + :trackers, + :journals, + :journal_details, + :repositories, + :changesets + + def setup + @project = Project.find(1) + @issue_keyword = '%unable to print recipes%' + @issue = Issue.find(1) + @changeset_keyword = '%very first commit%' + @changeset = Changeset.find(100) + end + + def test_search_by_anonymous + User.current = nil + + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert r.include?(@changeset) + + # Removes the :view_changesets permission from Anonymous role + remove_permission Role.anonymous, :view_changesets + + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert !r.include?(@changeset) + + # Make the project private + @project.update_attribute :is_public, false + r = Issue.search(@issue_keyword).first + assert !r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert !r.include?(@changeset) + end + + def test_search_by_user + User.current = User.find_by_login('rhill') + assert User.current.memberships.empty? + + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert r.include?(@changeset) + + # Removes the :view_changesets permission from Non member role + remove_permission Role.non_member, :view_changesets + + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert !r.include?(@changeset) + + # Make the project private + @project.update_attribute :is_public, false + r = Issue.search(@issue_keyword).first + assert !r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert !r.include?(@changeset) + end + + def test_search_by_allowed_member + User.current = User.find_by_login('jsmith') + assert User.current.projects.include?(@project) + + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert r.include?(@changeset) + + # Make the project private + @project.update_attribute :is_public, false + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert r.include?(@changeset) + end + + def test_search_by_unallowed_member + # Removes the :view_changesets permission from user's and non member role + remove_permission Role.find(1), :view_changesets + remove_permission Role.non_member, :view_changesets + + User.current = User.find_by_login('jsmith') + assert User.current.projects.include?(@project) + + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert !r.include?(@changeset) + + # Make the project private + @project.update_attribute :is_public, false + r = Issue.search(@issue_keyword).first + assert r.include?(@issue) + r = Changeset.search(@changeset_keyword).first + assert !r.include?(@changeset) + end + + def test_search_issue_with_multiple_hits_in_journals + i = Issue.find(1) + assert_equal 2, i.journals.count(:all, :conditions => "notes LIKE '%notes%'") + + r = Issue.search('%notes%').first + assert_equal 1, r.size + assert_equal i, r.first + end + + private + + def remove_permission(role, permission) + role.permissions = role.permissions - [ permission ] + role.save + end +end diff --git a/groups/test/unit/subversion_adapter_test.rb b/groups/test/unit/subversion_adapter_test.rb new file mode 100644 index 000000000..9f208839a --- /dev/null +++ b/groups/test/unit/subversion_adapter_test.rb @@ -0,0 +1,33 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'mkmf' + +require File.dirname(__FILE__) + '/../test_helper' + +class SubversionAdapterTest < Test::Unit::TestCase + + if find_executable0('svn') + def test_client_version + v = Redmine::Scm::Adapters::SubversionAdapter.client_version + assert v.is_a?(Array) + end + else + puts "Subversion binary NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end diff --git a/groups/test/unit/tracker_test.rb b/groups/test/unit/tracker_test.rb index 406bdd6db..6dab8890c 100644 --- a/groups/test/unit/tracker_test.rb +++ b/groups/test/unit/tracker_test.rb @@ -26,7 +26,7 @@ class TrackerTest < Test::Unit::TestCase target = Tracker.new(:name => 'Target') assert target.save - assert target.workflows.copy(source) + target.workflows.copy(source) target.reload assert_equal 89, target.workflows.size end diff --git a/groups/test/unit/user_test.rb b/groups/test/unit/user_test.rb index 3209f261a..925549544 100644 --- a/groups/test/unit/user_test.rb +++ b/groups/test/unit/user_test.rb @@ -57,6 +57,12 @@ class UserTest < Test::Unit::TestCase assert_equal "john", @admin.login end + def test_destroy + User.find(2).destroy + assert_nil User.find_by_id(2) + assert Member.find_all_by_principal_type_and_principal_id('User', 2).empty? + end + def test_validate @admin.login = "" assert !@admin.save diff --git a/groups/test/unit/wiki_page_test.rb b/groups/test/unit/wiki_page_test.rb index bb8111176..e5ebeeea6 100644 --- a/groups/test/unit/wiki_page_test.rb +++ b/groups/test/unit/wiki_page_test.rb @@ -48,6 +48,50 @@ class WikiPageTest < Test::Unit::TestCase assert page.new_record? end + def test_parent_title + page = WikiPage.find_by_title('Another_page') + assert_nil page.parent_title + + page = WikiPage.find_by_title('Page_with_an_inline_image') + assert_equal 'CookBook documentation', page.parent_title + end + + def test_assign_parent + page = WikiPage.find_by_title('Another_page') + page.parent_title = 'CookBook documentation' + assert page.save + page.reload + assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent + end + + def test_unassign_parent + page = WikiPage.find_by_title('Page_with_an_inline_image') + page.parent_title = '' + assert page.save + page.reload + assert_nil page.parent + end + + def test_parent_validation + page = WikiPage.find_by_title('CookBook_documentation') + + # A page that doesn't exist + page.parent_title = 'Unknown title' + assert !page.save + assert_equal :activerecord_error_invalid, page.errors.on(:parent_title) + # A child page + page.parent_title = 'Page_with_an_inline_image' + assert !page.save + assert_equal :activerecord_error_circular_dependency, page.errors.on(:parent_title) + # The page itself + page.parent_title = 'CookBook_documentation' + assert !page.save + assert_equal :activerecord_error_circular_dependency, page.errors.on(:parent_title) + + page.parent_title = 'Another_page' + assert page.save + end + def test_destroy page = WikiPage.find(1) page.destroy diff --git a/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb b/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb index d7f437a5e..0b7ad21f5 100644 --- a/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb +++ b/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb @@ -25,14 +25,15 @@ module Redmine module ClassMethods def acts_as_event(options = {}) return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods) - options[:datetime] ||= :created_on - options[:title] ||= :title - options[:description] ||= :description - options[:author] ||= :author - options[:url] ||= {:controller => 'welcome'} - options[:type] ||= self.name.underscore.dasherize + default_options = { :datetime => :created_on, + :title => :title, + :description => :description, + :author => :author, + :url => {:controller => 'welcome'}, + :type => self.name.underscore.dasherize } + cattr_accessor :event_options - self.event_options = options + self.event_options = default_options.merge(options) send :include, Redmine::Acts::Event::InstanceMethods end end diff --git a/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb index dff76b913..9a81f363f 100644 --- a/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -23,6 +23,12 @@ module Redmine end module ClassMethods + # Options: + # * :columns - a column or an array of columns to search + # * :project_key - project foreign key (default to project_id) + # * :date_column - name of the datetime column (default to created_on) + # * :sort_order - name of the column used to sort results (default to :date_column or created_on) + # * :permission - permission required to search the model (default to :view_"objects") def acts_as_searchable(options = {}) return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods) @@ -35,19 +41,12 @@ module Redmine searchable_options[:columns] = [] << searchable_options[:columns] end - if searchable_options[:project_key] - elsif column_names.include?('project_id') - searchable_options[:project_key] = "#{table_name}.project_id" - else - raise 'No project key defined.' - end + searchable_options[:project_key] ||= "#{table_name}.project_id" + searchable_options[:date_column] ||= "#{table_name}.created_on" + searchable_options[:order_column] ||= searchable_options[:date_column] - if searchable_options[:date_column] - elsif column_names.include?('created_on') - searchable_options[:date_column] = "#{table_name}.created_on" - else - raise 'No date column defined defined.' - end + # Permission needed to search this model + searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission) # Should we search custom fields on this model ? searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? @@ -62,13 +61,24 @@ module Redmine end module ClassMethods - def search(tokens, project, options={}) + # Searches the model for the given tokens + # projects argument can be either nil (will search all projects), a project or an array of projects + # Returns the results and the results count + def search(tokens, projects=nil, options={}) tokens = [] << tokens unless tokens.is_a?(Array) + projects = [] << projects unless projects.nil? || projects.is_a?(Array) + find_options = {:include => searchable_options[:include]} - find_options[:limit] = options[:limit] if options[:limit] - find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC') + find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC') + + limit_options = {} + limit_options[:limit] = options[:limit] if options[:limit] + if options[:offset] + limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" + end + columns = searchable_options[:columns] - columns.slice!(1..-1) if options[:titles_only] + columns = columns[0..0] if options[:titles_only] token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"} @@ -87,21 +97,23 @@ module Redmine sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') - if options[:offset] - sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" - end find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] - results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do - find(:all, find_options) - end - if searchable_options[:with] && !options[:titles_only] - searchable_options[:with].each do |model, assoc| - results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc} + project_conditions = [] + project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) : + Project.allowed_to_condition(User.current, searchable_options[:permission])) + project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil? + + results = [] + results_count = 0 + + with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do + with_scope(:find => find_options) do + results_count = count(:all) + results = find(:all, limit_options) end - results.uniq! end - results + [results, results_count] end end end diff --git a/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb b/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb index 1f00e90a9..6a6827ee6 100644 --- a/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb +++ b/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb @@ -70,6 +70,13 @@ module ActiveRecord nodes end + # Returns list of descendants. + # + # root.descendants # => [child1, subchild1, subchild2] + def descendants + children + children.collect(&:children).flatten + end + # Returns the root node of the tree. def root node = self diff --git a/groups/vendor/plugins/acts_as_versioned/Rakefile b/groups/vendor/plugins/acts_as_versioned/Rakefile index 3ae69e961..5bccb5d8d 100644 --- a/groups/vendor/plugins/acts_as_versioned/Rakefile +++ b/groups/vendor/plugins/acts_as_versioned/Rakefile @@ -1,182 +1,182 @@ -require 'rubygems' - -Gem::manage_gems - -require 'rake/rdoctask' -require 'rake/packagetask' -require 'rake/gempackagetask' -require 'rake/testtask' -require 'rake/contrib/rubyforgepublisher' - -PKG_NAME = 'acts_as_versioned' -PKG_VERSION = '0.3.1' -PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" -PROD_HOST = "technoweenie@bidwell.textdrive.com" -RUBY_FORGE_PROJECT = 'ar-versioned' -RUBY_FORGE_USER = 'technoweenie' - -desc 'Default: run unit tests.' -task :default => :test - -desc 'Test the calculations plugin.' -Rake::TestTask.new(:test) do |t| - t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' - t.verbose = true -end - -desc 'Generate documentation for the calculations plugin.' -Rake::RDocTask.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models" - rdoc.options << '--line-numbers --inline-source' - rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -spec = Gem::Specification.new do |s| - s.name = PKG_NAME - s.version = PKG_VERSION - s.platform = Gem::Platform::RUBY - s.summary = "Simple versioning with active record models" - s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS) - s.files.delete "acts_as_versioned_plugin.sqlite.db" - s.files.delete "acts_as_versioned_plugin.sqlite3.db" - s.files.delete "test/debug.log" - s.require_path = 'lib' - s.autorequire = 'acts_as_versioned' - s.has_rdoc = true - s.test_files = Dir['test/**/*_test.rb'] - s.add_dependency 'activerecord', '>= 1.10.1' - s.add_dependency 'activesupport', '>= 1.1.1' - s.author = "Rick Olson" - s.email = "technoweenie@gmail.com" - s.homepage = "http://techno-weenie.net" -end - -Rake::GemPackageTask.new(spec) do |pkg| - pkg.need_tar = true -end - -desc "Publish the API documentation" -task :pdoc => [:rdoc] do - Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload -end - -desc 'Publish the gem and API docs' -task :publish => [:pdoc, :rubyforge_upload] - -desc "Publish the release files to RubyForge." -task :rubyforge_upload => :package do - files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" } - - if RUBY_FORGE_PROJECT then - require 'net/http' - require 'open-uri' - - project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/" - project_data = open(project_uri) { |data| data.read } - group_id = project_data[/[?&]group_id=(\d+)/, 1] - raise "Couldn't get group id" unless group_id - - # This echos password to shell which is a bit sucky - if ENV["RUBY_FORGE_PASSWORD"] - password = ENV["RUBY_FORGE_PASSWORD"] - else - print "#{RUBY_FORGE_USER}@rubyforge.org's password: " - password = STDIN.gets.chomp - end - - login_response = Net::HTTP.start("rubyforge.org", 80) do |http| - data = [ - "login=1", - "form_loginname=#{RUBY_FORGE_USER}", - "form_pw=#{password}" - ].join("&") - http.post("/account/login.php", data) - end - - cookie = login_response["set-cookie"] - raise "Login failed" unless cookie - headers = { "Cookie" => cookie } - - release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}" - release_data = open(release_uri, headers) { |data| data.read } - package_id = release_data[/[?&]package_id=(\d+)/, 1] - raise "Couldn't get package id" unless package_id - - first_file = true - release_id = "" - - files.each do |filename| - basename = File.basename(filename) - file_ext = File.extname(filename) - file_data = File.open(filename, "rb") { |file| file.read } - - puts "Releasing #{basename}..." - - release_response = Net::HTTP.start("rubyforge.org", 80) do |http| - release_date = Time.now.strftime("%Y-%m-%d %H:%M") - type_map = { - ".zip" => "3000", - ".tgz" => "3110", - ".gz" => "3110", - ".gem" => "1400" - }; type_map.default = "9999" - type = type_map[file_ext] - boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor" - - query_hash = if first_file then - { - "group_id" => group_id, - "package_id" => package_id, - "release_name" => PKG_FILE_NAME, - "release_date" => release_date, - "type_id" => type, - "processor_id" => "8000", # Any - "release_notes" => "", - "release_changes" => "", - "preformatted" => "1", - "submit" => "1" - } - else - { - "group_id" => group_id, - "release_id" => release_id, - "package_id" => package_id, - "step2" => "1", - "type_id" => type, - "processor_id" => "8000", # Any - "submit" => "Add This File" - } - end - - query = "?" + query_hash.map do |(name, value)| - [name, URI.encode(value)].join("=") - end.join("&") - - data = [ - "--" + boundary, - "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"", - "Content-Type: application/octet-stream", - "Content-Transfer-Encoding: binary", - "", file_data, "" - ].join("\x0D\x0A") - - release_headers = headers.merge( - "Content-Type" => "multipart/form-data; boundary=#{boundary}" - ) - - target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php" - http.post(target + query, data, release_headers) - end - - if first_file then - release_id = release_response.body[/release_id=(\d+)/, 1] - raise("Couldn't get release id") unless release_id - end - - first_file = false - end - end +require 'rubygems'
+
+Gem::manage_gems
+
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/testtask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_NAME = 'acts_as_versioned'
+PKG_VERSION = '0.3.1'
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+PROD_HOST = "technoweenie@bidwell.textdrive.com"
+RUBY_FORGE_PROJECT = 'ar-versioned'
+RUBY_FORGE_USER = 'technoweenie'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the calculations plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the calculations plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.summary = "Simple versioning with active record models"
+ s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
+ s.files.delete "acts_as_versioned_plugin.sqlite.db"
+ s.files.delete "acts_as_versioned_plugin.sqlite3.db"
+ s.files.delete "test/debug.log"
+ s.require_path = 'lib'
+ s.autorequire = 'acts_as_versioned'
+ s.has_rdoc = true
+ s.test_files = Dir['test/**/*_test.rb']
+ s.add_dependency 'activerecord', '>= 1.10.1'
+ s.add_dependency 'activesupport', '>= 1.1.1'
+ s.author = "Rick Olson"
+ s.email = "technoweenie@gmail.com"
+ s.homepage = "http://techno-weenie.net"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_tar = true
+end
+
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
+end
+
+desc 'Publish the gem and API docs'
+task :publish => [:pdoc, :rubyforge_upload]
+
+desc "Publish the release files to RubyForge."
+task :rubyforge_upload => :package do
+ files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
+
+ if RUBY_FORGE_PROJECT then
+ require 'net/http'
+ require 'open-uri'
+
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
+ project_data = open(project_uri) { |data| data.read }
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]
+ raise "Couldn't get group id" unless group_id
+
+ # This echos password to shell which is a bit sucky
+ if ENV["RUBY_FORGE_PASSWORD"]
+ password = ENV["RUBY_FORGE_PASSWORD"]
+ else
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
+ password = STDIN.gets.chomp
+ end
+
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
+ data = [
+ "login=1",
+ "form_loginname=#{RUBY_FORGE_USER}",
+ "form_pw=#{password}"
+ ].join("&")
+ http.post("/account/login.php", data)
+ end
+
+ cookie = login_response["set-cookie"]
+ raise "Login failed" unless cookie
+ headers = { "Cookie" => cookie }
+
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
+ release_data = open(release_uri, headers) { |data| data.read }
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]
+ raise "Couldn't get package id" unless package_id
+
+ first_file = true
+ release_id = ""
+
+ files.each do |filename|
+ basename = File.basename(filename)
+ file_ext = File.extname(filename)
+ file_data = File.open(filename, "rb") { |file| file.read }
+
+ puts "Releasing #{basename}..."
+
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
+ type_map = {
+ ".zip" => "3000",
+ ".tgz" => "3110",
+ ".gz" => "3110",
+ ".gem" => "1400"
+ }; type_map.default = "9999"
+ type = type_map[file_ext]
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
+
+ query_hash = if first_file then
+ {
+ "group_id" => group_id,
+ "package_id" => package_id,
+ "release_name" => PKG_FILE_NAME,
+ "release_date" => release_date,
+ "type_id" => type,
+ "processor_id" => "8000", # Any
+ "release_notes" => "",
+ "release_changes" => "",
+ "preformatted" => "1",
+ "submit" => "1"
+ }
+ else
+ {
+ "group_id" => group_id,
+ "release_id" => release_id,
+ "package_id" => package_id,
+ "step2" => "1",
+ "type_id" => type,
+ "processor_id" => "8000", # Any
+ "submit" => "Add This File"
+ }
+ end
+
+ query = "?" + query_hash.map do |(name, value)|
+ [name, URI.encode(value)].join("=")
+ end.join("&")
+
+ data = [
+ "--" + boundary,
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
+ "Content-Type: application/octet-stream",
+ "Content-Transfer-Encoding: binary",
+ "", file_data, ""
+ ].join("\x0D\x0A")
+
+ release_headers = headers.merge(
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
+ )
+
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
+ http.post(target + query, data, release_headers)
+ end
+
+ if first_file then
+ release_id = release_response.body[/release_id=(\d+)/, 1]
+ raise("Couldn't get release id") unless release_id
+ end
+
+ first_file = false
+ end
+ end
end
\ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb b/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb index 5e6f6e636..bba10c437 100644 --- a/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb +++ b/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb @@ -22,7 +22,7 @@ module ActiveRecord #:nodoc: module Acts #:nodoc: # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a - # versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version + # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version # column is present as well. # # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart @@ -49,9 +49,24 @@ module ActiveRecord #:nodoc: # page.revert_to(page.versions.last) # using versioned instance # page.title # => 'hello world' # + # page.versions.earliest # efficient query to find the first version + # page.versions.latest # efficient query to find the most recently created version + # + # + # Simple Queries to page between versions + # + # page.versions.before(version) + # page.versions.after(version) + # + # Access the previous/next versions from the versioned model itself + # + # version = page.versions.latest + # version.previous # go back one version + # version.next # go forward one version + # # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options module Versioned - CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes] + CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes] def self.included(base) # :nodoc: base.extend ClassMethods end @@ -80,7 +95,7 @@ module ActiveRecord #:nodoc: # end # # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes - # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have. + # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have. # Use this instead if you want to write your own attribute setters (and ignore if_changed): # # def name=(new_name) @@ -133,7 +148,7 @@ module ActiveRecord #:nodoc: # # that create_table does # Post.create_versioned_table # end - # + # # def self.down # Post.drop_versioned_table # end @@ -157,11 +172,11 @@ module ActiveRecord #:nodoc: return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods) send :include, ActiveRecord::Acts::Versioned::ActMethods - + cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column, - :version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name, :non_versioned_columns, + :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns, :version_association_options - + # legacy alias_method :non_versioned_fields, :non_versioned_columns alias_method :non_versioned_fields=, :non_versioned_columns= @@ -171,7 +186,7 @@ module ActiveRecord #:nodoc: alias_method :non_versioned_fields=, :non_versioned_columns= end - send :attr_accessor, :changed_attributes + send :attr_accessor, :altered_attributes self.versioned_class_name = options[:class_name] || "Version" self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key @@ -184,8 +199,7 @@ module ActiveRecord #:nodoc: self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column] self.version_association_options = { :class_name => "#{self.to_s}::#{versioned_class_name}", - :foreign_key => "#{versioned_foreign_key}", - :order => 'version', + :foreign_key => versioned_foreign_key, :dependent => :delete_all }.merge(options[:association_options] || {}) @@ -194,20 +208,30 @@ module ActiveRecord #:nodoc: silence_warnings do self.const_set(extension_module_name, Module.new(&extension)) end - + options[:extend] = self.const_get(extension_module_name) end class_eval do - has_many :versions, version_association_options + has_many :versions, version_association_options do + # finds earliest version of this record + def earliest + @earliest ||= find(:first, :order => 'version') + end + + # find latest version of this record + def latest + @latest ||= find(:first, :order => 'version desc') + end + end before_save :set_new_version after_create :save_version_on_create after_update :save_version after_save :clear_old_versions - after_save :clear_changed_attributes - + after_save :clear_altered_attributes + unless options[:if_changed].nil? - self.track_changed_attributes = true + self.track_altered_attributes = true options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array) options[:if_changed].each do |attr_name| define_method("#{attr_name}=") do |value| @@ -215,15 +239,40 @@ module ActiveRecord #:nodoc: end end end - + include options[:extend] if options[:extend].is_a?(Module) end # create the dynamic versioned model const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do def self.reloadable? ; false ; end + # find first version before the given version + def self.before(version) + find :first, :order => 'version desc', + :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version] + end + + # find first version after the given version. + def self.after(version) + find :first, :order => 'version', + :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version] + end + + def previous + self.class.before(self) + end + + def next + self.class.after(self) + end + + def versions_count + page.version + end end - + + versioned_class.cattr_accessor :original_class + versioned_class.original_class = self versioned_class.set_table_name versioned_table_name versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym, :class_name => "::#{self.to_s}", @@ -232,17 +281,22 @@ module ActiveRecord #:nodoc: versioned_class.set_sequence_name version_sequence_name if version_sequence_name end end - + module ActMethods def self.included(base) # :nodoc: base.extend ClassMethods end - + + # Finds a specific version of this record + def find_version(version = nil) + self.class.find_version(id, version) + end + # Saves a version of the model if applicable def save_version save_version_on_create if save_version? end - + # Saves a version of the model in the versioned table. This is called in the after_save callback by default def save_version_on_create rev = self.class.versioned_class.new @@ -263,16 +317,8 @@ module ActiveRecord #:nodoc: end end - # Finds a specific version of this model. - def find_version(version) - return version if version.is_a?(self.class.versioned_class) - return nil if version.is_a?(ActiveRecord::Base) - find_versions(:conditions => ['version = ?', version], :limit => 1).first - end - - # Finds versions of this model. Takes an options hash like <tt>find</tt> - def find_versions(options = {}) - versions.find(:all, options) + def versions_count + version end # Reverts a model to a given version. Takes either a version number or an instance of the versioned model @@ -280,14 +326,14 @@ module ActiveRecord #:nodoc: if version.is_a?(self.class.versioned_class) return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record? else - return false unless version = find_version(version) + return false unless version = versions.find_by_version(version) end self.clone_versioned_model(version, self) self.send("#{self.class.version_column}=", version.version) true end - # Reverts a model to a given version and saves the model. + # Reverts a model to a given version and saves the model. # Takes either a version number or an instance of the versioned model def revert_to!(version) revert_to(version) ? save_without_revision : false @@ -313,36 +359,36 @@ module ActiveRecord #:nodoc: def versioned_attributes self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) } end - + # If called with no parameters, gets whether the current model has changed and needs to be versioned. # If called with a single parameter, gets whether the parameter has changed. def changed?(attr_name = nil) attr_name.nil? ? - (!self.class.track_changed_attributes || (changed_attributes && changed_attributes.length > 0)) : - (changed_attributes && changed_attributes.include?(attr_name.to_s)) + (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) : + (altered_attributes && altered_attributes.include?(attr_name.to_s)) end - + # keep old dirty? method alias_method :dirty?, :changed? - + # Clones a model. Used when saving a new version or reverting a model's version. def clone_versioned_model(orig_model, new_model) self.versioned_attributes.each do |key| - new_model.send("#{key}=", orig_model.attributes[key]) if orig_model.has_attribute?(key) + new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key) end - + if orig_model.is_a?(self.class.versioned_class) new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column] elsif new_model.is_a?(self.class.versioned_class) new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column] end end - + # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>. def save_version? version_condition_met? && changed? end - + # Checks condition set in the :if option to check whether a revision should be created or not. Override this for # custom version condition checking. def version_condition_met? @@ -353,7 +399,7 @@ module ActiveRecord #:nodoc: version_condition.call(self) else version_condition - end + end end # Executes the block with the versioning callbacks disabled. @@ -378,43 +424,45 @@ module ActiveRecord #:nodoc: def empty_callback() end #:nodoc: - protected + protected # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version. def set_new_version self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?) end - + # Gets the next available version for the current record, or 1 for a new record def next_version return 1 if new_record? (versions.calculate(:max, :version) || 0) + 1 end - + # clears current changed attributes. Called after save. - def clear_changed_attributes - self.changed_attributes = [] + def clear_altered_attributes + self.altered_attributes = [] end def write_changed_attribute(attr_name, attr_value) # Convert to db type for comparison. Avoids failing Float<=>String comparisons. attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value) - (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db + (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db write_attribute(attr_name, attr_value_for_db) end - private - CALLBACKS.each do |attr_name| - alias_method "orig_#{attr_name}".to_sym, attr_name - end - module ClassMethods # Finds a specific version of a specific row of this model - def find_version(id, version) - find_versions(id, - :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version], - :limit => 1).first + def find_version(id, version = nil) + return find(id) unless version + + conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version] + options = { :conditions => conditions, :limit => 1 } + + if result = find_versions(id, options).first + result + else + raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}" + end end - + # Finds versions of a specific model. Takes an options hash like <tt>find</tt> def find_versions(id, options = {}) versioned_class.find :all, { @@ -426,7 +474,7 @@ module ActiveRecord #:nodoc: def versioned_columns self.columns.select { |c| !non_versioned_columns.include?(c.name) } end - + # Returns an instance of the dynamic versioned model def versioned_class const_get versioned_class_name @@ -438,36 +486,40 @@ module ActiveRecord #:nodoc: if !self.content_columns.find { |c| %w(version lock_version).include? c.name } self.connection.add_column table_name, :version, :integer end - + self.connection.create_table(versioned_table_name, create_table_options) do |t| t.column versioned_foreign_key, :integer t.column :version, :integer end - + updated_col = nil self.versioned_columns.each do |col| updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name) self.connection.add_column versioned_table_name, col.name, col.type, :limit => col.limit, - :default => col.default + :default => col.default, + :scale => col.scale, + :precision => col.precision end - + if type_col = self.columns_hash[inheritance_column] self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type, :limit => type_col.limit, - :default => type_col.default + :default => type_col.default, + :scale => type_col.scale, + :precision => type_col.precision end - + if updated_col.nil? self.connection.add_column versioned_table_name, :updated_at, :timestamp end end - + # Rake migration task to drop the versioned table def drop_versioned_table self.connection.drop_table versioned_table_name end - + # Executes the block with the versioning callbacks disabled. # # Foo.without_revision do @@ -476,17 +528,18 @@ module ActiveRecord #:nodoc: # def without_revision(&block) class_eval do - CALLBACKS.each do |attr_name| + CALLBACKS.each do |attr_name| + alias_method "orig_#{attr_name}".to_sym, attr_name alias_method attr_name, :empty_callback end end - result = block.call + block.call + ensure class_eval do CALLBACKS.each do |attr_name| alias_method attr_name, "orig_#{attr_name}".to_sym end end - result end # Turns off optimistic locking for the duration of the block @@ -501,7 +554,7 @@ module ActiveRecord #:nodoc: result = block.call ActiveRecord::Base.lock_optimistically = true if current result - end + end end end end diff --git a/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb b/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb index 1740db8dc..86f50620c 100644 --- a/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb +++ b/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb @@ -1,12 +1,21 @@ +$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib') +$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib') $:.unshift(File.dirname(__FILE__) + '/../lib') - require 'test/unit' -require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb')) -require 'active_record/fixtures' +begin + require 'active_support' + require 'active_record' + require 'active_record/fixtures' +rescue LoadError + require 'rubygems' + retry +end +require 'acts_as_versioned' config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") -ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite']) +ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']} +ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) load(File.dirname(__FILE__) + "/schema.rb") @@ -19,17 +28,9 @@ if ENV['DB'] == 'postgresql' end Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" -$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path) +$:.unshift(Test::Unit::TestCase.fixture_path) class Test::Unit::TestCase #:nodoc: - def create_fixtures(*table_names) - if block_given? - Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } - else - Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) - end - end - # Turn off transactional fixtures if you're working with MyISAM tables in MySQL self.use_transactional_fixtures = true diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb b/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb index 3c38f2fcf..086ac2b40 100644 --- a/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb +++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb @@ -1,6 +1,6 @@ class Widget < ActiveRecord::Base acts_as_versioned :sequence_name => 'widgets_seq', :association_options => { - :dependent => nil, :order => 'version desc' + :dependent => :nullify, :order => 'version desc' } non_versioned_columns << 'foo' end
\ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb b/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb index d85e95883..4ead4a8fe 100644 --- a/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb +++ b/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb @@ -9,9 +9,14 @@ if ActiveRecord::Base.connection.supports_migrations? class MigrationTest < Test::Unit::TestCase self.use_transactional_fixtures = false def teardown - ActiveRecord::Base.connection.initialize_schema_information - ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0" - + if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information) + ActiveRecord::Base.connection.initialize_schema_information + ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0" + else + ActiveRecord::Base.connection.initialize_schema_migrations_table + ActiveRecord::Base.connection.assume_migrated_upto_version(0) + end + Thing.connection.drop_table "things" rescue nil Thing.connection.drop_table "thing_versions" rescue nil Thing.reset_column_information @@ -21,8 +26,17 @@ if ActiveRecord::Base.connection.supports_migrations? assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' } # take 'er up ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/') - t = Thing.create :title => 'blah blah' + t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing' assert_equal 1, t.versions.size + + # check that the price column has remembered its value correctly + assert_equal t.price, t.versions.first.price + assert_equal t.title, t.versions.first.title + assert_equal t[:type], t.versions.first[:type] + + # make sure that the precision of the price column has been preserved + assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision + assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale # now lets take 'er back down ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/') diff --git a/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb b/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb index c1e1a4b98..a7bc2082b 100644 --- a/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb +++ b/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb @@ -4,9 +4,10 @@ require File.join(File.dirname(__FILE__), 'fixtures/widget') class VersionedTest < Test::Unit::TestCase fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions + set_fixture_class :page_versions => Page::Version def test_saves_versioned_copy - p = Page.create :title => 'first title', :body => 'first body' + p = Page.create! :title => 'first title', :body => 'first body' assert !p.new_record? assert_equal 1, p.versions.size assert_equal 1, p.version @@ -16,13 +17,13 @@ class VersionedTest < Test::Unit::TestCase def test_saves_without_revision p = pages(:welcome) old_versions = p.versions.count - + p.save_without_revision - + p.without_revision do p.update_attributes :title => 'changed' end - + assert_equal old_versions, p.versions.count end @@ -30,7 +31,7 @@ class VersionedTest < Test::Unit::TestCase p = pages(:welcome) assert_equal 24, p.version assert_equal 'Welcome to the weblog', p.title - + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23" assert_equal 23, p.version assert_equal 'Welcome to the weblg', p.title @@ -57,56 +58,56 @@ class VersionedTest < Test::Unit::TestCase p = pages(:welcome) assert_equal 24, p.version assert_equal 'Welcome to the weblog', p.title - + assert p.revert_to!(p.versions.first), "Couldn't revert to 23" assert_equal 23, p.version assert_equal 'Welcome to the weblg', p.title end - + def test_rollback_fails_with_invalid_revision p = locked_pages(:welcome) assert !p.revert_to!(locked_pages(:thinking)) end def test_saves_versioned_copy_with_options - p = LockedPage.create :title => 'first title' + p = LockedPage.create! :title => 'first title' assert !p.new_record? assert_equal 1, p.versions.size assert_instance_of LockedPage.versioned_class, p.versions.first end - + def test_rollback_with_version_number_with_options p = locked_pages(:welcome) assert_equal 'Welcome to the weblog', p.title assert_equal 'LockedPage', p.versions.first.version_type - + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23" assert_equal 'Welcome to the weblg', p.title assert_equal 'LockedPage', p.versions.first.version_type end - + def test_rollback_with_version_class_with_options p = locked_pages(:welcome) assert_equal 'Welcome to the weblog', p.title assert_equal 'LockedPage', p.versions.first.version_type - + assert p.revert_to!(p.versions.first), "Couldn't revert to 1" assert_equal 'Welcome to the weblg', p.title assert_equal 'LockedPage', p.versions.first.version_type end - + def test_saves_versioned_copy_with_sti - p = SpecialLockedPage.create :title => 'first title' + p = SpecialLockedPage.create! :title => 'first title' assert !p.new_record? assert_equal 1, p.versions.size assert_instance_of LockedPage.versioned_class, p.versions.first assert_equal 'SpecialLockedPage', p.versions.first.version_type end - + def test_rollback_with_version_number_with_sti p = locked_pages(:thinking) assert_equal 'So I was thinking', p.title - + assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1" assert_equal 'So I was thinking!!!', p.title assert_equal 'SpecialLockedPage', p.versions.first.version_type @@ -115,11 +116,11 @@ class VersionedTest < Test::Unit::TestCase def test_lock_version_works_with_versioning p = locked_pages(:thinking) p2 = LockedPage.find(p.id) - + p.title = 'fresh title' p.save assert_equal 2, p.versions.size # limit! - + assert_raises(ActiveRecord::StaleObjectError) do p2.title = 'stale title' p2.save @@ -127,15 +128,15 @@ class VersionedTest < Test::Unit::TestCase end def test_version_if_condition - p = Page.create :title => "title" + p = Page.create! :title => "title" assert_equal 1, p.version - + Page.feeling_good = false p.save assert_equal 1, p.version Page.feeling_good = true end - + def test_version_if_condition2 # set new if condition Page.class_eval do @@ -143,46 +144,46 @@ class VersionedTest < Test::Unit::TestCase alias_method :old_feeling_good, :feeling_good? alias_method :feeling_good?, :new_feeling_good end - - p = Page.create :title => "title" + + p = Page.create! :title => "title" assert_equal 1, p.version # version does not increment assert_equal 1, p.versions(true).size - + p.update_attributes(:title => 'new title') assert_equal 1, p.version # version does not increment assert_equal 1, p.versions(true).size - + p.update_attributes(:title => 'a title') assert_equal 2, p.version assert_equal 2, p.versions(true).size - + # reset original if condition Page.class_eval { alias_method :feeling_good?, :old_feeling_good } end - + def test_version_if_condition_with_block # set new if condition old_condition = Page.version_condition Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' } - - p = Page.create :title => "title" + + p = Page.create! :title => "title" assert_equal 1, p.version # version does not increment assert_equal 1, p.versions(true).size - + p.update_attributes(:title => 'a title') assert_equal 1, p.version # version does not increment assert_equal 1, p.versions(true).size - + p.update_attributes(:title => 'b title') assert_equal 2, p.version assert_equal 2, p.versions(true).size - + # reset original if condition Page.version_condition = old_condition end def test_version_no_limit - p = Page.create :title => "title", :body => 'first body' + p = Page.create! :title => "title", :body => 'first body' p.save p.save 5.times do |i| @@ -191,7 +192,7 @@ class VersionedTest < Test::Unit::TestCase end def test_version_max_limit - p = LockedPage.create :title => "title" + p = LockedPage.create! :title => "title" p.update_attributes(:title => "title1") p.update_attributes(:title => "title2") 5.times do |i| @@ -199,31 +200,29 @@ class VersionedTest < Test::Unit::TestCase assert p.versions(true).size <= 2, "locked version can only store 2 versions" end end - - def test_track_changed_attributes_default_value - assert !Page.track_changed_attributes - assert LockedPage.track_changed_attributes - assert SpecialLockedPage.track_changed_attributes + + def test_track_altered_attributes_default_value + assert !Page.track_altered_attributes + assert LockedPage.track_altered_attributes + assert SpecialLockedPage.track_altered_attributes end - + def test_version_order assert_equal 23, pages(:welcome).versions.first.version assert_equal 24, pages(:welcome).versions.last.version - assert_equal 23, pages(:welcome).find_versions.first.version - assert_equal 24, pages(:welcome).find_versions.last.version end - - def test_track_changed_attributes - p = LockedPage.create :title => "title" + + def test_track_altered_attributes + p = LockedPage.create! :title => "title" assert_equal 1, p.lock_version assert_equal 1, p.versions(true).size - + p.title = 'title' assert !p.save_version? p.save assert_equal 2, p.lock_version # still increments version because of optimistic locking assert_equal 1, p.versions(true).size - + p.title = 'updated title' assert p.save_version? p.save @@ -236,27 +235,38 @@ class VersionedTest < Test::Unit::TestCase assert_equal 4, p.lock_version assert_equal 2, p.versions(true).size # version 1 deleted end - + def assert_page_title(p, i, version_field = :version) p.title = "title#{i}" p.save assert_equal "title#{i}", p.title assert_equal (i+4), p.send(version_field) end - + def test_find_versions assert_equal 2, locked_pages(:welcome).versions.size - assert_equal 1, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%weblog%']).length - assert_equal 2, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%web%']).length - assert_equal 0, locked_pages(:thinking).find_versions(:conditions => ['title LIKE ?', '%web%']).length - assert_equal 2, locked_pages(:welcome).find_versions.length + assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).length + assert_equal 2, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length + assert_equal 0, locked_pages(:thinking).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length + assert_equal 2, locked_pages(:welcome).versions.length + end + + def test_find_version + assert_equal page_versions(:welcome_1), Page.find_version(pages(:welcome).id, 23) + assert_equal page_versions(:welcome_2), Page.find_version(pages(:welcome).id, 24) + assert_equal pages(:welcome), Page.find_version(pages(:welcome).id) + + assert_equal page_versions(:welcome_1), pages(:welcome).find_version(23) + assert_equal page_versions(:welcome_2), pages(:welcome).find_version(24) + assert_equal pages(:welcome), pages(:welcome).find_version + + assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(pages(:welcome).id, 1) } + assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(0, 23) } end - + def test_with_sequence assert_equal 'widgets_seq', Widget.versioned_class.sequence_name - Widget.create :name => 'new widget' - Widget.create :name => 'new widget' - Widget.create :name => 'new widget' + 3.times { Widget.create! :name => 'new widget' } assert_equal 3, Widget.count assert_equal 3, Widget.versioned_class.count end @@ -268,26 +278,26 @@ class VersionedTest < Test::Unit::TestCase def test_has_many_through_with_custom_association assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors end - + def test_referential_integrity pages(:welcome).destroy assert_equal 0, Page.count assert_equal 0, Page::Version.count end - + def test_association_options association = Page.reflect_on_association(:versions) options = association.options assert_equal :delete_all, options[:dependent] assert_equal 'version', options[:order] - + association = Widget.reflect_on_association(:versions) options = association.options - assert_nil options[:dependent] + assert_equal :nullify, options[:dependent] assert_equal 'version desc', options[:order] assert_equal 'widget_id', options[:foreign_key] - - widget = Widget.create :name => 'new widget' + + widget = Widget.create! :name => 'new widget' assert_equal 1, Widget.count assert_equal 1, Widget.versioned_class.count widget.destroy @@ -300,14 +310,38 @@ class VersionedTest < Test::Unit::TestCase page_version = page.versions.last assert_equal page, page_version.page end - - def test_unchanged_attributes - landmarks(:washington).attributes = landmarks(:washington).attributes + + def test_unaltered_attributes + landmarks(:washington).attributes = landmarks(:washington).attributes.except("id") assert !landmarks(:washington).changed? end - + def test_unchanged_string_attributes - landmarks(:washington).attributes = landmarks(:washington).attributes.inject({}) { |params, (key, value)| params.update key => value.to_s } + landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) } assert !landmarks(:washington).changed? end -end + + def test_should_find_earliest_version + assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest + end + + def test_should_find_latest_version + assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest + end + + def test_should_find_previous_version + assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous + assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2)) + end + + def test_should_find_next_version + assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next + assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1)) + end + + def test_should_find_version_count + assert_equal 24, pages(:welcome).versions_count + assert_equal 24, page_versions(:welcome_1).versions_count + assert_equal 24, page_versions(:welcome_2).versions_count + end +end
\ No newline at end of file diff --git a/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb b/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb index 53e4455cf..2cb122795 100644 --- a/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb +++ b/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb @@ -13,6 +13,7 @@ module Redmine class_eval do has_many :watchers, :as => :watchable, :dependent => :delete_all + has_many :watcher_users, :through => :watchers, :source => :user end end end @@ -22,25 +23,40 @@ module Redmine base.extend ClassMethods end + # Returns an array of users that are proposed as watchers + def addable_watcher_users + self.project.users.sort - self.watcher_users + end + + # Adds user as a watcher def add_watcher(user) self.watchers << Watcher.new(:user => user) end + # Removes user from the watchers list def remove_watcher(user) return nil unless user && user.is_a?(User) Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}" end + # Adds/removes watcher + def set_watcher(user, watching=true) + watching ? add_watcher(user) : remove_watcher(user) + end + + # Returns if object is watched by user def watched_by?(user) !self.watchers.find(:first, :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil? end + # Returns an array of watchers' email addresses def watcher_recipients self.watchers.collect { |w| w.user.mail if w.user.active? }.compact end module ClassMethods + # Returns the objects that are watched by user def watched_by(user) find(:all, :include => :watchers, @@ -50,4 +66,4 @@ module Redmine end end end -end
\ No newline at end of file +end diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb index f6d71ade2..e5d87b233 100644 --- a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb +++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb @@ -122,7 +122,7 @@ module Scanners end when :include_expected - if scan(/<[^>\n]+>?|"[^"\n\\]*(?:\\.[^"\n\\]*)*"?/) + if scan(/[^\n]+/) kind = :include state = :initial diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb index f437410dc..51c024838 100644 --- a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb @@ -138,8 +138,8 @@ module ActionView #:nodoc: def add_options(option_tags, options, value = nil)
option_tags = "<option value=\"\"></option>\n" + option_tags if options[:include_blank]
- if value.blank? && options[:prompt]
- ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : l(:actionview_instancetag_blank_option)}</option>\n") + option_tags
+ if options[:prompt]
+ ("<option value=\"\">--- #{options[:prompt].kind_of?(String) ? options[:prompt] : l(:actionview_instancetag_blank_option)} ---</option>\n") + option_tags
else
option_tags
end
diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb index aa65991b0..f05c4cddb 100644 --- a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb +++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb @@ -168,7 +168,7 @@ module ActiveRecord #:nodoc: if attr == "base"
full_messages << (msg.is_a?(Symbol) ? l(msg) : msg)
else
- full_messages << @base.class.human_attribute_name(attr) + " " + (msg.is_a?(Symbol) ? l(msg) : msg)
+ full_messages << @base.class.human_attribute_name(attr) + " " + (msg.is_a?(Symbol) ? l(msg) : msg.to_s)
end
end
end
diff --git a/groups/vendor/plugins/rfpdf/init.rb b/groups/vendor/plugins/rfpdf/init.rb index 7e51d9eba..339bacfdb 100644 --- a/groups/vendor/plugins/rfpdf/init.rb +++ b/groups/vendor/plugins/rfpdf/init.rb @@ -1,3 +1,9 @@ require 'rfpdf' -ActionView::Base::register_template_handler 'rfpdf', RFPDF::View
\ No newline at end of file +begin + ActionView::Template::register_template_handler 'rfpdf', RFPDF::View +rescue NameError + # Rails < 2.1 + RFPDF::View.backward_compatibility_mode = true + ActionView::Base::register_template_handler 'rfpdf', RFPDF::View +end diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb index 6fe3eee8a..5684c702d 100644 --- a/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb @@ -1,473 +1,473 @@ -# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com> -# 1.12 contributed by Ed Moss. -# -# The MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# This is direct port of 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(i<nb) - c=s[i] - if(c<128) - l+=cw[c.chr] if cw[c.chr] - i+=1 - else - 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') - MBMultiCell(w,h,txt,border,align,fill) - else - super(w,h,txt,border,align,fill) - end - end - - def MBMultiCell(w,h,txt,border=0,align='L',fill=0) - #Multi-byte version of MultiCell() - cw=@CurrentFont['cw'] - if(w==0) - w=@w-@rMargin-@x - end - wmax=(w-2*@cMargin)*1000/@FontSize - s=txt.gsub("\r",'') - nb=s.length - if(nb>0 and s[nb-1]=="\n") - nb-=1 - end - b=0 - if(border) - if(border==1) - border='LTRB' - b='LRT' - b2='LR' - else - b2='' - if(border.to_s.index('L')) - b2+='L' - end - if(border.to_s.index('R')) - b2+='R' - end - b=border.to_s.index('T') ? b2+'T' : b2 - end - end - sep=-1 - i=0 - j=0 - l=0 - nl=1 - while(i<nb) - #Get next character - c=s[i] - #Check if ASCII or MB - ascii=(c<128) - if(c=="\n") - #Explicit line break - Cell(w,h,s[j,i-j],b,2,align,fill) - i+=1 - sep=-1 - j=i - l=0 - nl+=1 - if(border and nl==2) - b=b2 - end - next - end - if(!ascii) - sep=i - ls=l - elsif(c==' ') - sep=i - ls=l - end - l+=ascii ? (cw[c.chr] || 0) : 1000 - if(l>wmax) - #Automatic line break - if(sep==-1 or i==j) - if(i==j) - i+=ascii ? 1 : 2 - end - Cell(w,h,s[j,i-j],b,2,align,fill) - else - Cell(w,h,s[j,sep-j],b,2,align,fill) - i=(s[sep]==' ') ? sep+1 : sep - end - sep=-1 - j=i - l=0 -# nl+=1 - if(border and nl==2) - b=b2 - end - else - i+=ascii ? 1 : 2 - end - end - #Last chunk - if(border and not border.to_s.index('B').nil?) - b+='B' - end - Cell(w,h,s[j,i-j],b,2,align,fill) - @x=@lMargin - end - - def Write(h,txt,link='') - if(@CurrentFont['type']=='Type0') - MBWrite(h,txt,link) - else - super(h,txt,link) - end - end - - def MBWrite(h,txt,link) - #Multi-byte version of Write() - cw=@CurrentFont['cw'] - w=@w-@rMargin-@x - wmax=(w-2*@cMargin)*1000/@FontSize - s=txt.gsub("\r",'') - nb=s.length - sep=-1 - i=0 - j=0 - l=0 - nl=1 - while(i<nb) - #Get next character - c=s[i] - #Check if ASCII or MB - ascii=(c<128) - if(c=="\n") - #Explicit line break - Cell(w,h,s[j,i-j],0,2,'',0,link) - i+=1 - sep=-1 - j=i - l=0 - if(nl==1) - @x=@lMargin - w=@w-@rMargin-@x - wmax=(w-2*@cMargin)*1000/@FontSize - end - nl+=1 - next - end - if(!ascii or c==' ') - sep=i - end - l+=ascii ? cw[c.chr] : 1000 - 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+=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('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>') - out('endobj') - end - # mqr=get_magic_quotes_runtime() - # set_magic_quotes_runtime(0) - @FontFiles.each_pair do |file, info| - #Font file embedding - newobj() - @FontFiles[file]['n']=@n - if(defined('FPDF_FONTPATH')) - file=FPDF_FONTPATH+file - end - size=filesize(file) - if(!size) - Error('Font file not found') - end - out('<</Length '+size) - if(file[-2]=='.z') - out('/Filter /FlateDecode') - end - out('/Length1 '+info['length1']) - unless info['length2'].nil? - out('/Length2 '+info['length2']+' /Length3 0') - end - out('>>') - f=fopen(file,'rb') - putstream(fread(f,size)) - fclose(f) - out('endobj') - end -# - # set_magic_quotes_runtime(mqr) -# - @fonts.each_pair do |k, font| - #Font objects - newobj() - @fonts[k]['n']=@n - out('<</Type /Font') - if(font['type']=='Type0') - putType0(font) - else - name=font['name'] - out('/BaseFont /'+name) - if(font['type']=='core') - #Standard font - out('/Subtype /Type1') - if(name!='Symbol' and name!='ZapfDingbats') - out('/Encoding /WinAnsiEncoding') - end - else - #Additional font - out('/Subtype /'+font['type']) - out('/FirstChar 32') - out('/LastChar 255') - out('/Widths '+(@n+1)+' 0 R') - out('/FontDescriptor '+(@n+2)+' 0 R') - if(font['enc']) - if !font['diff'].nil? - out('/Encoding '+(nf+font['diff'])+' 0 R') - else - out('/Encoding /WinAnsiEncoding') - end - end - end - out('>>') - out('endobj') - if(font['type']!='core') - #Widths - newobj() - cw=font['cw'] - s='[' - 32.upto(255) do |i| - s+=cw[i.chr]+' ' - end - out(s+']') - out('endobj') - #Descriptor - newobj() - s='<</Type /FontDescriptor /FontName /'+name - font['desc'].each_pair do |k, v| - s+=' /'+k+' '+v - end - file=font['file'] - if(file) - s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R' - end - out(s+'>>') - out('endobj') - end - end - end - end - - def putType0(font) - #Type0 - out('/Subtype /Type0') - out('/BaseFont /'+font['name']+'-'+font['CMap']) - out('/Encoding /'+font['CMap']) - out('/DescendantFonts ['+(@n+1).to_s+' 0 R]') - out('>>') - out('endobj') - #CIDFont - newobj() - out('<</Type /Font') - out('/Subtype /CIDFontType0') - out('/BaseFont /'+font['name']) - out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>') - 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('<</Type /FontDescriptor') - out('/FontName /'+font['name']) - out('/Flags 6') - out('/FontBBox [0 -200 1000 900]') - out('/ItalicAngle 0') - out('/Ascent 800') - out('/Descent -200') - out('/CapHeight 800') - out('/StemV 50') - out('>>') - out('endobj') - end -end +# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
+# 1.12 contributed by Ed Moss.
+#
+# The MIT License
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# This is direct port of 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(i<nb)
+ c=s[i]
+ if(c<128)
+ l+=cw[c.chr] if cw[c.chr]
+ i+=1
+ else
+ 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')
+ MBMultiCell(w,h,txt,border,align,fill)
+ else
+ super(w,h,txt,border,align,fill)
+ end
+ end
+
+ def MBMultiCell(w,h,txt,border=0,align='L',fill=0)
+ #Multi-byte version of MultiCell()
+ cw=@CurrentFont['cw']
+ if(w==0)
+ w=@w-@rMargin-@x
+ end
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ if(nb>0 and s[nb-1]=="\n")
+ nb-=1
+ end
+ b=0
+ if(border)
+ if(border==1)
+ border='LTRB'
+ b='LRT'
+ b2='LR'
+ else
+ b2=''
+ if(border.to_s.index('L'))
+ b2+='L'
+ end
+ if(border.to_s.index('R'))
+ b2+='R'
+ end
+ b=border.to_s.index('T') ? b2+'T' : b2
+ end
+ end
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while(i<nb)
+ #Get next character
+ c=s[i]
+ #Check if ASCII or MB
+ ascii=(c<128)
+ if(c.chr=="\n")
+ #Explicit line break
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ i+=1
+ sep=-1
+ j=i
+ l=0
+ nl+=1
+ if(border and nl==2)
+ b=b2
+ end
+ next
+ end
+ if(!ascii)
+ sep=i
+ ls=l
+ elsif(c==' ')
+ sep=i
+ ls=l
+ end
+ l+=ascii ? (cw[c.chr] || 0) : 1100
+ if(l>wmax)
+ #Automatic line break
+ if(sep==-1 or i==j)
+ if(i==j)
+ i+=ascii ? 1 : 3
+ 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 : 3
+ end
+ end
+ #Last chunk
+ if(border and not border.to_s.index('B').nil?)
+ b+='B'
+ end
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ @x=@lMargin
+ end
+
+ def Write(h,txt,link='')
+ if(@CurrentFont['type']=='Type0')
+ MBWrite(h,txt,link)
+ else
+ super(h,txt,link)
+ end
+ end
+
+ def MBWrite(h,txt,link)
+ #Multi-byte version of Write()
+ cw=@CurrentFont['cw']
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while(i<nb)
+ #Get next character
+ c=s[i]
+ #Check if ASCII or MB
+ ascii=(c<128)
+ if(c.chr=="\n")
+ #Explicit line break
+ Cell(w,h,s[j,i-j],0,2,'',0,link)
+ i+=1
+ sep=-1
+ j=i
+ l=0
+ if(nl==1)
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl+=1
+ next
+ end
+ if(!ascii or c==' ')
+ sep=i
+ end
+ l+=ascii ? cw[c.chr] : 1100
+ 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+=1
+ nl+=1
+ next
+ end
+ if(i==j)
+ i+=ascii ? 1 : 3
+ 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 : 3
+ end
+ end
+ #Last chunk
+ if(i!=j)
+ Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link)
+ end
+ end
+
+private
+
+ def putfonts()
+ nf=@n
+ @diffs.each do |diff|
+ #Encodings
+ newobj()
+ out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
+ out('endobj')
+ end
+ # mqr=get_magic_quotes_runtime()
+ # set_magic_quotes_runtime(0)
+ @FontFiles.each_pair do |file, info|
+ #Font file embedding
+ newobj()
+ @FontFiles[file]['n']=@n
+ if(defined('FPDF_FONTPATH'))
+ file=FPDF_FONTPATH+file
+ end
+ size=filesize(file)
+ if(!size)
+ Error('Font file not found')
+ end
+ out('<</Length '+size)
+ if(file[-2]=='.z')
+ out('/Filter /FlateDecode')
+ end
+ out('/Length1 '+info['length1'])
+ unless info['length2'].nil?
+ out('/Length2 '+info['length2']+' /Length3 0')
+ end
+ out('>>')
+ f=fopen(file,'rb')
+ putstream(fread(f,size))
+ fclose(f)
+ out('endobj')
+ end
+#
+ # set_magic_quotes_runtime(mqr)
+#
+ @fonts.each_pair do |k, font|
+ #Font objects
+ newobj()
+ @fonts[k]['n']=@n
+ out('<</Type /Font')
+ if(font['type']=='Type0')
+ putType0(font)
+ else
+ name=font['name']
+ out('/BaseFont /'+name)
+ if(font['type']=='core')
+ #Standard font
+ out('/Subtype /Type1')
+ if(name!='Symbol' and name!='ZapfDingbats')
+ out('/Encoding /WinAnsiEncoding')
+ end
+ else
+ #Additional font
+ out('/Subtype /'+font['type'])
+ out('/FirstChar 32')
+ out('/LastChar 255')
+ out('/Widths '+(@n+1)+' 0 R')
+ out('/FontDescriptor '+(@n+2)+' 0 R')
+ if(font['enc'])
+ if !font['diff'].nil?
+ out('/Encoding '+(nf+font['diff'])+' 0 R')
+ else
+ out('/Encoding /WinAnsiEncoding')
+ end
+ end
+ end
+ out('>>')
+ out('endobj')
+ if(font['type']!='core')
+ #Widths
+ newobj()
+ cw=font['cw']
+ s='['
+ 32.upto(255) do |i|
+ s+=cw[i.chr]+' '
+ end
+ out(s+']')
+ out('endobj')
+ #Descriptor
+ newobj()
+ s='<</Type /FontDescriptor /FontName /'+name
+ font['desc'].each_pair do |k, v|
+ s+=' /'+k+' '+v
+ end
+ file=font['file']
+ if(file)
+ s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R'
+ end
+ out(s+'>>')
+ out('endobj')
+ end
+ end
+ end
+ end
+
+ def putType0(font)
+ #Type0
+ out('/Subtype /Type0')
+ out('/BaseFont /'+font['name']+'-'+font['CMap'])
+ out('/Encoding /'+font['CMap'])
+ out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
+ out('>>')
+ out('endobj')
+ #CIDFont
+ newobj()
+ out('<</Type /Font')
+ out('/Subtype /CIDFontType0')
+ out('/BaseFont /'+font['name'])
+ out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>')
+ 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('<</Type /FontDescriptor')
+ out('/FontName /'+font['name'])
+ out('/Flags 6')
+ out('/FontBBox [0 -200 1000 900]')
+ out('/ItalicAngle 0')
+ out('/Ascent 800')
+ out('/Descent -200')
+ out('/CapHeight 800')
+ out('/StemV 50')
+ out('>>')
+ out('endobj')
+ end
+end
diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb index 185811202..6b6267331 100644 --- a/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb +++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb @@ -30,6 +30,8 @@ module RFPDF class View + @@backward_compatibility_mode = false + cattr_accessor :backward_compatibility_mode def initialize(action_view) @action_view = action_view @@ -45,6 +47,14 @@ module RFPDF :temp_dir => "#{File.expand_path(RAILS_ROOT)}/tmp" }.merge(@action_view.controller.instance_eval{ @options_for_rfpdf } || {}).with_indifferent_access end + + def self.compilable? + false + end + + def compilable? + self.class.compilable? + end def render(template, local_assigns = {}) @pdf_name = "Default.pdf" if @pdf_name.nil? @@ -66,7 +76,7 @@ module RFPDF local_assigns.each do |key,val| class << self; self; end.send(:define_method,key){ val } end - ERB.new(template).result(binding) + ERB.new(@@backward_compatibility_mode == true ? template : template.source).result(binding) end end |