From bb2de53d93adb517fbfcfb37e33adc959b2848b3 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 18 Nov 2007 14:32:39 +0000 Subject: [PATCH 001/710] Added an alternate theme which provides issue list colorization based on issues priority. git-svn-id: http://redmine.rubyforge.org/svn/trunk@912 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/show.rhtml | 2 +- .../alternate/stylesheets/application.css | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 public/themes/alternate/stylesheets/application.css diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 0745f01f3..6e4f22574 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -10,7 +10,7 @@

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

-
+
">

<%=h @issue.subject %>

<%= authoring @issue.created_on, @issue.author %>. diff --git a/public/themes/alternate/stylesheets/application.css b/public/themes/alternate/stylesheets/application.css new file mode 100644 index 000000000..ced63a418 --- /dev/null +++ b/public/themes/alternate/stylesheets/application.css @@ -0,0 +1,68 @@ +@import url(../../../stylesheets/application.css); + +body { background-color:#EEEEEE; } +#header, #top-menu { margin: 0px 10px 0px 11px; } +#main { background: #EEEEEE; margin: 8px 10px 0px 10px; } +#content { background: #fff; border-right: 1px solid #bbb; border-bottom: 1px solid #bbb; border-left: 1px solid #d7d7d7; border-top: 1px solid #d7d7d7; } +#footer { background-color:#EEEEEE; border: 0px; } + +/* Headers */ +h2, h3, h4, .wiki h1, .wiki h2, .wiki h3 {border-bottom: 0px;} + +/* Menu */ +#main-menu li a { background-color: #507AAA; font-weight: bold;} +#main-menu li a:hover { background: #507AAA; text-decoration: underline; } + +/* Tables */ +table.list tbody td, table.list tbody tr:hover td { border: solid 1px #d7d7d7; } +table.list thead th { + border-width: 1px; + border-style: solid; + border-top-color: #d7d7d7; + border-right-color: #d7d7d7; + border-left-color: #d7d7d7; + border-bottom-color: #999999; +} + +/* Issues grid styles by priorities (provided by Wynn Netherland) */ +table.list tr.issue a { color: #666; } + +tr.odd.priority-5, table.list tbody tr.odd.priority-5:hover { color: #900; font-weight: bold; } +tr.odd.priority-5 { background: #ffc4c4; } +tr.even.priority-5, table.list tbody tr.even.priority-5:hover { color: #900; font-weight: bold; } +tr.even.priority-5 { background: #ffd4d4; } +tr.priority-5 a, tr.priority-5:hover a { color: #900; } +tr.odd.priority-5 td, tr.even.priority-5 td { border-color: #ffb4b4; } + +tr.odd.priority-4, table.list tbody tr.odd.priority-4:hover { color: #900; } +tr.odd.priority-4 { background: #ffc4c4; } +tr.even.priority-4, table.list tbody tr.even.priority-4:hover { color: #900; } +tr.even.priority-4 { background: #ffd4d4; } +tr.priority-4 a { color: #900; } +tr.odd.priority-4 td, tr.even.priority-4 td { border-color: #ffb4b4; } + +tr.odd.priority-3, table.list tbody tr.odd.priority-3:hover { color: #900; } +tr.odd.priority-3 { background: #fee; } +tr.even.priority-3, table.list tbody tr.even.priority-3:hover { color: #900; } +tr.even.priority-3 { background: #fff2f2; } +tr.priority-3 a { color: #900; } +tr.odd.priority-3 td, tr.even.priority-3 td { border-color: #fcc; } + +tr.odd.priority-1, table.list tbody tr.odd.priority-1:hover { color: #559; } +tr.odd.priority-1 { background: #eaf7ff; } +tr.even.priority-1, table.list tbody tr.even.priority-1:hover { color: #559; } +tr.even.priority-1 { background: #f2faff; } +tr.priority-1 a { color: #559; } +tr.odd.priority-1 td, tr.even.priority-1 td { border-color: #add7f3; } + +/* Buttons */ +input[type="button"], input[type="submit"], input[type="reset"] { background-color: #f2f2f2; color: #222222; border: 1px outset #cccccc; } +input[type="button"]:hover, input[type="submit"]:hover, input[type="reset"]:hover { background-color: #ccccbb; } + +/* Fields */ +input[type="text"], textarea, select { padding: 2px; border: 1px solid #d7d7d7; } +input[type="text"] { padding: 3px; } +input[type="text"]:focus, textarea:focus, select:focus { border: 1px solid #888866; } + +/* Misc */ +.box { background-color: #fcfcfc; } From f8aa2dc9b71640a4da008e8ef0968a8cd9ce7385 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 18 Nov 2007 15:53:58 +0000 Subject: [PATCH 002/710] 'fixed version' field can now be displayed on the issue list. Category and fixed version fields added to the CSV export. git-svn-id: http://redmine.rubyforge.org/svn/trunk@914 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 +- app/helpers/issues_helper.rb | 6 +++++- app/models/query.rb | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index a1f910243..92443441c 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -49,7 +49,7 @@ class IssuesController < ApplicationController @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) @issue_pages = Paginator.new self, @issue_count, limit, params['page'] @issues = Issue.find :all, :order => sort_clause, - :include => [ :assigned_to, :status, :tracker, :project, :priority, :category ], + :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], :conditions => @query.statement, :limit => limit, :offset => @issue_pages.current.offset diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index c779c9d16..f9a88f6dd 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -116,6 +116,8 @@ module IssuesHelper l(:field_priority), l(:field_subject), l(:field_assigned_to), + l(:field_category), + l(:field_fixed_version), l(:field_author), l(:field_start_date), l(:field_due_date), @@ -136,7 +138,9 @@ module IssuesHelper issue.tracker.name, issue.priority.name, issue.subject, - (issue.assigned_to ? issue.assigned_to.name : ""), + issue.assigned_to, + issue.category, + issue.fixed_version, issue.author.name, issue.start_date ? l_date(issue.start_date) : nil, issue.due_date ? l_date(issue.due_date) : nil, diff --git a/app/models/query.rb b/app/models/query.rb index 8b15e294b..30df55b96 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -100,6 +100,7 @@ class Query < ActiveRecord::Base QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), + QueryColumn.new(:fixed_version), QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), From 9c9ae217716304f7062fc31c2f4e4f8adafee2b1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 18 Nov 2007 17:46:55 +0000 Subject: [PATCH 003/710] There's now 3 account activation strategies (available in application settings): * activation by email: the user receives an email containing a link to active his account * manual activation: an email is sent to administrators for account approval (default) * automatic activation: the user can log in as soon as he has registered git-svn-id: http://redmine.rubyforge.org/svn/trunk@915 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/account_controller.rb | 68 ++++++++++++------- app/models/mailer.rb | 10 ++- app/views/account/register.rhtml | 3 - ...account_activation_request.text.html.rhtml | 2 + ...ccount_activation_request.text.plain.rhtml | 2 + app/views/settings/edit.rhtml | 7 +- config/settings.yml | 2 +- lang/bg.yml | 6 ++ lang/cs.yml | 6 ++ lang/de.yml | 6 ++ lang/en.yml | 10 ++- lang/es.yml | 6 ++ lang/fr.yml | 10 ++- lang/he.yml | 6 ++ lang/it.yml | 6 ++ lang/ja.yml | 6 ++ lang/ko.yml | 8 ++- lang/nl.yml | 6 ++ lang/pl.yml | 6 ++ lang/pt-br.yml | 6 ++ lang/pt.yml | 6 ++ lang/ro.yml | 6 ++ lang/ru.yml | 6 ++ lang/sr.yml | 6 ++ lang/sv.yml | 6 ++ lang/zh.yml | 6 ++ test/integration/account_test.rb | 43 +++++++++++- 27 files changed, 226 insertions(+), 35 deletions(-) create mode 100644 app/views/mailer/account_activation_request.text.html.rhtml create mode 100644 app/views/mailer/account_activation_request.text.plain.rhtml diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index e8e70076b..37a810bdf 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -21,7 +21,7 @@ class AccountController < ApplicationController include CustomFieldsHelper # prevents login action to be filtered by check_if_login_required application scope filter - skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register] + skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate] # Show user's account def show @@ -106,40 +106,62 @@ class AccountController < ApplicationController # User self-registration def register redirect_to(home_url) && return unless Setting.self_registration? - if params[:token] - token = Token.find_by_action_and_value("register", params[:token]) - redirect_to(home_url) && return unless token and !token.expired? - user = token.user - redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED - user.status = User::STATUS_ACTIVE - if user.save - token.destroy - flash[:notice] = l(:notice_account_activated) - redirect_to :action => 'login' - return - end + if request.get? + @user = User.new(:language => Setting.default_language) + @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } else - if request.get? - @user = User.new(:language => Setting.default_language) - @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } - else - @user = User.new(params[:user]) - @user.admin = false - @user.login = params[:user][:login] - @user.status = User::STATUS_REGISTERED - @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + @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] + if params["custom_fields"] @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } @user.custom_values = @custom_values + end + 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 :controller => 'account', :action => 'login' + redirect_to :action => 'login' + end + when '3' + # Automatic activation + @user.status = User::STATUS_ACTIVE + if @user.save + flash[:notice] = l(:notice_account_activated) + redirect_to :action => 'login' + end + else + # Manual activation by the administrator + if @user.save + # Sends an email to the administrators + Mailer.deliver_account_activation_request(@user) + flash[:notice] = l(:notice_account_pending) + redirect_to :action => 'login' end end end end + # Token based account activation + def activate + redirect_to(home_url) && return unless Setting.self_registration? && params[:token] + token = Token.find_by_action_and_value('register', params[:token]) + redirect_to(home_url) && return unless token and !token.expired? + user = token.user + redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED + user.status = User::STATUS_ACTIVE + if user.save + token.destroy + flash[:notice] = l(:notice_account_activated) + end + redirect_to :action => 'login' + end + private def logged_user=(user) if user && user.is_a?(User) diff --git a/app/models/mailer.rb b/app/models/mailer.rb index aa372120d..fe432e9a6 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -88,6 +88,14 @@ class Mailer < ActionMailer::Base :password => password, :login_url => url_for(:controller => 'account', :action => 'login') end + + def account_activation_request(user) + # Send the email to all active administrators + recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact + subject l(:mail_subject_account_activation_request) + body :user => user, + :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') + end def lost_password(token) set_language_if_valid(token.user.language) @@ -102,7 +110,7 @@ class Mailer < ActionMailer::Base recipients token.user.mail subject l(:mail_subject_register) body :token => token, - :url => url_for(:controller => 'account', :action => 'register', :token => token.value) + :url => url_for(:controller => 'account', :action => 'activate', :token => token.value) end def test(user) diff --git a/app/views/account/register.rhtml b/app/views/account/register.rhtml index ef9a12ac0..f04bfbb0e 100644 --- a/app/views/account/register.rhtml +++ b/app/views/account/register.rhtml @@ -29,9 +29,6 @@ <% for @custom_value in @custom_values %>

<%= custom_field_tag_with_label @custom_value %>

<% end %> - -

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

diff --git a/app/views/mailer/account_activation_request.text.html.rhtml b/app/views/mailer/account_activation_request.text.html.rhtml new file mode 100644 index 000000000..145ecfc8e --- /dev/null +++ b/app/views/mailer/account_activation_request.text.html.rhtml @@ -0,0 +1,2 @@ +

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

+

<%= link_to @url, @url %>

diff --git a/app/views/mailer/account_activation_request.text.plain.rhtml b/app/views/mailer/account_activation_request.text.plain.rhtml new file mode 100644 index 000000000..f431e22d3 --- /dev/null +++ b/app/views/mailer/account_activation_request.text.plain.rhtml @@ -0,0 +1,2 @@ +<%= l(:mail_body_account_activation_request, @user.login) %> +<%= @url %> diff --git a/app/views/settings/edit.rhtml b/app/views/settings/edit.rhtml index 62aa7974c..9b4cc2d57 100644 --- a/app/views/settings/edit.rhtml +++ b/app/views/settings/edit.rhtml @@ -78,7 +78,12 @@ <%= select_tag 'settings[autologin]', options_for_select( [[l(:label_disabled), "0"]] + [1, 7, 30, 365].collect{|days| [lwr(:actionview_datehelper_time_in_words_day, days), days.to_s]}, Setting.autologin) %>

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

+<%= select_tag 'settings[self_registration]', + options_for_select( [[l(:label_disabled), "0"], + [l(:label_registration_activation_by_email), "1"], + [l(:label_registration_manual_activation), "2"], + [l(:label_registration_automatic_activation), "3"] + ], Setting.self_registration ) %>

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

diff --git a/config/settings.yml b/config/settings.yml index c59a5242e..e9b9eebfd 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -28,7 +28,7 @@ welcome_text: login_required: default: 0 self_registration: - default: 1 + default: '2' lost_password: default: 1 attachment_max_size: diff --git a/lang/bg.yml b/lang/bg.yml index 09a943973..b60173ae1 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/cs.yml b/lang/cs.yml index 493bc77a5..487ec967a 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/de.yml b/lang/de.yml index 7463e93ec..fc00753b3 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/en.yml b/lang/en.yml index c51a76c80..1df0cf7c9 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -75,6 +75,7 @@ notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +notice_account_pending: "Your account was created and is now pending administrator approval." mail_subject_lost_password: Your Redmine password mail_body_lost_password: 'To change your Redmine password, click on the following link:' @@ -82,6 +83,8 @@ mail_subject_register: Redmine account activation mail_body_register: 'To activate your Redmine account, click on the following link:' mail_body_account_information_external: You can use your "%s" account to log into Redmine. mail_body_account_information: Your Redmine account information +mail_subject_account_activation_request: Redmine account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' gui_validation_error: 1 error gui_validation_error_plural: %d errors @@ -171,8 +174,8 @@ setting_app_title: Application title setting_app_subtitle: Application subtitle setting_welcome_text: Welcome text setting_default_language: Default language -setting_login_required: Authent. required -setting_self_registration: Self-registration enabled +setting_login_required: Authentication required +setting_self_registration: Self-registration setting_attachment_max_size: Attachment max. size setting_issues_export_limit: Issues export limit setting_mail_from: Emission email address @@ -446,6 +449,9 @@ label_user_mail_option_all: "For any event on all my projects" label_user_mail_option_selected: "For any event on the selected projects only..." label_user_mail_option_none: "Only for things I watch or I'm involved in" label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" +label_registration_activation_by_email: account activation by email +label_registration_manual_activation: manual account activation +label_registration_automatic_activation: automatic account activation button_login: Login button_submit: Submit diff --git a/lang/es.yml b/lang/es.yml index 6807cacb9..5175e6c19 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -541,3 +541,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/fr.yml b/lang/fr.yml index a44604a0d..8e1af9a4f 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -75,6 +75,7 @@ notice_email_error: "Erreur lors de l'envoi de l'email (%s)" notice_feeds_access_key_reseted: Votre clé d'accès aux flux RSS a été réinitialisée. notice_failed_to_save_issues: "%d demande(s) sur les %d sélectionnées n'ont pas pu être mise(s) à jour: %s." notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour." +notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur." mail_subject_lost_password: Votre mot de passe redMine mail_body_lost_password: 'Pour changer votre mot de passe Redmine, cliquez sur le lien suivant:' @@ -82,6 +83,8 @@ mail_subject_register: Activation de votre compte redMine mail_body_register: 'Pour activer votre compte Redmine, cliquez sur le lien suivant:' mail_body_account_information_external: Vous pouvez utiliser votre compte "%s" pour vous connecter à Redmine. mail_body_account_information: Paramètres de connexion de votre compte Redmine +mail_subject_account_activation_request: "Demande d'activation d'un compte Redmine" +mail_body_account_activation_request: "Un nouvel utilisateur (%s) s'est inscrit. Son compte nécessite votre approbation:" gui_validation_error: 1 erreur gui_validation_error_plural: %d erreurs @@ -171,8 +174,8 @@ setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application setting_welcome_text: Texte d'accueil setting_default_language: Langue par défaut -setting_login_required: Authentif. obligatoire -setting_self_registration: Enregistrement autorisé +setting_login_required: Authentification obligatoire +setting_self_registration: Inscription des nouveaux utilisateurs setting_attachment_max_size: Taille max des fichiers setting_issues_export_limit: Limite export demandes setting_mail_from: Adresse d'émission @@ -446,6 +449,9 @@ label_user_mail_option_all: "Pour tous les événements de tous mes projets" label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..." label_user_mail_option_none: "Seulement pour ce que je surveille ou à quoi je participe" label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue" +label_registration_activation_by_email: activation du compte par email +label_registration_manual_activation: activation manuelle du compte +label_registration_automatic_activation: activation automatique du compte button_login: Connexion button_submit: Soumettre diff --git a/lang/he.yml b/lang/he.yml index 089cfdc7d..26e808e00 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/it.yml b/lang/it.yml index 13c848935..a80070894 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/ja.yml b/lang/ja.yml index 6fd8dfcc6..86a104d15 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -539,3 +539,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/ko.yml b/lang/ko.yml index 9998f2f64..e46a63959 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -170,7 +170,7 @@ setting_app_subtitle: Application subtitle setting_welcome_text: Welcome text setting_default_language: Default language setting_login_required: Authent. required -setting_self_registration: Self-registration enabled +setting_self_registration: Self-registration setting_attachment_max_size: Attachment max. size setting_issues_export_limit: Issues export limit setting_mail_from: Emission mail address @@ -538,3 +538,9 @@ setting_protocol: Protocol mail_body_account_information: Your Redmine account information 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: Redmine 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." diff --git a/lang/nl.yml b/lang/nl.yml index ab1913e19..1758514a8 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -539,3 +539,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/pl.yml b/lang/pl.yml index 0b433bacb..e2d90de74 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -538,3 +538,9 @@ mail_body_account_information: Twoje konto w Redmine 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: Redmine 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." diff --git a/lang/pt-br.yml b/lang/pt-br.yml index d4f51b61d..80c57d3f4 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/pt.yml b/lang/pt.yml index bfd89bbae..00d136416 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/ro.yml b/lang/ro.yml index 049efa7b1..ad8881b92 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -538,3 +538,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/ru.yml b/lang/ru.yml index 782d715d9..100fad703 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -538,3 +538,9 @@ default_activity_development: Разработка enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) +label_registration_activation_by_email: account activation by email +mail_subject_account_activation_request: Redmine 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." diff --git a/lang/sr.yml b/lang/sr.yml index 1072af7eb..769d8177f 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -539,3 +539,9 @@ button_copy: Copy 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: Redmine 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." diff --git a/lang/sv.yml b/lang/sv.yml index d22b5c478..6421173fd 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -539,3 +539,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/lang/zh.yml b/lang/zh.yml index 19e5b122b..d738afdb4 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -541,3 +541,9 @@ mail_body_account_information: Your Redmine 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: Redmine 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." diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb index 6799b9288..e9d665d19 100644 --- a/test/integration/account_test.rb +++ b/test/integration/account_test.rb @@ -56,5 +56,46 @@ class AccountTest < ActionController::IntegrationTest log_user('jsmith', 'newpass') assert_equal 0, Token.count - end + end + + def test_register_with_automatic_activation + Setting.self_registration = '3' + + get 'account/register' + assert_response :success + assert_template 'account/register' + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"}, + :password => "newpass", :password_confirmation => "newpass" + assert_redirected_to 'account/login' + log_user('newuser', 'newpass') + end + + def test_register_with_manual_activation + Setting.self_registration = '2' + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"}, + :password => "newpass", :password_confirmation => "newpass" + assert_redirected_to 'account/login' + assert !User.find_by_login('newuser').active? + end + + def test_register_with_email_activation + Setting.self_registration = '1' + Token.delete_all + + post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"}, + :password => "newpass", :password_confirmation => "newpass" + assert_redirected_to 'account/login' + assert !User.find_by_login('newuser').active? + + token = Token.find(:first) + assert_equal 'register', token.action + assert_equal 'newuser@foo.bar', token.user.mail + assert !token.expired? + + get 'account/activate', :token => token.value + assert_redirected_to 'account/login' + log_user('newuser', 'newpass') + end end From a1f3497ec46881753fc13d25c3cd2cb344ae27d8 Mon Sep 17 00:00:00 2001 From: Nicolas Chuche Date: Sun, 18 Nov 2007 18:51:48 +0000 Subject: [PATCH 004/710] * add Redmine.pm to authenticate with mod_perl * add a --test option in reposman.rb * change owner right to fit with apache write access to repositories * add a deprecated warning in reposman.pl git-svn-id: http://redmine.rubyforge.org/svn/trunk@916 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 210 ++++++++++++++++++++++++++++++++++++++++++ extra/svn/reposman.pl | 5 + extra/svn/reposman.rb | 24 ++++- 3 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 extra/svn/Redmine.pm diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm new file mode 100644 index 000000000..7505b71cb --- /dev/null +++ b/extra/svn/Redmine.pm @@ -0,0 +1,210 @@ +package Apache::Authn::Redmine; + +=head1 Apache::Authn::Redmine + +Redmine - a mod_perl module to authenticate webdav subversion users +against redmine database + +=head1 SYNOPSIS + +This module allow anonymous users to browse public project and +registred users to browse and commit their project. authentication is +done on the redmine database. + +This method is far simpler than the one with pam_* and works with all +database without an hassle but you need to have apache/mod_perl on the +svn server. + +=head1 INSTALLATION + +For this to automagically work, you need to have a recent reposman.rb +(after r860) and if you already use reposman, read the last section to +migrate. + +Sorry ruby users but you need some perl modules, at least mod_perl2, +DBI and DBD::mysql (or the DBD driver for you database as it should +work on allmost all databases). + +On debian/ubuntu you must do : + + aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl + +=head1 CONFIGURATION + + ## if the module isn't in your perl path + PerlRequire /usr/local/apache/Redmine.pm + ## else + # PerlModule Apache::Authn::Redmine + + DAV svn + SVNParentPath "/var/svn" + + AuthType Basic + AuthName redmine + Require valid-user + + PerlAccessHandler Apache::Authn::Redmine::access_handler + PerlAuthenHandler Apache::Authn::Redmine::authen_handler + + ## for mysql + PerlSetVar dsn DBI:mysql:database=databasename;host=my.db.server + ## for postgres + # PerlSetVar dsn DBI:Pg:dbname=databasename;host=my.db.server + + PerlSetVar db_user redmine + PerlSetVar db_pass password + + +To be able to browse repository inside redmine, you must add something +like that : + + + DAV svn + SVNParentPath "/var/svn" + Order deny,allow + Deny from all + # only allow reading orders + + Allow from redmine.server.ip + + + +and you will have to use this reposman.rb command line to create repository : + + reposman.rb --redmine my.redmine.server --svn-dir /var/svn --owner www-data -u http://svn.server/svn-private/ + +=head1 MIGRATION FROM OLDER RELEASES + +If you use an older reposman.rb (r860 or before), you need to change +rights on repositories to allow the apache user to read and write +S + + sudo chown -R www-data /var/svn/* + sudo chmod -R u+w /var/svn/* + +And you need to upgrade at least reposman.rb (after r860). + +=cut + +use strict; + +use DBI; +use Digest::SHA1; + +use Apache2::Module; +use Apache2::Access; +use Apache2::ServerRec qw(); +use Apache2::RequestRec qw(); +use Apache2::RequestUtil qw(); +use Apache2::Const qw(:common); +# use Apache2::Directive qw(); + +my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; + +sub access_handler { + my $r = shift; + + unless ($r->some_auth_required) { + $r->log_reason("No authentication has been configured"); + return FORBIDDEN; + } + + my $method = $r->method; + return OK unless 1 == $read_only_methods{$method}; + + my $project_id = get_project_identifier($r); + + $r->set_handlers(PerlAuthenHandler => [\&OK]) + if is_public_project($project_id, $r); + + return OK +} + +sub authen_handler { + my $r = shift; + + my ($res, $redmine_pass) = $r->get_basic_auth_pw(); + return $res unless $res == OK; + + if (is_member($r->user, $redmine_pass, $r)) { + return OK; + } else { + $r->note_auth_failure(); + return AUTH_REQUIRED; + } +} + +sub is_public_project { + my $project_id = shift; + my $r = shift; + + my $dbh = connect_database($r); + my $sth = $dbh->prepare( + "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;" + ); + + $sth->execute($project_id); + my $ret = $sth->fetchrow_array ? 1 : 0; + $dbh->disconnect(); + + $ret; +} + +# perhaps we should use repository right (other read right) to check public access. +# it could be faster BUT it doesn't work for the moment. +# sub is_public_project_by_file { +# my $project_id = shift; +# my $r = shift; + +# my $tree = Apache2::Directive::conftree(); +# my $node = $tree->lookup('Location', $r->location); +# my $hash = $node->as_hash; + +# my $svnparentpath = $hash->{SVNParentPath}; +# my $repos_path = $svnparentpath . "/" . $project_id; +# return 1 if (stat($repos_path))[2] & 00007; +# } + +sub is_member { + my $redmine_user = shift; + my $redmine_pass = shift; + my $r = shift; + + my $dbh = connect_database($r); + my $project_id = get_project_identifier($r); + + my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); + + my $sth = $dbh->prepare( + "SELECT hashed_password FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND login=? AND identifier=?;" + ); + $sth->execute($redmine_user, $project_id); + + my $ret; + while (my @row = $sth->fetchrow_array) { + if ($row[0] eq $pass_digest) { + $ret = 1; + last; + } + } + $dbh->disconnect(); + + $ret; +} + +sub get_project_identifier { + my $r = shift; + + my $location = $r->location; + my ($identifier) = $r->uri =~ m{$location/*([^/]+)}; + $identifier; +} + +sub connect_database { + my $r = shift; + + my ($dsn, $db_user, $db_pass) = map { $r->dir_config($_) } qw/dsn db_user db_pass/; + return DBI->connect($dsn, $db_user, $db_pass); +} + +1; diff --git a/extra/svn/reposman.pl b/extra/svn/reposman.pl index bee800b8b..b8ce8f8af 100755 --- a/extra/svn/reposman.pl +++ b/extra/svn/reposman.pl @@ -23,6 +23,11 @@ use vars qw/$VERSION/; $VERSION = "1.0"; +my $warning = "This program is now deprecated. Use the reposman.rb for new features"; +print STDERR "*" x length($warning), "\n", + $warning, "\n", + "*" x length($warning), "\n\n"; + my %opts = (verbose => 0); GetOptions(\%opts, 'verbose|v+', 'version|V', 'help|h', 'man|m', 'quiet|q', 'svn-dir|s=s', 'redmine-host|r=s') or pod2usage(2); diff --git a/extra/svn/reposman.rb b/extra/svn/reposman.rb index d950f45e4..729970406 100755 --- a/extra/svn/reposman.rb +++ b/extra/svn/reposman.rb @@ -37,6 +37,8 @@ # -u file:///var/svn/ # if the repository is local # if this option isn't set, reposman won't register the repository # +# -t, --test +# only show what should be done # # -h, --help: # show help and exit @@ -64,6 +66,7 @@ opts = GetoptLong.new( ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT], ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT], + ['--test', '-t', GetoptLong::NO_ARGUMENT], ['--verbose', '-v', GetoptLong::NO_ARGUMENT], ['--version', '-V', GetoptLong::NO_ARGUMENT], ['--help' , '-h', GetoptLong::NO_ARGUMENT], @@ -76,6 +79,7 @@ $redmine_host = '' $repos_base = '' $svn_owner = 'root' $svn_url = false +$test = false def log(text,level=0, exit=false) return if $quiet or level > $verbose @@ -91,6 +95,7 @@ begin when '--owner'; $svn_owner = arg.dup when '--url'; $svn_url = arg.dup when '--verbose'; $verbose += 1 + when '--test'; $test = true when '--version'; puts Version; exit when '--help'; RDoc::usage when '--quiet'; $quiet = true @@ -100,6 +105,10 @@ rescue exit 1 end +if $test + log("running in test mode") +end + $svn_url += "/" if $svn_url and not $svn_url.match(/\/$/) if ($redmine_host.empty? or $repos_base.empty?) @@ -136,7 +145,7 @@ def set_owner_and_rights(project, repos_path, &block) yield if block_given? else uid, gid = Etc.getpwnam($svn_owner).uid, Etc.getgrnam(project.identifier).gid - right = project.is_public ? 0575 : 0570 + right = project.is_public ? 0775 : 0770 yield if block_given? Find.find(repos_path) do |f| File.chmod right, f @@ -176,6 +185,11 @@ projects.each do |project| owner = owner_name(repos_path) next if project.is_public == other_read and owner == $svn_owner + if $test + log("\tchange mode on #{repos_path}") + next + end + begin set_owner_and_rights(project, repos_path) rescue Errno::EPERM => e @@ -186,7 +200,13 @@ projects.each do |project| log("\tmode change on #{repos_path}"); else - project.is_public ? File.umask(0202) : File.umask(0207) + project.is_public ? File.umask(0002) : File.umask(0007) + + if $test + log("\tcreate repository #{repos_path}") + log("\trepository #{repos_path} registered in Redmine with url #{$svn_url}#{project.identifier}") if $svn_url; + next + end begin set_owner_and_rights(project, repos_path) do From deb182337d14872c5481059382459f5c21502162 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 19 Nov 2007 22:28:43 +0000 Subject: [PATCH 005/710] * Added time zone support: users can select their time zone on their account view. * Updated Polish translation (Mariusz Olejnik). * Fixed: Projects should be listed with case mixed. git-svn-id: http://redmine.rubyforge.org/svn/trunk@917 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 8 +- app/models/project.rb | 2 +- app/models/user.rb | 8 ++ app/views/my/account.rhtml | 1 + .../079_add_user_preferences_time_zone.rb | 9 ++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 119 +++++++++--------- lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh.yml | 1 + 24 files changed, 104 insertions(+), 61 deletions(-) create mode 100644 db/migrate/079_add_user_preferences_time_zone.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 215945423..80694e744 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -79,9 +79,15 @@ module ApplicationHelper def format_time(time, include_date = true) return nil unless time time = time.to_time if time.is_a?(String) + zone = User.current.time_zone + if time.utc? + local = zone ? zone.adjust(time) : time.getlocal + else + local = zone ? zone.adjust(time.getutc) : time + end @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) - include_date ? time.strftime("#{@date_format} #{@time_format}") : time.strftime(@time_format) + include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format) end def authoring(created, author) diff --git a/app/models/project.rb b/app/models/project.rb index 1fbab2e4d..afaa049c6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -150,7 +150,7 @@ class Project < ActiveRecord::Base end def <=>(project) - name <=> project.name + name.downcase <=> project.name.downcase end def allows_to?(action) diff --git a/app/models/user.rb b/app/models/user.rb index 9c8d1d9a3..2543bed19 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -130,6 +130,10 @@ class User < ActiveRecord::Base self.preference ||= UserPreference.new(:user => self) end + def time_zone + self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone] + end + # Return user's RSS key (a 40 chars long string), used to access feeds def rss_key token = self.rss_token || Token.create(:user => self, :action => 'feeds') @@ -231,6 +235,10 @@ class AnonymousUser < User false end + def time_zone + nil + end + # Anonymous user has no RSS key def rss_key nil diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index 11bba9c8d..e64051771 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -14,6 +14,7 @@

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

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

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

<%= pref_fields.check_box :hide_mail %>

<% end %>
diff --git a/db/migrate/079_add_user_preferences_time_zone.rb b/db/migrate/079_add_user_preferences_time_zone.rb new file mode 100644 index 000000000..9e36790a9 --- /dev/null +++ b/db/migrate/079_add_user_preferences_time_zone.rb @@ -0,0 +1,9 @@ +class AddUserPreferencesTimeZone < ActiveRecord::Migration + def self.up + add_column :user_preferences, :time_zone, :string + end + + def self.down + remove_column :user_preferences, :time_zone + end +end diff --git a/lang/bg.yml b/lang/bg.yml index b60173ae1..50b54cb7c 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/cs.yml b/lang/cs.yml index 487ec967a..d64e8d888 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/de.yml b/lang/de.yml index fc00753b3..b25e8d930 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/en.yml b/lang/en.yml index 1df0cf7c9..c1745b4e9 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -169,6 +169,7 @@ field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time field_column_names: Columns +field_time_zone: Time zone setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/lang/es.yml b/lang/es.yml index 5175e6c19..824c84681 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -547,3 +547,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/fr.yml b/lang/fr.yml index 8e1af9a4f..322ddd261 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -169,6 +169,7 @@ field_assignable: Demandes assignables à ce rôle field_redirect_existing_links: Rediriger les liens existants field_estimated_hours: Temps estimé field_column_names: Colonnes +field_time_zone: Fuseau horaire setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/lang/he.yml b/lang/he.yml index 26e808e00..f630fe051 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/it.yml b/lang/it.yml index a80070894..f103e18cc 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/ja.yml b/lang/ja.yml index 86a104d15..3a801fcf8 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -545,3 +545,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/ko.yml b/lang/ko.yml index e46a63959..3886d7781 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/nl.yml b/lang/nl.yml index 1758514a8..1397f4c6e 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -545,3 +545,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/pl.yml b/lang/pl.yml index e2d90de74..7796d6d2a 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -102,12 +102,12 @@ field_value: Wartość field_category: Kategoria field_title: Tytuł field_project: Projekt -field_issue: Zgłoszenie +field_issue: Zagadnienie field_status: Status field_notes: Notatki -field_is_closed: Zgłoszenie zamknięte +field_is_closed: Zagadnienie zamknięte field_is_default: Domyślny status -field_tracker: Typ zgłoszenia +field_tracker: Typ zagadnienia field_subject: Temat field_due_date: Data oddania field_assigned_to: Przydzielony do @@ -117,9 +117,9 @@ field_user: Użytkownik field_role: Rola field_homepage: Strona www field_is_public: Publiczny -field_parent: Subprojekt -field_is_in_chlog: Zgłoszenia pokazane w zapisie zmian -field_is_in_roadmap: Zgłoszenia pokazane na mapie +field_parent: Podprojekt +field_is_in_chlog: Zagadnienie pokazywane w zapisie zmian +field_is_in_roadmap: Zagadnienie pokazywane na mapie field_login: Login field_mail_notification: Powiadomienia Email field_admin: Administrator @@ -152,8 +152,8 @@ field_hours: Godzin field_activity: Aktywność field_spent_on: Data field_identifier: Identifikator -field_is_filter: Używane jako filter -field_issue_to_id: Powiązane zgłoszenie +field_is_filter: Atrybut filtrowania +field_issue_to_id: Powiązania zagadnienia field_delay: Opóźnienie setting_app_title: Tytuł aplikacji @@ -163,7 +163,7 @@ setting_default_language: Domyślny język setting_login_required: Identyfikacja wymagana setting_self_registration: Własna rejestracja umożliwiona setting_attachment_max_size: Maks. rozm. załącznika -setting_issues_export_limit: Limit eksportu zgłoszeń +setting_issues_export_limit: Limit eksportu zagadnień setting_mail_from: Adres email wysyłki setting_host_name: Nazwa hosta setting_text_formatting: Formatowanie tekstu @@ -184,10 +184,10 @@ label_project_new: Nowy projekt label_project_plural: Projekty label_project_all: Wszystkie projekty label_project_latest: Ostatnie projekty -label_issue: Zgłoszenie -label_issue_new: Nowe zgłoszenie -label_issue_plural: Zgłoszenia -label_issue_view_all: Zobacz wszystkie zgłoszenia +label_issue: Zagadnienie +label_issue_new: Nowe zagadnienie +label_issue_plural: Zagadnienia +label_issue_view_all: Zobacz wszystkie zagadnienia label_document: Dokument label_document_new: Nowy dokument label_document_plural: Dokumenty @@ -198,15 +198,15 @@ label_role_and_permissions: Role i Uprawnienia label_member: Uczestnik label_member_new: Nowy uczestnik label_member_plural: Uczestnicy -label_tracker: Typ zgłoszenia -label_tracker_plural: Typy zgłoszeń -label_tracker_new: Nowy typ zgłoszenia +label_tracker: Typ zagadnienia +label_tracker_plural: Typy zagadnień +label_tracker_new: Nowy typ zagadnienia label_workflow: Przepływ -label_issue_status: Status zgłoszenia -label_issue_status_plural: Statusy zgłoszeń +label_issue_status: Status zagadnienia +label_issue_status_plural: Statusy zagadnień label_issue_status_new: Nowy status -label_issue_category: Kategoria zgłoszenia -label_issue_category_plural: Kategorie zgłoszeń +label_issue_category: Kategoria zagadnienia +label_issue_category_plural: Kategorie zagadnień label_issue_category_new: Nowa kategoria label_custom_field: Dowolne pole label_custom_field_plural: Dowolne pola @@ -226,8 +226,8 @@ label_administration: Administracja label_login: Login label_logout: Wylogowanie label_help: Pomoc -label_reported_issues: Zaraportowane zgłoszenia -label_assigned_to_me_issues: Zgłoszenia przypisane do mnie +label_reported_issues: Wprowadzone zagadnienia +label_assigned_to_me_issues: Zagadnienia przypisane do mnie label_last_login: Ostatnie połączenie label_last_updates: Ostatnia zmieniana label_last_updates_plural: %d ostatnie zmiany @@ -244,8 +244,8 @@ label_subproject_plural: Podprojekty label_min_max_length: Min - Maks długość label_list: Lista label_date: Data -label_integer: L. pojedyńcza -label_boolean: Wart. logiczna +label_integer: Liczba całkowita +label_boolean: Wartość logiczna label_string: Tekst label_text: Długi tekst label_attribute: Atrybut @@ -333,7 +333,7 @@ label_deleted: usunięte label_latest_revision: Ostatnia zmiana label_latest_revision_plural: Ostatnie zmiany label_view_revisions: Pokaż zmiany -label_max_size: Kamsymalny rozmiar +label_max_size: Maksymalny rozmiar label_on: 'włączone' label_sort_highest: Przesuń na górę label_sort_higher: Do góry @@ -341,7 +341,7 @@ label_sort_lower: Do dołu label_sort_lowest: Przesuń na dół label_roadmap: Mapa label_roadmap_due_in: W czasie -label_roadmap_no_issues: Brak zgłoszeń do tej wersji +label_roadmap_no_issues: Brak zagadnień do tej wersji label_search: Szukaj label_result_plural: Rezultatów label_all_words: Wszystkie słowa @@ -356,7 +356,7 @@ label_current_version: Obecna wersja label_preview: Podgląd label_feed_plural: Ilość RSS label_changes_details: Szczegóły wszystkich zmian -label_issue_tracking: Śledzenie zgłoszeń +label_issue_tracking: Śledzenie zagadnień label_spent_time: Spędzony czas label_f_hour: %.2f godzina label_f_hour_plural: %.2f godzin @@ -371,8 +371,8 @@ label_diff_side_by_side: obok siebie label_options: Opcje label_copy_workflow_from: Kopiuj przepływ z label_permissions_report: Raport uprawnień -label_watched_issues: Obserwowane zgłoszenia -label_related_issues: Powiązane zgłoszenia +label_watched_issues: Obserwowane zagadnienia +label_related_issues: Powiązane zagadnienia label_applied_status: Stosowany status label_loading: Ładowanie... label_relation_new: Nowe powiązanie @@ -446,7 +446,7 @@ text_select_mail_notifications: Zaznacz czynności przy których użytkownik pow text_regexp_info: np. ^[A-Z0-9]+$ text_min_max_length_info: 0 oznacza brak restrykcji text_project_destroy_confirmation: Jesteś pewien, że chcesz usunąć ten projekt i wszyskie powiązane dane? -text_workflow_edit: Zaznacz rolę i typ zgłoszenia do edycji przepływu +text_workflow_edit: Zaznacz rolę i typ zagadnienia do edycji przepływu text_are_you_sure: Jesteś pewien ? text_journal_changed: zmienione %s do %s text_journal_set_to: ustawione na %s @@ -457,14 +457,14 @@ text_tip_task_begin_end_day: zadanie zaczynające i kończące się dzisiaj text_project_identifier_info: 'Małe litery (a-z), liczby i myślniki dozwolone.
Raz zapisany, identyfikator nie może być zmieniony.' text_caracters_maximum: %d znaków maksymalnie. text_length_between: Długość pomiędzy %d i %d znaków. -text_tracker_no_workflow: Brak przepływu zefiniowanego dla tego typu zgłoszenia +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: Zgłoszenia odnoszące i ustalające we wrzutkach CVS +text_issues_ref_in_commit_messages: Zagadnienia odnoszące i ustalające we wrzutkach CVS default_role_manager: Kierownik default_role_developper: Programista -default_role_reporter: Raportujący +default_role_reporter: Wprowadzajacy default_tracker_bug: Błąd default_tracker_feature: Cecha default_tracker_support: Wsparcie @@ -484,25 +484,25 @@ default_priority_immediate: Natyczmiastowy default_activity_design: Projektowanie default_activity_development: Rozwój -enumeration_issue_priorities: Priorytety zgłoszeń +enumeration_issue_priorities: Priorytety zagadnień enumeration_doc_categories: Kategorie dokumentów enumeration_activities: Działania (śledzenie czasu) button_rename: Zmień nazwę -text_issue_category_destroy_question: Zgłoszenia (%d) są przypisane do tej kategorii. Co chcesz uczynić? +text_issue_category_destroy_question: Zagadnienia (%d) są przypisane do tej kategorii. Co chcesz uczynić? label_feeds_access_key_created_on: Klucz dostępu RSS stworzony %s dni temu -setting_cross_project_issue_relations: Zezwól na powiązania zgłoszeń między projektami +setting_cross_project_issue_relations: Zezwól na powiązania zagadnień między projektami label_roadmap_overdue: %s spóźnienia label_module_plural: Moduły label_this_week: ten tydzień label_jump_to_a_project: Skocz do projektu... -field_assignable: Zgłoszenia mogą być przypisane do tej roli +field_assignable: Zagadnienia mogą być przypisane do tej roli label_sort_by: Sortuj po %s -text_issue_updated: Zgłoszenie %s zostało zaktualizowane. +text_issue_updated: Zagadnienie %s zostało zaktualizowane. notice_feeds_access_key_reseted: Twój klucz dostępu RSS został zrestetowany. field_redirect_existing_links: Przekierowanie istniejących odnośników -text_issue_category_reassign_to: Przywróć zgłoszenia do tej kategorii +text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii notice_email_sent: Email został wysłany do %s -text_issue_added: Zgłoszenie %s zostało zaraportowane. +text_issue_added: Zagadnienie %s zostało wprowadzone. text_wiki_destroy_confirmation: Jesteś pewien, że chcesz usunąć to wiki i całą jego zawartość ? notice_email_error: Wystąpił błąd w trakcie wysyłania maila (%s) label_updated_time: Zaktualizowane %s temu @@ -518,29 +518,30 @@ label_default_columns: Domyślne kolumny setting_issue_list_default_columns: Domyślne kolumny wiświetlane na liście zagadnień setting_repositories_encodings: Kodowanie repozytoriów notice_no_issue_selected: "Nie wybrano zagadnienia! Zaznacz zagadnienie, które chcesz edytować." -label_bulk_edit_selected_issues: Bulk edit selected issues +label_bulk_edit_selected_issues: Zbiorowa edycja zagadnień label_no_change_option: (Bez zmian) notice_failed_to_save_issues: "Błąd podczas zapisu zagadnień %d z %d zaznaczonych: %s." label_theme: Temat label_default: Domyślne label_search_titles_only: Przeszukuj tylko tytuły -label_nobody: 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 +label_nobody: nikt +button_change_password: Zmień hasło +text_user_mail_option: "W przypadku niezaznaczonych projektów, będziesz otrzymywał powiadomienia tylko na temat zagadnien, które obserwujesz, lub w których bierzesz udział (np. jesteś autorem lub adresatem)." +label_user_mail_option_selected: "Tylko dla każdego zdarzenia w wybranych projektach..." +label_user_mail_option_all: "Dla każdego zdarzenia w każdym moim projekcie" +label_user_mail_option_none: "Tylko to co obserwuje lub w czym biorę udział" +setting_emails_footer: Stopka e-mail +label_float: Liczba rzeczywista +button_copy: Kopia mail_body_account_information_external: Możesz użyć twojego "%s" konta do zalogowania do Redmine. mail_body_account_information: Twoje konto w Redmine -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: Redmine 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." +setting_protocol: Protokoł +label_user_mail_no_self_notified: "Nie chcę powiadomień o zmianach, które sam wprowadzam." +setting_time_format: Format czasu +label_registration_activation_by_email: aktywacja konta przez e-mail +mail_subject_account_activation_request: Zapytanie aktywacyjne konta Redmine +mail_body_account_activation_request: 'Zarejestrowano nowego użytkownika: (%s). Konto oczekuje na twoje zatwierdzenie:' +label_registration_automatic_activation: automatyczna aktywacja kont +label_registration_manual_activation: manualna aktywacja kont +notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdzenie administratora." +field_time_zone: Time zone diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 80c57d3f4..7444aad99 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/pt.yml b/lang/pt.yml index 00d136416..d75247aa9 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/ro.yml b/lang/ro.yml index ad8881b92..4cb2e4f8e 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/ru.yml b/lang/ru.yml index 100fad703..010b687e6 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -544,3 +544,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/sr.yml b/lang/sr.yml index 769d8177f..27be7da86 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -545,3 +545,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/sv.yml b/lang/sv.yml index 6421173fd..f5bd5a139 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -545,3 +545,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 diff --git a/lang/zh.yml b/lang/zh.yml index d738afdb4..257286778 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -547,3 +547,4 @@ mail_body_account_activation_request: 'A new user (%s) has registered. His accou 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 From 99f9aea80a2bc43cdfc2933728f0ab72d7bf99d5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 20 Nov 2007 12:07:28 +0000 Subject: [PATCH 006/710] * Referencing issues in commit messages: enter * in 'Referencing keywords' to link any issue id without using keywords. * Updated Polish translation (Mariusz Olejnik). git-svn-id: http://redmine.rubyforge.org/svn/trunk@918 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 2 +- app/models/changeset.rb | 9 +++++++ lang/pl.yml | 14 +++++------ test/unit/changeset_test.rb | 42 +++++++++++++++++++++++++++++++ test/unit/repository_test.rb | 2 +- 5 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 test/unit/changeset_test.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 80694e744..9c8e9c67d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -231,7 +231,7 @@ module ApplicationHelper # example: # #52 -> #52 # r52 -> r52 (project.id is 6) - text = text.gsub(%r{([\s,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m| + text = text.gsub(%r{([\s\(,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m| leading, otype, oid = $1, $2, $3 link = nil if otype == 'r' diff --git a/app/models/changeset.rb b/app/models/changeset.rb index e4e221732..355a5754c 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -63,6 +63,14 @@ class Changeset < ActiveRecord::Base return if kw_regexp.blank? referenced_issues = [] + + if ref_keywords.delete('*') + # find any issue ID in the comments + target_issue_ids = [] + comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] } + referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids) + end + comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match| action = match[0] target_issue_ids = match[1].scan(/\d+/) @@ -80,6 +88,7 @@ class Changeset < ActiveRecord::Base end referenced_issues += target_issues end + self.issues = referenced_issues.uniq end end diff --git a/lang/pl.yml b/lang/pl.yml index 7796d6d2a..760250e69 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -261,11 +261,11 @@ label_attachment_delete: Skasuj plik label_attachment_plural: Pliki label_report: Raport label_report_plural: Raporty -label_news: Nowość -label_news_new: Dodaj nowość -label_news_plural: Nowości -label_news_latest: Ostatnie nowości -label_news_view_all: Pokaż wszystkie nowości +label_news: Wiadomość +label_news_new: Dodaj wiadomość +label_news_plural: Wiadomości +label_news_latest: Ostatnie wiadomości +label_news_view_all: Pokaż wszystkie wiadomości label_change_log: Lista zmian label_settings: Ustawienia label_overview: Przegląd @@ -334,7 +334,7 @@ label_latest_revision: Ostatnia zmiana label_latest_revision_plural: Ostatnie zmiany label_view_revisions: Pokaż zmiany label_max_size: Maksymalny rozmiar -label_on: 'włączone' +label_on: 'z' label_sort_highest: Przesuń na górę label_sort_higher: Do góry label_sort_lower: Do dołu @@ -544,4 +544,4 @@ mail_body_account_activation_request: 'Zarejestrowano nowego użytkownika: (%s). label_registration_automatic_activation: automatyczna aktywacja kont label_registration_manual_activation: manualna aktywacja kont notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdzenie administratora." -field_time_zone: Time zone +field_time_zone: Strefa czasowa diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb new file mode 100644 index 000000000..ee53f18ff --- /dev/null +++ b/test/unit/changeset_test.rb @@ -0,0 +1,42 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class ChangesetTest < Test::Unit::TestCase + fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :trackers + + def setup + end + + def test_ref_keywords_any + Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id + Setting.commit_fix_done_ratio = '90' + Setting.commit_ref_keywords = '*' + Setting.commit_fix_keywords = 'fixes , closes' + + c = Changeset.new(:repository => Project.find(1).repository, + :committed_on => Time.now, + :comments => 'New commit (#2). Fixes #1') + c.scan_comment_for_issue_ids + + assert_equal [1, 2], c.issue_ids.sort + fixed = Issue.find(1) + assert fixed.closed? + assert_equal 90, fixed.done_ratio + end +end diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb index b802403f5..843b0b42c 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -68,7 +68,7 @@ class RepositoryTest < Test::Unit::TestCase COMMENT changeset = Changeset.new( :comments => comment, :commit_date => Time.now, :revision => 0, :scmid => 'f39b7922fb3c', - :committer => 'foo ', :committed_on => Time.now, :repository_id => repository ) + :committer => 'foo ', :committed_on => Time.now, :repository => repository ) assert( changeset.save ) assert_not_equal( comment, changeset.comments ) assert_equal( 'This is a loooooooooooooooooooooooooooong comment', changeset.comments ) From 987a5aa22114ec2e931464782351431e4dfec97c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 20 Nov 2007 15:40:16 +0000 Subject: [PATCH 007/710] Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums. These permissions need to be explicitly given to the Anonymous role (Admin -> Roles & Permissions -> Anonymous). git-svn-id: http://redmine.rubyforge.org/svn/trunk@919 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/application.rb | 4 --- app/controllers/documents_controller.rb | 2 +- app/controllers/issues_controller.rb | 19 ++++++------ app/controllers/messages_controller.rb | 6 ++-- app/controllers/my_controller.rb | 10 +++---- app/controllers/news_controller.rb | 2 +- app/controllers/projects_controller.rb | 11 ++++--- app/controllers/queries_controller.rb | 8 ++--- app/controllers/search_controller.rb | 4 +-- app/controllers/timelog_controller.rb | 6 ++-- app/controllers/welcome_controller.rb | 4 +-- app/controllers/wiki_controller.rb | 4 +-- app/models/attachment.rb | 7 +---- app/models/query.rb | 7 ++--- app/models/user.rb | 40 +++++++++++++++---------- app/views/news/show.rhtml | 2 +- db/migrate/080_add_users_type.rb | 10 +++++++ lib/redmine.rb | 12 ++++---- test/unit/user_test.rb | 9 +++++- 19 files changed, 88 insertions(+), 79 deletions(-) create mode 100644 db/migrate/080_add_users_type.rb diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 991b3fff7..e186455a3 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -23,10 +23,6 @@ class ApplicationController < ActionController::Base require_dependency "repository/#{scm.underscore}" end - def logged_in_user - User.current.logged? ? User.current : nil - end - def current_role @current_role ||= User.current.role_for_project(@project) end diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 63ee96134..94532b65b 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -49,7 +49,7 @@ class DocumentsController < ApplicationController @attachments = [] params[:attachments].each { |file| next unless file.size > 0 - a = Attachment.create(:container => @document, :file => file, :author => logged_in_user) + a = Attachment.create(:container => @document, :file => file, :author => User.current) @attachments << a unless a.new_record? } if params[:attachments] and params[:attachments].is_a? Array Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('document_added') diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 92443441c..cca3fe623 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -82,7 +82,7 @@ class IssuesController < ApplicationController def show @custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") - @status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user + @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker) respond_to do |format| format.html { render :template => 'issues/show.rhtml' } format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } @@ -95,7 +95,7 @@ class IssuesController < ApplicationController @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } else begin - @issue.init_journal(self.logged_in_user) + @issue.init_journal(User.current) # Retrieve custom fields and values if 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]) } @@ -117,7 +117,7 @@ class IssuesController < ApplicationController journal = @issue.init_journal(User.current, params[:notes]) params[:attachments].each { |file| next unless file.size > 0 - a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user) + a = Attachment.create(:container => @issue, :file => file, :author => User.current) journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename) unless a.new_record? @@ -132,17 +132,17 @@ class IssuesController < ApplicationController end def change_status - @status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user + @status_options = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker) @new_status = IssueStatus.find(params[:new_status_id]) if params[:confirm] begin - journal = @issue.init_journal(self.logged_in_user, params[:notes]) + journal = @issue.init_journal(User.current, params[:notes]) @issue.status = @new_status if @issue.update_attributes(params[:issue]) # Save attachments params[:attachments].each { |file| next unless file.size > 0 - a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user) + a = Attachment.create(:container => @issue, :file => file, :author => User.current) journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename) unless a.new_record? @@ -150,7 +150,7 @@ class IssuesController < ApplicationController # Log time if current_role.allowed_to?(:log_time) - @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today) + @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 @@ -176,7 +176,7 @@ class IssuesController < ApplicationController def destroy_attachment a = @issue.attachments.find(params[:attachment_id]) a.destroy - journal = @issue.init_journal(self.logged_in_user) + journal = @issue.init_journal(User.current) journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :old_value => a.filename) @@ -225,12 +225,11 @@ private def retrieve_query if params[:query_id] @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)}) - @query.executed_by = logged_in_user session[:query] = @query else if params[:set_filter] or !session[:query] or session[:query].project != @project # Give it a name, required to be valid - @query = Query.new(:name => "_", :executed_by => logged_in_user) + @query = Query.new(:name => "_") @query.project = @project if params[:fields] and params[:fields].is_a? Array params[:fields].each do |field| diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 74a957d6c..645aadf1c 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -31,12 +31,12 @@ class MessagesController < ApplicationController def new @message = Message.new(params[:message]) - @message.author = logged_in_user + @message.author = User.current @message.board = @board if request.post? && @message.save params[:attachments].each { |file| next unless file.size > 0 - Attachment.create(:container => @message, :file => file, :author => logged_in_user) + Attachment.create(:container => @message, :file => file, :author => User.current) } if params[:attachments] and params[:attachments].is_a? Array redirect_to :action => 'show', :id => @message end @@ -44,7 +44,7 @@ class MessagesController < ApplicationController def reply @reply = Message.new(params[:reply]) - @reply.author = logged_in_user + @reply.author = User.current @reply.board = @board @message.children << @reply redirect_to :action => 'show', :id => @message diff --git a/app/controllers/my_controller.rb b/app/controllers/my_controller.rb index 2fa5a9d9c..cb326bc93 100644 --- a/app/controllers/my_controller.rb +++ b/app/controllers/my_controller.rb @@ -44,7 +44,7 @@ class MyController < ApplicationController # Show user's page def page - @user = self.logged_in_user + @user = User.current @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT end @@ -76,7 +76,7 @@ class MyController < ApplicationController # Manage user's password def password - @user = self.logged_in_user + @user = User.current flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id if request.post? if @user.check_password?(params[:password]) @@ -102,7 +102,7 @@ class MyController < ApplicationController # User's page layout configuration def page_layout - @user = self.logged_in_user + @user = User.current @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup session[:page_layout] = @blocks %w(top left right).each {|f| session[:page_layout][f] ||= [] } @@ -116,7 +116,7 @@ class MyController < ApplicationController def add_block block = params[:block] render(:nothing => true) and return unless block && (BLOCKS.keys.include? block) - @user = self.logged_in_user + @user = User.current # remove if already present in a group %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block } # add it on top @@ -151,7 +151,7 @@ class MyController < ApplicationController # Save user's page layout def page_layout_save - @user = self.logged_in_user + @user = User.current @user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout] @user.pref.save session[:page_layout] = nil diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index c41c5844e..109afe454 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -45,7 +45,7 @@ class NewsController < ApplicationController def add_comment @comment = Comment.new(params[:comment]) - @comment.author = logged_in_user + @comment.author = User.current if @news.comments << @comment flash[:notice] = l(:label_comment_added) redirect_to :action => 'show', :id => @news diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 289b34e24..0f50cd780 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -48,7 +48,7 @@ class ProjectsController < ApplicationController # Lists visible projects def list projects = Project.find :all, - :conditions => Project.visible_by(logged_in_user), + :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]} @@ -176,7 +176,7 @@ class ProjectsController < ApplicationController if request.post? and @document.save # Save the attachments params[:attachments].each { |a| - Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0 + Attachment.create(:container => @document, :file => a, :author => User.current) unless a.size == 0 } if params[:attachments] and params[:attachments].is_a? Array flash[:notice] = l(:notice_successful_create) Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') @@ -216,7 +216,7 @@ class ProjectsController < ApplicationController return end @issue.status = default_status - @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker))if logged_in_user + @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)) if request.get? @issue.start_date ||= Date.today @@ -321,10 +321,9 @@ class ProjectsController < ApplicationController # Add a news to @project def add_news - @news = News.new(:project => @project) + @news = News.new(:project => @project, :author => User.current) if request.post? @news.attributes = params[:news] - @news.author_id = self.logged_in_user.id if self.logged_in_user if @news.save flash[:notice] = l(:notice_successful_create) Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added') @@ -340,7 +339,7 @@ class ProjectsController < ApplicationController @attachments = [] params[:attachments].each { |file| next unless file.size > 0 - a = Attachment.create(:container => @version, :file => file, :author => logged_in_user) + a = Attachment.create(:container => @version, :file => file, :author => User.current) @attachments << a unless a.new_record? } if params[:attachments] and params[:attachments].is_a? Array Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added') diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb index 7feafd35b..69bad345a 100644 --- a/app/controllers/queries_controller.rb +++ b/app/controllers/queries_controller.rb @@ -22,14 +22,13 @@ class QueriesController < ApplicationController def index @queries = @project.queries.find(:all, :order => "name ASC", - :conditions => ["is_public = ? or user_id = ?", true, (logged_in_user ? logged_in_user.id : 0)]) + :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)]) end def new @query = Query.new(params[:query]) @query.project = @project - @query.user = logged_in_user - @query.executed_by = logged_in_user + @query.user = User.current @query.is_public = false unless current_role.allowed_to?(:manage_public_queries) @query.column_names = nil if params[:default_columns] @@ -71,9 +70,8 @@ private def find_project if params[:id] @query = Query.find(params[:id]) - @query.executed_by = logged_in_user @project = @query.project - render_403 unless @query.editable_by?(logged_in_user) + render_403 unless @query.editable_by?(User.current) else @project = Project.find(params[:project_id]) end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 2c00b3d74..7c50d4dcb 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -31,7 +31,7 @@ class SearchController < ApplicationController begin; offset = params[:offset].to_time if params[:offset]; rescue; end # quick jump to an issue - if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(logged_in_user)) + if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current)) redirect_to :controller => "issues", :action => "show", :id => $1 return end @@ -87,7 +87,7 @@ class SearchController < ApplicationController end else operator = @all_words ? ' AND ' : ' OR ' - Project.with_scope(:find => {:conditions => Project.visible_by(logged_in_user)}) do + Project.with_scope(:find => {:conditions => Project.visible_by(User.current)}) do @results += Project.find(:all, :limit => limit, :conditions => [ (["(LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'projects' end # if only one project is found, user is redirected to its overview diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 68c0edefa..1a1bace3a 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -107,15 +107,15 @@ class TimelogController < ApplicationController @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause) @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours } - @owner_id = logged_in_user ? logged_in_user.id : 0 + @owner_id = User.current.id send_csv and return if 'csv' == params[:export] render :action => 'details', :layout => false if request.xhr? end def edit - render_404 and return if @time_entry && @time_entry.user != logged_in_user - @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today) + render_404 and return if @time_entry && @time_entry.user != User.current + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) @time_entry.attributes = params[:time_entry] if request.post? and @time_entry.save flash[:notice] = l(:notice_successful_update) diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 2eac2268f..b4be7fb1c 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -19,7 +19,7 @@ class WelcomeController < ApplicationController layout 'base' def index - @news = News.latest logged_in_user - @projects = Project.latest logged_in_user + @news = News.latest User.current + @projects = Project.latest User.current end end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 7609323f4..37a36bf56 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -69,7 +69,7 @@ class WikiController < ApplicationController #@content.text = params[:content][:text] #@content.comments = params[:content][:comments] @content.attributes = params[:content] - @content.author = logged_in_user + @content.author = User.current # if page is new @page.save will also save content, but not if page isn't a new record if (@page.new_record? ? @page.save : @content.save) redirect_to :action => 'index', :id => @project, :page => @page.title @@ -157,7 +157,7 @@ class WikiController < ApplicationController # Save the attachments params[:attachments].each { |file| next unless file.size > 0 - a = Attachment.create(:container => @page, :file => file, :author => logged_in_user) + a = Attachment.create(:container => @page, :file => file, :author => User.current) } if params[:attachments] and params[:attachments].is_a? Array redirect_to :action => 'index', :page => @page.title end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index d2bcab33f..7262498c4 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -21,7 +21,7 @@ class Attachment < ActiveRecord::Base belongs_to :container, :polymorphic => true belongs_to :author, :class_name => "User", :foreign_key => "author_id" - validates_presence_of :container, :filename + validates_presence_of :container, :filename, :author validates_length_of :filename, :maximum => 255 validates_length_of :disk_filename, :maximum => 255 @@ -82,11 +82,6 @@ class Attachment < ActiveRecord::Base def increment_download increment!(:downloads) end - - # returns last created projects - def self.most_downloaded - find(:all, :limit => 5, :order => "downloads DESC") - end def project container.is_a?(Project) ? container : container.project diff --git a/app/models/query.rb b/app/models/query.rb index 30df55b96..f869f648b 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -112,11 +112,8 @@ class Query < ActiveRecord::Base def initialize(attributes = nil) super attributes self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } - end - - def executed_by=(user) - @executed_by = user - set_language_if_valid(user.language) if user + @executed_by = User.current.logged? ? User.current : nil + set_language_if_valid(executed_by.language) if executed_by end def validate diff --git a/app/models/user.rb b/app/models/user.rb index 2543bed19..93b73dd7a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ require "digest/sha1" class User < ActiveRecord::Base # Account statuses + STATUS_ANONYMOUS = 0 STATUS_ACTIVE = 1 STATUS_REGISTERED = 2 STATUS_LOCKED = 3 @@ -36,15 +37,15 @@ class User < ActiveRecord::Base # Prevents unauthorized assignments attr_protected :login, :admin, :password, :password_confirmation, :hashed_password - validates_presence_of :login, :firstname, :lastname, :mail + validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } validates_uniqueness_of :login, :mail # Login must contain lettres, numbers, underscores only - validates_format_of :login, :with => /^[a-z0-9_\-@\.]+$/i + validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i validates_length_of :login, :maximum => 30 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i validates_length_of :firstname, :lastname, :maximum => 30 - validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i - validates_length_of :mail, :maximum => 60 + 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 # Password length between 4 and 12 validates_length_of :password, :in => 4..12, :allow_nil => true validates_confirmation_of :password, :allow_nil => true @@ -216,11 +217,17 @@ class User < ActiveRecord::Base end def self.current - @current_user ||= AnonymousUser.new + @current_user ||= User.anonymous end def self.anonymous - AnonymousUser.new + return @anonymous_user if @anonymous_user + anonymous_user = AnonymousUser.find(:first) + if anonymous_user.nil? + anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0) + raise 'Unable to create the anonymous user.' if anonymous_user.new_record? + end + @anonymous_user = anonymous_user end private @@ -231,16 +238,17 @@ private end class AnonymousUser < User - def logged? - false + + def validate_on_create + # There should be only one AnonymousUser in the database + errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first) end - def time_zone - nil - end - - # Anonymous user has no RSS key - def rss_key - nil - end + # Overrides a few properties + def logged?; false end + def admin; false end + def name; 'Anonymous' end + def mail; nil end + def time_zone; nil end + def rss_key; nil end end diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index ef5bbcd4c..2b51c1fee 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -26,7 +26,7 @@

<%= l(:label_comment_plural) %>

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

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

+

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

<%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
diff --git a/db/migrate/080_add_users_type.rb b/db/migrate/080_add_users_type.rb new file mode 100644 index 000000000..c907b472e --- /dev/null +++ b/db/migrate/080_add_users_type.rb @@ -0,0 +1,10 @@ +class AddUsersType < ActiveRecord::Migration + def self.up + add_column :users, :type, :string + User.update_all "type = 'User'" + end + + def self.down + remove_column :users, :type + end +end diff --git a/lib/redmine.rb b/lib/redmine.rb index 959cfc0c8..8a79b265a 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -29,11 +29,11 @@ Redmine::AccessControl.map do |map| :issues => [:index, :changes, :show, :context_menu], :queries => :index, :reports => :issue_report}, :public => true - map.permission :add_issues, {:projects => :add_issue}, :require => :loggedin + map.permission :add_issues, {:projects => :add_issue} map.permission :edit_issues, {:projects => :bulk_edit_issues, - :issues => [:edit, :destroy_attachment]}, :require => :loggedin - map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}, :require => :loggedin - map.permission :add_issue_notes, {:issues => :add_note}, :require => :loggedin + :issues => [:edit, :destroy_attachment]} + map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} + map.permission :add_issue_notes, {:issues => :add_note} map.permission :change_issue_status, {:issues => :change_status}, :require => :loggedin map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin map.permission :delete_issues, {:issues => :destroy}, :require => :member @@ -53,7 +53,7 @@ Redmine::AccessControl.map do |map| map.project_module :news do |map| map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member map.permission :view_news, {:news => [:index, :show]}, :public => true - map.permission :comment_news, {:news => :add_comment}, :require => :loggedin + map.permission :comment_news, {:news => :add_comment} end map.project_module :documents do |map| @@ -83,7 +83,7 @@ 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]}, :require => :loggedin + map.permission :add_messages, {:messages => [:new, :reply]} end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 82cbbdaa7..9f58d278f 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -60,7 +60,7 @@ class UserTest < Test::Unit::TestCase def test_validate @admin.login = "" assert !@admin.save - assert_equal 2, @admin.errors.count + assert_equal 1, @admin.errors.count end def test_password @@ -87,6 +87,13 @@ class UserTest < Test::Unit::TestCase assert_equal nil, user end + def test_create_anonymous + AnonymousUser.delete_all + anon = User.anonymous + assert !anon.new_record? + assert_kind_of AnonymousUser, anon + end + def test_rss_key assert_nil @jsmith.rss_token key = @jsmith.rss_key From 8d91afc33e3b5e7ef491fb30fb5ccbda35a1d02d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 20 Nov 2007 20:29:03 +0000 Subject: [PATCH 008/710] Added per-project tracker selection. Trackers can be selected on project settings. git-svn-id: http://redmine.rubyforge.org/svn/trunk@920 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 1 + app/controllers/projects_controller.rb | 26 +++++++----- app/controllers/reports_controller.rb | 4 +- app/helpers/projects_helper.rb | 2 +- app/models/custom_value.rb | 4 +- app/models/issue.rb | 6 ++- app/models/project.rb | 1 + app/models/tracker.rb | 4 ++ app/views/issues/_sidebar.rhtml | 2 +- app/views/issues/context_menu.rhtml | 2 +- app/views/projects/_form.rhtml | 24 +++++++++-- app/views/projects/add.rhtml | 12 +++--- app/views/projects/move_issues.rhtml | 9 +++- app/views/projects/show.rhtml | 2 +- db/migrate/081_create_projects_trackers.rb | 19 +++++++++ test/fixtures/projects_trackers.yml | 46 +++++++++++++++++++++ test/functional/projects_controller_test.rb | 2 +- test/unit/issue_test.rb | 2 +- 18 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 db/migrate/081_create_projects_trackers.rb create mode 100644 test/fixtures/projects_trackers.yml diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index cca3fe623..081d8f895 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -194,6 +194,7 @@ class IssuesController < ApplicationController :change_status => User.current.allowed_to?(:change_issue_status, @project), :add => User.current.allowed_to?(:add_issues, @project), :move => User.current.allowed_to?(:move_issues, @project), + :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), :delete => User.current.allowed_to?(:delete_issues, @project)} render :layout => false end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0f50cd780..37a788690 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -57,11 +57,13 @@ class ProjectsController < ApplicationController # Add a new project def add @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.all @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}") @project = Project.new(params[:project]) @project.enabled_module_names = Redmine::AccessControl.available_project_modules 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 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)) } @@ -80,7 +82,7 @@ class ProjectsController < ApplicationController @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} @subprojects = @project.active_children @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.trackers @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false]) @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id]) @total_hours = @project.time_entries.sum(:hours) @@ -92,6 +94,7 @@ class ProjectsController < ApplicationController @custom_fields = IssueCustomField.find(:all) @issue_category ||= IssueCategory.new @member ||= @project.members.new + @trackers = Tracker.all @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } @repository ||= @project.repository @wiki ||= @project.wiki @@ -207,7 +210,7 @@ class ProjectsController < ApplicationController @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue]) @issue.project = @project @issue.author = User.current - @issue.tracker ||= Tracker.find(params[:tracker_id]) + @issue.tracker ||= @project.trackers.find(params[:tracker_id]) default_status = IssueStatus.default unless default_status @@ -293,6 +296,7 @@ class ProjectsController < ApplicationController def move_issues @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids] redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues + @projects = [] # find projects to which the user is allowed to move the issue if User.current.admin? @@ -301,14 +305,14 @@ class ProjectsController < ApplicationController else User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)} end - # issue can be moved to any tracker - @trackers = Tracker.find(:all) - if request.post? && params[:new_project_id] && @projects.collect(&:id).include?(params[:new_project_id].to_i) && params[:new_tracker_id] - new_project = Project.find_by_id(params[:new_project_id]) - new_tracker = params[:new_tracker_id].blank? ? nil : Tracker.find_by_id(params[:new_tracker_id]) + @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] + @target_project ||= @project + @trackers = @target_project.trackers + if request.post? + new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) unsaved_issue_ids = [] @issues.each do |issue| - unsaved_issue_ids << issue.id unless issue.move_to(new_project, new_tracker) + unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) end if unsaved_issue_ids.empty? flash[:notice] = l(:notice_successful_update) unless @issues.empty? @@ -316,7 +320,9 @@ class ProjectsController < ApplicationController flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) end redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return end + render :layout => false if request.xhr? end # Add a news to @project @@ -354,13 +360,13 @@ class ProjectsController < ApplicationController # Show changelog for @project def changelog - @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') + @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') retrieve_selected_tracker_ids(@trackers) @versions = @project.versions.sort end def roadmap - @trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position') + @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true]) retrieve_selected_tracker_ids(@trackers) @versions = @project.versions.sort @versions = @versions.select {|v| !v.completed? } unless params[:completed] diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 6b95944a0..e18e117a6 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -25,7 +25,7 @@ class ReportsController < ApplicationController case params[:detail] when "tracker" @field = "tracker_id" - @rows = Tracker.find :all, :order => 'position' + @rows = @project.trackers @data = issues_by_tracker @report_title = l(:field_tracker) render :template => "reports/issue_report_details" @@ -60,7 +60,7 @@ class ReportsController < ApplicationController @report_title = l(:field_subproject) render :template => "reports/issue_report_details" else - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.trackers @versions = @project.versions.sort @priorities = Enumeration::get_values('IPRI') @categories = @project.issue_categories diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5b78db71c..4b1b1a954 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -190,7 +190,7 @@ module ProjectsHelper end if Object.const_defined?(:Magick) def new_issue_selector - trackers = Tracker.find(:all, :order => 'position') + trackers = @project.trackers # can't use form tag inside helper content_tag('form', select_tag('tracker_id', '' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"), diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index afe4c1afb..c3d6b7bb9 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -31,9 +31,9 @@ protected when 'float' begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end when 'date' - errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.empty? + errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.blank? when 'list' - errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include? value or value.empty? + errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank? end end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 60cca4051..f7b01ea6a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -40,7 +40,7 @@ class Issue < ActiveRecord::Base acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} - validates_presence_of :subject, :description, :priority, :tracker, :author, :status + 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 @@ -106,6 +106,10 @@ class Issue < ActiveRecord::Base end end + def validate_on_create + errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker) + end + def before_create # default assignment based on category if assigned_to.nil? && category && category.assigned_to diff --git a/app/models/project.rb b/app/models/project.rb index afaa049c6..be46d6189 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -24,6 +24,7 @@ class Project < ActiveRecord::Base has_many :users, :through => :members has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :enabled_modules, :dependent => :delete_all + has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] has_many :issue_changes, :through => :issues, :source => :journals has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 90ef31912..6de2a098c 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -29,6 +29,10 @@ class Tracker < ActiveRecord::Base def to_s; name end + def self.all + find(:all, :order => 'position') + end + private def check_integrity raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id]) diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml index 3b42ce465..6e61755e8 100644 --- a/app/views/issues/_sidebar.rhtml +++ b/app/views/issues/_sidebar.rhtml @@ -1,4 +1,4 @@ -<% if authorize_for('projects', 'add_issue') %> +<% if authorize_for('projects', 'add_issue') && @project.trackers.any? %>

<%= l(:label_issue_new) %>

<%= l(:label_tracker) %>: <%= new_issue_selector %> <% end %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index e44911daf..3af49fb04 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -32,7 +32,7 @@
  • <%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue}, - :class => 'icon-copy', :disabled => !@can[:add] %>
  • + :class => 'icon-copy', :disabled => !@can[:copy] %>
  • <%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon-move', :disabled => !@can[:move] %>
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index aa30f1eaa..885ccf4bd 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -17,16 +17,32 @@ <% for @custom_value in @custom_values %>

    <%= custom_field_tag_with_label @custom_value %>

    <% end %> + + +<% unless @trackers.empty? %> +
    <%=l(:label_tracker_plural)%> +<% @trackers.each do |tracker| %> + +<% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +
    +<% end %> <% unless @custom_fields.empty? %> -

    +

    <%=l(:label_custom_field_plural)%> <% for custom_field in @custom_fields %> + +<% end %> +
    <% end %> - + <% content_for :header_tags do %> <%= javascript_include_tag 'calendar/calendar' %> diff --git a/app/views/projects/add.rhtml b/app/views/projects/add.rhtml index 4818cae4a..e3ee0b591 100644 --- a/app/views/projects/add.rhtml +++ b/app/views/projects/add.rhtml @@ -3,13 +3,13 @@ <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> -
    -

    +

    <%= l(:label_module_plural) %> <% Redmine::AccessControl.available_project_modules.each do |m| %> -<%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> <%= m.to_s.humanize %> -<% end %>

    -
    - + +<% end %> + <%= submit_tag l(:button_save) %> <% end %> diff --git a/app/views/projects/move_issues.rhtml b/app/views/projects/move_issues.rhtml index b29653037..95eaf9dec 100644 --- a/app/views/projects/move_issues.rhtml +++ b/app/views/projects/move_issues.rhtml @@ -1,7 +1,7 @@

    <%=l(:button_move)%>

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

    @@ -15,7 +15,12 @@

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

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

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

    diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index 94df0bf3d..458e7975e 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -56,7 +56,7 @@
    <% content_for :sidebar do %> - <% if authorize_for('projects', 'add_issue') %> + <% if authorize_for('projects', 'add_issue') && @project.trackers.any? %>

    <%= l(:label_issue_new) %>

    <%= l(:label_tracker) %>: <%= new_issue_selector %> <% end %> diff --git a/db/migrate/081_create_projects_trackers.rb b/db/migrate/081_create_projects_trackers.rb new file mode 100644 index 000000000..70fea188e --- /dev/null +++ b/db/migrate/081_create_projects_trackers.rb @@ -0,0 +1,19 @@ +class CreateProjectsTrackers < ActiveRecord::Migration + def self.up + create_table :projects_trackers, :id => false do |t| + t.column :project_id, :integer, :default => 0, :null => false + t.column :tracker_id, :integer, :default => 0, :null => false + end + add_index :projects_trackers, :project_id, :name => :projects_trackers_project_id + + # Associates all trackers to all projects (as it was before) + tracker_ids = Tracker.find(:all).collect(&:id) + Project.find(:all).each do |project| + project.tracker_ids = tracker_ids + end + end + + def self.down + drop_table :projects_trackers + end +end diff --git a/test/fixtures/projects_trackers.yml b/test/fixtures/projects_trackers.yml new file mode 100644 index 000000000..cfca5b228 --- /dev/null +++ b/test/fixtures/projects_trackers.yml @@ -0,0 +1,46 @@ +--- +projects_trackers_012: + project_id: 4 + tracker_id: 3 +projects_trackers_001: + project_id: 1 + tracker_id: 1 +projects_trackers_013: + project_id: 5 + tracker_id: 1 +projects_trackers_002: + project_id: 1 + tracker_id: 2 +projects_trackers_014: + project_id: 5 + tracker_id: 2 +projects_trackers_003: + project_id: 1 + tracker_id: 3 +projects_trackers_015: + project_id: 5 + tracker_id: 3 +projects_trackers_004: + project_id: 2 + tracker_id: 1 +projects_trackers_005: + project_id: 2 + tracker_id: 2 +projects_trackers_006: + project_id: 2 + tracker_id: 3 +projects_trackers_007: + project_id: 3 + tracker_id: 1 +projects_trackers_008: + project_id: 3 + tracker_id: 2 +projects_trackers_009: + project_id: 3 + tracker_id: 3 +projects_trackers_010: + project_id: 4 + tracker_id: 1 +projects_trackers_011: + project_id: 4 + tracker_id: 2 diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index e6c06cf56..c41adaa6c 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -22,7 +22,7 @@ require 'projects_controller' class ProjectsController; def rescue_action(e) raise e end; end class ProjectsControllerTest < Test::Unit::TestCase - fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :issue_statuses, :enabled_modules, :enumerations + fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations def setup @controller = ProjectsController.new diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index 6ffbe0a8b..da91dd02c 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -18,7 +18,7 @@ require File.dirname(__FILE__) + '/../test_helper' class IssueTest < Test::Unit::TestCase - fixtures :projects, :users, :members, :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_values, :time_entries 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) From 5e6fa147da47a4b1e17b0d39567b8ab7641b1a8c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 21 Nov 2007 18:45:18 +0000 Subject: [PATCH 009/710] * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter' * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt) * Fixed: Wrong feed URLs on the home page git-svn-id: http://redmine.rubyforge.org/svn/trunk@921 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 4 ++-- app/views/welcome/index.rhtml | 3 +-- lib/redmine/scm/adapters/mercurial_adapter.rb | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index f869f648b..4cc5a63a5 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -164,8 +164,6 @@ class Query < ActiveRecord::Base end @project.all_custom_fields.select(&:is_filter?).each do |field| case field.field_format - when "string", "int" - options = { :type => :string, :order => 20 } when "text" options = { :type => :text, :order => 20 } when "list" @@ -174,6 +172,8 @@ class Query < ActiveRecord::Base 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 diff --git a/app/views/welcome/index.rhtml b/app/views/welcome/index.rhtml index af09eea93..d1e871c4a 100644 --- a/app/views/welcome/index.rhtml +++ b/app/views/welcome/index.rhtml @@ -26,6 +26,5 @@ <% content_for :header_tags do %> -<%= auto_discovery_link_tag(:rss, {:controller => 'feeds', :action => 'news', :key => @key}, {:title => l(:label_news_latest)}) %> -<%= auto_discovery_link_tag(:atom, {:controller => 'feeds', :action => 'news', :key => @key, :format => 'atom'}, {:title => l(:label_news_latest)}) %> +<%= auto_discovery_link_tag(:atom, {:controller => 'news', :action => 'index', :key => User.current.rss_key, :format => 'atom'}, {:title => l(:label_news_latest)}) %> <% end %> diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 54fa8c4f8..a631916c5 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -91,7 +91,7 @@ module Redmine :author => changeset[:user], :time => Time.parse(changeset[:date]), :message => changeset[:description], - :paths => changeset[:files].split.collect{|path| {:action => 'X', :path => "/#{path}"}} + :paths => changeset[:files].to_s.split.collect{|path| {:action => 'X', :path => "/#{path}"}} }) changeset = {} end From 0634591b3db01098057f2e20e101d7886ddbff5a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 22 Nov 2007 18:03:02 +0000 Subject: [PATCH 010/710] Themes: * Fixed: themes are not found when running Apache+fastcgi * Added 'Classic' theme (inspired from the v0.51 design) git-svn-id: http://redmine.rubyforge.org/svn/trunk@922 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/layouts/base.rhtml | 18 ++++----- lib/redmine/themes.rb | 2 +- .../classic/stylesheets/application.css | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 public/themes/classic/stylesheets/application.css diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml index 3cd384c1e..df7b2daf9 100644 --- a/app/views/layouts/base.rhtml +++ b/app/views/layouts/base.rhtml @@ -22,18 +22,18 @@
    <% if User.current.logged? %> <%=l(:label_logged_as)%> <%= User.current.login %> - - <%= link_to l(:label_my_account), { :controller => 'my', :action => 'account' } %> - <%= link_to l(:label_logout), { :controller => 'account', :action => 'logout' } %> + <%= link_to l(:label_my_account), { :controller => 'my', :action => 'account' }, :class => 'myaccount' %> + <%= link_to l(:label_logout), { :controller => 'account', :action => 'logout' }, :class => 'logout' %> <% else %> - <%= link_to l(:label_login), { :controller => 'account', :action => 'login' } %> - <%= link_to(l(:label_register), :controller => 'account',:action => 'register') if Setting.self_registration? %> + <%= link_to l(:label_login), { :controller => 'account', :action => 'login' }, :class => 'signin' %> + <%= link_to(l(:label_register), { :controller => 'account',:action => 'register' }, :class => 'register') if Setting.self_registration? %> <% end %>
    - <%= link_to l(:label_home), home_url %> - <%= link_to l(:label_my_page), { :controller => 'my', :action => 'page'} if User.current.logged? %> - <%= link_to l(:label_project_plural), { :controller => 'projects' } %> - <%= link_to l(:label_administration), { :controller => 'admin' } if User.current.admin? %> - <%= link_to l(:label_help), Redmine::Info.help_url %> + <%= link_to l(:label_home), home_url, :class => 'home' %> + <%= link_to l(:label_my_page), { :controller => 'my', :action => 'page'}, :class => 'mypage' if User.current.logged? %> + <%= link_to l(:label_project_plural), { :controller => 'projects' }, :class => 'projects' %> + <%= link_to l(:label_administration), { :controller => 'admin' }, :class => 'admin' if User.current.admin? %> + <%= link_to l(:label_help), Redmine::Info.help_url, :class => 'help' %> diff --git a/app/views/messages/show.rhtml b/app/views/messages/show.rhtml index 772f0653e..e39c09d50 100644 --- a/app/views/messages/show.rhtml +++ b/app/views/messages/show.rhtml @@ -1,27 +1,30 @@

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

    +

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

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

    +

    <%= l(:label_reply_plural) %>

    <% @message.children.each do |message| %> "> -

    <%=h message.subject %> - <%= message.author.name %>, <%= format_time(message.created_on) %>

    +

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

    <%= textilizable message.content %>
    + <%= link_to_attachments message.attachments, :no_author => true %> <% end %> +
    <% if authorize_for('messages', 'reply') %> -

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

    +

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

    <% end %> From c21ee95ade208c09562c1a3940bec7535839a17c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 23 Nov 2007 18:27:26 +0000 Subject: [PATCH 012/710] * Updated Russian translation (iGor kMeta) * Fixed front page typo: View all issues instead of View all news (Derek Montgomery) git-svn-id: http://redmine.rubyforge.org/svn/trunk@924 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/welcome/index.rhtml | 2 +- lang/ru.yml | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/views/welcome/index.rhtml b/app/views/welcome/index.rhtml index d1e871c4a..2e7ec2c06 100644 --- a/app/views/welcome/index.rhtml +++ b/app/views/welcome/index.rhtml @@ -6,7 +6,7 @@

    <%=l(:label_news_latest)%>

    <%= render :partial => 'news/news', :collection => @news %> - <%= link_to l(:label_issue_view_all), :controller => 'news' %> + <%= link_to l(:label_news_view_all), :controller => 'news' %>
    <% end %> diff --git a/lang/ru.yml b/lang/ru.yml index 010b687e6..ffc964f0c 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -29,7 +29,7 @@ activerecord_error_blank: необходимо заполнить activerecord_error_too_long: слишком длинное значение activerecord_error_too_short: слишком короткое значение activerecord_error_wrong_length: не соответствует длине -activerecord_error_taken: уже был принят +activerecord_error_taken: уже используется activerecord_error_not_a_number: не является числом activerecord_error_not_a_date: дата недействительна activerecord_error_greater_than_start_date: должна быть позднее даты начала @@ -48,8 +48,8 @@ general_text_no: 'Нет' general_text_yes: 'Да' general_lang_name: 'Russian (Русский)' general_csv_separator: ',' -general_csv_encoding: ISO-8859-1 -general_pdf_encoding: ISO-8859-1 +general_csv_encoding: UTF-8 +general_pdf_encoding: UTF-8 general_day_names: Понедельник,Вторник,Среда,Четверг,Пятница,Суббота,Воскресенье general_first_day_of_week: '1' @@ -148,17 +148,17 @@ field_attr_lastname: Атрибут Фамилия field_attr_mail: Атрибут Email field_onthefly: Создание пользователя на лету field_start_date: Начало -field_done_ratio: %% Готовности +field_done_ratio: Готовность в %% field_auth_source: Режим аутентификации field_hide_mail: Скрывать мой email field_comments: Комментарий field_url: URL field_start_page: Стартовая страница field_subproject: Подпроект -field_hours: Часы +field_hours: Час(а)(ов) field_activity: Деятельность field_spent_on: Дата -field_identifier: Признак +field_identifier: Ун. идентификатор field_is_filter: Используется в качестве фильтра field_issue_to_id: Связанные задачи field_delay: Отложить @@ -243,13 +243,13 @@ label_administration: Администрирование label_login: Войти label_logout: Выйти label_help: Помощь -label_reported_issues: Задачи, по которым предоставлен отчет +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_registered_on: Зарегистрирован(а) +label_activity: Активность label_new: Новый label_logged_as: Вошел как label_environment: Окружение @@ -460,7 +460,7 @@ button_add: Добавить button_change: Изменить button_apply: Применить button_clear: Очистить -button_lock: Закрыть +button_lock: Заблокировать button_unlock: Открыть button_download: Загрузить button_list: Список @@ -538,10 +538,10 @@ default_activity_development: Разработка enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) -label_registration_activation_by_email: account activation by email -mail_subject_account_activation_request: Redmine 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 +label_registration_activation_by_email: активация аккаунтов по email +mail_subject_account_activation_request: Запрос на активацию пользователя в системе Redmine +mail_body_account_activation_request: 'Новый пользователь (%s) зарегистирован. Аккаунт ожидает вашего утверждения:' +label_registration_automatic_activation: автоматическая активация аккаунтов +label_registration_manual_activation: активировать аккаунты вручную +notice_account_pending: "Ваш аккаунт уже создан и ожидает подтверждения администратора." +field_time_zone: Часовой пояс From 866e9e2503713c67fd33b389d4e840c04ce1562d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 23 Nov 2007 23:23:39 +0000 Subject: [PATCH 013/710] Fixed: error on account/register when validation fails. git-svn-id: http://redmine.rubyforge.org/svn/trunk@925 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/account_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 37a810bdf..a1cbf5ffd 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -115,10 +115,10 @@ class AccountController < ApplicationController @user.login = params[:user][:login] @user.status = User::STATUS_REGISTERED @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] - if params["custom_fields"] - @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } - @user.custom_values = @custom_values - end + @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 From 29b3614bcb759214bb1aba77c27ac11c8ef6b15b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 24 Nov 2007 12:25:07 +0000 Subject: [PATCH 014/710] Forums enhancements: * messages can now be edited/deleted (explicit permissions need to be given) * topics can be locked so that no reply can be added (only by users allowed to edit messages) * topics can be marked as sticky so that they always appear at the top of the list (only by users allowed to edit messages) git-svn-id: http://redmine.rubyforge.org/svn/trunk@926 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/boards_controller.rb | 2 +- app/controllers/messages_controller.rb | 51 +++++++++++++++++--- app/helpers/application_helper.rb | 4 +- app/models/message.rb | 18 +++++++ app/views/boards/index.rhtml | 2 +- app/views/boards/show.rhtml | 18 +++---- app/views/messages/_form.rhtml | 8 ++- app/views/messages/edit.rhtml | 6 +++ app/views/messages/show.rhtml | 27 +++++++---- db/migrate/082_add_messages_locked.rb | 9 ++++ db/migrate/083_add_messages_sticky.rb | 9 ++++ lib/redmine.rb | 2 + public/images/sticky.png | Bin 0 -> 461 bytes public/stylesheets/application.css | 5 ++ test/fixtures/boards.yml | 6 +-- test/fixtures/messages.yml | 38 +++++++++++++-- test/functional/boards_controller_test.rb | 50 +++++++++++++++++++ test/functional/messages_controller_test.rb | 49 +++++++++++++++++++ test/unit/message_test.rb | 26 ++++++++++ 19 files changed, 294 insertions(+), 36 deletions(-) create mode 100644 app/views/messages/edit.rhtml create mode 100644 db/migrate/082_add_messages_locked.rb create mode 100644 db/migrate/083_add_messages_sticky.rb create mode 100644 public/images/sticky.png create mode 100644 test/functional/boards_controller_test.rb create mode 100644 test/functional/messages_controller_test.rb diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 3a8b021a3..200792370 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -41,7 +41,7 @@ class BoardsController < ApplicationController @topic_count = @board.topics.count @topic_pages = Paginator.new self, @topic_count, 25, params['page'] - @topics = @board.topics.find :all, :order => sort_clause, + @topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}", :include => [:author, {:last_reply => :author}], :limit => @topic_pages.items_per_page, :offset => @topic_pages.current.offset diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 9352c4af4..46c9adadd 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -17,22 +17,30 @@ class MessagesController < ApplicationController layout 'base' - before_filter :find_project, :authorize + before_filter :find_board, :only => :new + before_filter :find_message, :except => :new + before_filter :authorize verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } helper :attachments include AttachmentsHelper + # Show a topic and its replies def show @reply = Message.new(:subject => "RE: #{@message.subject}") render :action => "show", :layout => false if request.xhr? end + # Create a new topic def new @message = Message.new(params[:message]) @message.author = User.current - @message.board = @board + @message.board = @board + if params[:message] && User.current.allowed_to?(:edit_messages, @project) + @message.locked = params[:message]['locked'] + @message.sticky = params[:message]['sticky'] + end if request.post? && @message.save params[:attachments].each { |file| Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0 @@ -41,24 +49,55 @@ class MessagesController < ApplicationController end end + # Reply to a topic def reply @reply = Message.new(params[:reply]) @reply.author = User.current @reply.board = @board - @message.children << @reply + @topic.children << @reply if !@reply.new_record? params[:attachments].each { |file| Attachment.create(:container => @reply, :file => file, :author => User.current) if file.size > 0 } if params[:attachments] and params[:attachments].is_a? Array end - redirect_to :action => 'show', :id => @message + redirect_to :action => 'show', :id => @topic + end + + # Edit a message + def edit + if params[:message] && User.current.allowed_to?(:edit_messages, @project) + @message.locked = params[:message]['locked'] + @message.sticky = params[:message]['sticky'] + end + if request.post? && @message.update_attributes(params[:message]) + params[:attachments].each { |file| + Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0 + } if params[:attachments] and params[:attachments].is_a? Array + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'show', :id => @topic + end + end + + # Delete a messages + def destroy + @message.destroy + redirect_to @message.parent.nil? ? + { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : + { :action => 'show', :id => @message.parent } end private - def find_project + def find_message + find_board + @message = @board.messages.find(params[:id], :include => :parent) + @topic = @message.root + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_board @board = Board.find(params[:board_id], :include => :project) @project = @board.project - @message = @board.topics.find(params[:id]) if params[:id] rescue ActiveRecord::RecordNotFound render_404 end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9c8e9c67d..f4746c627 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -34,7 +34,7 @@ module ApplicationHelper # Display a link to user's account page def link_to_user(user) - link_to user.name, :controller => 'account', :action => 'show', :id => user + user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous' end def link_to_issue(issue) @@ -92,7 +92,7 @@ module ApplicationHelper 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.name, time_tag) + l(:label_added_time_by, author || 'Anonymous', time_tag) end def day_name(day) diff --git a/app/models/message.rb b/app/models/message.rb index 909c06a9e..038665cce 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -30,9 +30,15 @@ class Message < ActiveRecord::Base :description => :content, :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}} + attr_protected :locked, :sticky validates_presence_of :subject, :content validates_length_of :subject, :maximum => 255 + def validate_on_create + # Can not reply to a locked topic + errors.add_to_base 'Topic is locked' if root.locked? + end + def after_create board.update_attribute(:last_message_id, self.id) board.increment! :messages_count @@ -43,6 +49,18 @@ class Message < ActiveRecord::Base end end + def after_destroy + # The following line is required so that the previous counter + # updates (due to children removal) are not overwritten + board.reload + board.decrement! :messages_count + board.decrement! :topics_count unless parent + end + + def sticky? + sticky == 1 + end + def project board.project end diff --git a/app/views/boards/index.rhtml b/app/views/boards/index.rhtml index 3291d0194..cd4e85e9a 100644 --- a/app/views/boards/index.rhtml +++ b/app/views/boards/index.rhtml @@ -19,7 +19,7 @@ <% if board.last_message %> - <%= board.last_message.author.name %>, <%= format_time(board.last_message.created_on) %>
    + <%= authoring board.last_message.created_on, board.last_message.author %>
    <%= link_to_message board.last_message %> <% end %>
    diff --git a/app/views/boards/show.rhtml b/app/views/boards/show.rhtml index 0af89fdb7..8bcf960b2 100644 --- a/app/views/boards/show.rhtml +++ b/app/views/boards/show.rhtml @@ -18,7 +18,7 @@

    <%=h @board.name %>

    <% if @topics.any? %> - +
    @@ -28,18 +28,16 @@ <% @topics.each do |topic| %> - - - - - - + + + + + <% end %> diff --git a/app/views/messages/_form.rhtml b/app/views/messages/_form.rhtml index 25d88cd44..c2f7fb569 100644 --- a/app/views/messages/_form.rhtml +++ b/app/views/messages/_form.rhtml @@ -3,7 +3,13 @@


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

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

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

    <%= wikitoolbar_for 'message_content' %> diff --git a/app/views/messages/edit.rhtml b/app/views/messages/edit.rhtml new file mode 100644 index 000000000..808b6ea27 --- /dev/null +++ b/app/views/messages/edit.rhtml @@ -0,0 +1,6 @@ +

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

    + +<% form_for :message, @message, :url => {:action => 'edit'}, :html => {:multipart => true} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/app/views/messages/show.rhtml b/app/views/messages/show.rhtml index e39c09d50..bb7e2b7f3 100644 --- a/app/views/messages/show.rhtml +++ b/app/views/messages/show.rhtml @@ -1,28 +1,37 @@ -

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

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

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

    -

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

    +

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

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

    -

    <%= l(:label_reply_plural) %>

    -<% @message.children.each do |message| %> +<% @topic.children.each do |message| %> "> +
    + <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => message}, :class => 'icon icon-edit' %> + <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> +
    +

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

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

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

    <% for issue in issues %> - <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> + "> - - + <% end %> diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml index 5c6c906db..89d11ddea 100644 --- a/app/views/my/page.rhtml +++ b/app/views/my/page.rhtml @@ -31,3 +31,10 @@ <% end if @blocks['right'] %> +<% content_for :header_tags do %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + + +<%= javascript_tag 'new ContextMenu({})' %> From f2a12d9eb5aeacf2b538c9b0062f197112c0f3ab Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 24 Nov 2007 16:55:08 +0000 Subject: [PATCH 018/710] Added user status criteria in Redmine.pm git-svn-id: http://redmine.rubyforge.org/svn/trunk@930 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm index 7505b71cb..b76622e3d 100644 --- a/extra/svn/Redmine.pm +++ b/extra/svn/Redmine.pm @@ -176,7 +176,7 @@ sub is_member { my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); my $sth = $dbh->prepare( - "SELECT hashed_password FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND login=? AND identifier=?;" + "SELECT hashed_password FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;" ); $sth->execute($redmine_user, $project_id); From 3937caeb3c8fe87763e19c179d418949fb31dc61 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 Nov 2007 12:11:40 +0000 Subject: [PATCH 019/710] Trac importer improvements (by Mat Trudel): * better support for wiki internal links (still not perfect, but much improved) * support for unordered lists * support for most of trac's highlighting tags (underline, bold, etc) * import progress dots now flush to stdout on every dot, so the import doesn't look frozen * support for migration of multiple trac instances into a single Redmine install (as separate projects) git-svn-id: http://redmine.rubyforge.org/svn/trunk@931 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 77 +++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index ba002cb6f..6467a5430 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -24,6 +24,7 @@ namespace :redmine do task :migrate_from_trac => :environment do module TracMigrate + TICKET_MAP = []; DEFAULT_STATUS = IssueStatus.default assigned_status = IssueStatus.find_by_position(2) @@ -181,11 +182,38 @@ namespace :redmine do # Basic wiki syntax conversion def self.convert_wiki_text(text) # Titles - text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "h#{$1.length}. #{$2}\n"} - # Links + text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"} + # External Links text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"} + # Internal Links + text = text.gsub(/[[BR]]/, "\n") # This has to go before the rules below + text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} + text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} # Revisions links text = text.gsub(/\[(\d+)\]/, 'r\1') + # Ticket number re-writing + text = text.gsub(/#(\d+)/) do |s| + TICKET_MAP[$1.to_i] ||= $1 + "\##{TICKET_MAP[$1.to_i]}" + end + # Preformatted blocks + text = text.gsub(/\{\{\{/, '
    ')
    +        text = text.gsub(/\}\}\}/, '
    ') + # Highlighting + text = text.gsub(/'''''([^\s])/, '_*\1') + text = text.gsub(/([^\s])'''''/, '\1*_') + text = text.gsub(/'''/, '*') + text = text.gsub(/''/, '_') + text = text.gsub(/__/, '+') + text = text.gsub(/~~/, '-') + text = text.gsub(/`/, '@') + text = text.gsub(/,,/, '~') + # Lists + text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "} + + text end @@ -193,16 +221,9 @@ namespace :redmine do establish_connection({:adapter => trac_adapter, :database => trac_db_path}) - # Quick database test before clearing Redmine data + # Quick database test TracComponent.count - - puts "Deleting data" - CustomField.destroy_all - Issue.destroy_all - IssueCategory.destroy_all - Version.destroy_all - User.destroy_all "login <> 'admin'" - + migrated_components = 0 migrated_milestones = 0 migrated_tickets = 0 @@ -215,6 +236,7 @@ namespace :redmine do issues_category_map = {} TracComponent.find(:all).each do |component| print '.' + STDOUT.flush c = IssueCategory.new :project => @target_project, :name => encode(component.name[0, limit_for(IssueCategory, 'name')]) next unless c.save @@ -228,9 +250,10 @@ namespace :redmine do version_map = {} TracMilestone.find(:all).each do |milestone| print '.' + STDOUT.flush v = Version.new :project => @target_project, :name => encode(milestone.name[0, limit_for(Version, 'name')]), - :description => encode(milestone.description[0, limit_for(Version, 'description')]), + :description => encode(milestone.description.to_s[0, limit_for(Version, 'description')]), :effective_date => milestone.due next unless v.save version_map[milestone.name] = v @@ -244,9 +267,16 @@ namespace :redmine do custom_field_map = {} TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field| print '.' - f = IssueCustomField.new :name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, - :field_format => 'string' - next unless f.save + STDOUT.flush + # Redmine custom field name + field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize + # Find if the custom already exists in Redmine + f = IssueCustomField.find_by_name(field_name) + # Or create a new one + f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize, + :field_format => 'string') + + next if f.new_record? f.trackers = Tracker.find(:all) f.projects << @target_project custom_field_map[field.name] = f @@ -254,9 +284,10 @@ namespace :redmine do puts # Trac 'resolution' field as a Redmine custom field - r = IssueCustomField.new :name => 'Resolution', + r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" }) + r = IssueCustomField.new(:name => 'Resolution', :field_format => 'list', - :is_filter => true + :is_filter => true) if r.nil? r.trackers = Tracker.find(:all) r.projects << @target_project r.possible_values = %w(fixed invalid wontfix duplicate worksforme) @@ -264,8 +295,9 @@ namespace :redmine do # Tickets print "Migrating tickets" - TracTicket.find(:all).each do |ticket| + TracTicket.find(:all, :order => 'id ASC').each do |ticket| print '.' + STDOUT.flush i = Issue.new :project => @target_project, :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]), :description => convert_wiki_text(encode(ticket.description)), @@ -276,9 +308,10 @@ namespace :redmine do i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank? i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER - i.id = ticket.id i.custom_values << CustomValue.new(:custom_field => custom_field_map['resolution'], :value => ticket.resolution) unless ticket.resolution.blank? + i.id = ticket.id unless Issue.exists?(ticket.id) next unless i.save + TICKET_MAP[ticket.id] = i.id migrated_tickets += 1 # Owner @@ -327,6 +360,7 @@ namespace :redmine do # Custom fields ticket.customs.each do |custom| + next if custom_field_map[custom.name].nil? v = CustomValue.new :custom_field => custom_field_map[custom.name], :value => custom.value v.customized = i @@ -344,6 +378,7 @@ namespace :redmine do if wiki.save TracWikiPage.find(:all, :order => 'name, version').each do |page| print '.' + STDOUT.flush p = wiki.find_or_new_page(page.name) p.content = WikiContent.new(:page => p) if p.new_record? p.content.text = page.text @@ -415,6 +450,8 @@ namespace :redmine do puts "Unable to create a project with identifier '#{identifier}'!" unless project.save # enable issues and wiki for the created project project.enabled_module_names = ['issue_tracking', 'wiki'] + project.trackers << TRACKER_BUG + project.trackers << TRACKER_FEATURE end @target_project = project.new_record? ? nil : project end @@ -436,7 +473,7 @@ namespace :redmine do end puts - puts "WARNING: Your Redmine data will be deleted during this process." + puts "WARNING: Your Redmine install will have a new project added during this process." print "Are you sure you want to continue ? [y/N] " break unless STDIN.gets.match(/^y$/i) puts From 508b0bbb8edf1423160021db6a4844a0d957c476 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 26 Nov 2007 18:47:49 +0000 Subject: [PATCH 020/710] =?UTF-8?q?*=20Updated=20Spanish=20translation=20(?= =?UTF-8?q?Gumer=20Coronel=20P=C3=A9rez)=20*=20Fixed=20mailer=20test=20err?= =?UTF-8?q?ors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@932 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- .../mailer/message_posted.text.html.rhtml | 2 +- .../mailer/message_posted.text.plain.rhtml | 2 +- lang/es.yml | 202 +++++++++--------- 3 files changed, 103 insertions(+), 103 deletions(-) diff --git a/app/views/mailer/message_posted.text.html.rhtml b/app/views/mailer/message_posted.text.html.rhtml index 558a6e52a..837272c0a 100644 --- a/app/views/mailer/message_posted.text.html.rhtml +++ b/app/views/mailer/message_posted.text.html.rhtml @@ -1,4 +1,4 @@

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

    -<%= @message.author.name %> +<%= @message.author %> <%= textilizable @message.content %> diff --git a/app/views/mailer/message_posted.text.plain.rhtml b/app/views/mailer/message_posted.text.plain.rhtml index cc1120567..ef6a3b3ae 100644 --- a/app/views/mailer/message_posted.text.plain.rhtml +++ b/app/views/mailer/message_posted.text.plain.rhtml @@ -1,4 +1,4 @@ <%= @message_url %> -<%= @message.author.name %> +<%= @message.author %> <%= @message.content %> diff --git a/lang/es.yml b/lang/es.yml index 8ade705c8..ab4309ca1 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -5,8 +5,8 @@ actionview_datehelper_select_month_names: Enero,Febrero,Marzo,Abril,Mayo,Junio,J actionview_datehelper_select_month_names_abbr: Ene,Feb,Mar,Abr,Mayo,Jun,Jul,Ago,Sep,Oct,Nov,Dic actionview_datehelper_select_month_prefix: actionview_datehelper_select_year_prefix: -actionview_datehelper_time_in_words_day: 1 day -actionview_datehelper_time_in_words_day_plural: %d days +actionview_datehelper_time_in_words_day: 1 día +actionview_datehelper_time_in_words_day_plural: %d días actionview_datehelper_time_in_words_hour_about: una hora aproximadamente actionview_datehelper_time_in_words_hour_about_plural: aproximadamente %d horas actionview_datehelper_time_in_words_hour_about_single: una hora aproximadamente @@ -17,15 +17,15 @@ actionview_datehelper_time_in_words_minute_plural: %d minutos actionview_datehelper_time_in_words_minute_single: 1 minuto actionview_datehelper_time_in_words_second_less_than: menos de un segundo actionview_datehelper_time_in_words_second_less_than_plural: menos de %d segundos -actionview_instancetag_blank_option: Por favor selecciona +actionview_instancetag_blank_option: Por favor seleccione -activerecord_error_inclusion: is not included in the list -activerecord_error_exclusion: is reserved -activerecord_error_invalid: is invalid -activerecord_error_confirmation: doesn't match confirmation -activerecord_error_accepted: must be accepted -activerecord_error_empty: can't be empty -activerecord_error_blank: can't be blank +activerecord_error_inclusion: no está incluído en la lista +activerecord_error_exclusion: está reservado +activerecord_error_invalid: no es válido +activerecord_error_confirmation: la confirmación no coincide +activerecord_error_accepted: debe ser aceptado +activerecord_error_empty: no puede estar vacío +activerecord_error_blank: no puede estar en blanco activerecord_error_too_long: es demasiado largo activerecord_error_too_short: es demasiado corto activerecord_error_wrong_length: la longitud es incorrecta @@ -54,27 +54,27 @@ general_day_names: Lunes,Martes,Miércoles,Jueves,Viernes,Sábado,Domingo general_first_day_of_week: '1' notice_account_updated: Cuenta creada correctamente. -notice_account_invalid_creditentials: Inválido usuario o contraseña +notice_account_invalid_creditentials: Usuario o contraseña inválido. notice_account_password_updated: Contraseña modificada correctamente. -notice_account_wrong_password: Contraseña incorrecta +notice_account_wrong_password: Contraseña incorrecta. notice_account_register_done: Cuenta creada correctamente. notice_account_unknown_email: Usuario desconocido. -notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. +notice_can_t_change_password: Esta cuenta utiliza una fuente de autenticación externa. No es posible cambiar la contraseña. notice_account_lost_email_sent: Un correo con instrucciones para elegir una nueva contraseña le ha sido enviado. -notice_account_activated: Tu cuenta ha sido activada. Ahora se encuentra conectado. +notice_account_activated: Su cuenta ha sido activada. Ahora se encuentra conectado. notice_successful_create: Creación correcta. notice_successful_update: Modificación correcta. notice_successful_delete: Borrado correcto. notice_successful_connection: Conexión correcta. -notice_file_not_found: La página que intentabas tener acceso no existe ni se ha quitado. +notice_file_not_found: La página a la que intentas acceder no existe. notice_locking_conflict: Los datos han sido modificados por otro usuario. -notice_scm_error: La entrada y/o la revisión no existe en el depósito. +notice_scm_error: La entrada y/o la revisión no existe en el repositorio. notice_not_authorized: No tiene autorización para acceder a esta página. mail_subject_lost_password: Tu contraseña del CIYAT - Gestor de Solicitudes -mail_body_lost_password: 'To change your Redmine password, click on the following link:' +mail_body_lost_password: 'Para cambiar su contraseña de Redmine, haga click en el siguiente enlace:' mail_subject_register: Activación de la cuenta del CIYAT - Gestor de Solicitudes -mail_body_register: 'To activate your Redmine account, click on the following link:' +mail_body_register: 'Para activar su cuenta Redmine, haga click en el siguiente enlace:' gui_validation_error: 1 error gui_validation_error_plural: %d errores @@ -85,7 +85,7 @@ field_summary: Resumen field_is_required: Obligatorio field_firstname: Nombre field_lastname: Apellido -field_mail: Email +field_mail: Correo electrónico field_filename: Fichero field_filesize: Tamaño field_downloads: Descargas @@ -109,7 +109,7 @@ field_is_closed: Petición resuelta field_is_default: Estado por defecto field_tracker: Tracker field_subject: Tema -field_due_date: Fecha debida +field_due_date: Fecha fin field_assigned_to: Asignado a field_priority: Prioridad field_fixed_version: Versión corregida @@ -117,11 +117,11 @@ field_user: Usuario field_role: Perfil field_homepage: Sitio web field_is_public: Público -field_parent: Proyecto secundario de +field_parent: Proyecto padre field_is_in_chlog: Consultar las peticiones en el histórico field_is_in_roadmap: Consultar las peticiones en el roadmap field_login: Identificador -field_mail_notification: Notificación por mail +field_mail_notification: Notificaciones por correo field_admin: Administrador field_last_login_on: Última conexión field_language: Idioma @@ -139,11 +139,11 @@ field_attr_login: Cualidad del identificador field_attr_firstname: Cualidad del nombre field_attr_lastname: Cualidad del apellido field_attr_mail: Cualidad del Email -field_onthefly: Creación del usuario On-the-fly -field_start_date: Comienzo +field_onthefly: Creación del usuario "al vuelo" +field_start_date: Fecha de inicio field_done_ratio: %% Realizado -field_auth_source: Modo de la autentificación -field_hide_mail: Ocultar mi dirección de email +field_auth_source: Modo de identificación +field_hide_mail: Ocultar mi dirección de correo field_comment: Comentario field_url: URL field_start_page: Página principal @@ -156,24 +156,24 @@ field_is_filter: Usado como filtro field_issue_to_id: Petición Relacionada field_delay: Retraso -setting_app_title: Título del aplicación -setting_app_subtitle: Subtítulo del aplicación -setting_welcome_text: Texto bienvenida +setting_app_title: Título de la aplicación +setting_app_subtitle: Subtítulo de la aplicación +setting_welcome_text: Texto de bienvenida setting_default_language: Idioma por defecto -setting_login_required: Autentif. requerida +setting_login_required: Se requiere identificación setting_self_registration: Registro permitido setting_attachment_max_size: Tamaño máximo del fichero -setting_issues_export_limit: Issues export limit -setting_mail_from: Email de la emisión -setting_host_name: Nombre de anfitrión +setting_issues_export_limit: Límite de exportación de peticiones +setting_mail_from: Correo desde el que enviar mensajes +setting_host_name: Nombre de host setting_text_formatting: Formato de texto -setting_wiki_compression: Compresión de la historia de Wiki -setting_feeds_limit: Feed content limit -setting_autofetch_changesets: Autofetch SVN commits +setting_wiki_compression: Compresión del historial de Wiki +setting_feeds_limit: Límite de contenido para sindicación +setting_autofetch_changesets: Autorellenar los commits del repositorio setting_sys_api_enabled: Enable WS for repository management -setting_commit_ref_keywords: Referencing keywords -setting_commit_fix_keywords: Fixing keywords -setting_autologin: Autologin +setting_commit_ref_keywords: Palabras clave para la referencia +setting_commit_fix_keywords: Palabras clave para la corrección +setting_autologin: Conexión automática setting_date_format: Formato de la fecha label_user: Usuario @@ -183,7 +183,7 @@ label_project: Proyecto label_project_new: Nuevo proyecto label_project_plural: Proyectos label_project_all: Todos los proyectos -label_project_latest: Los proyectos más últimos +label_project_latest: Últimos proyectos label_issue: Petición label_issue_new: Nueva petición label_issue_plural: Peticiones @@ -201,7 +201,7 @@ label_member_plural: Miembros label_tracker: Tracker label_tracker_plural: Trackers label_tracker_new: Nuevo tracker -label_workflow: Workflow +label_workflow: Flujo de trabajo label_issue_status: Estado de petición label_issue_status_plural: Estados de las peticiones label_issue_status_new: Nuevo estado @@ -213,12 +213,12 @@ label_custom_field_plural: Campos personalizados label_custom_field_new: Nuevo campo personalizado label_enumerations: Listas de valores label_enumeration_new: Nuevo valor -label_information: Informacion -label_information_plural: Informaciones +label_information: Información +label_information_plural: Información label_please_login: Conexión label_register: Registrar label_password_lost: ¿Olvidaste la contraseña? -label_home: Principal +label_home: Inicio label_my_page: Mi página label_my_account: Mi cuenta label_my_projects: Mis proyectos @@ -290,7 +290,7 @@ label_next: Próximo label_previous: Anterior label_used_by: Utilizado por label_details: Detalles -label_add_note: Agregar una nota +label_add_note: Añadir una nota label_per_page: Por la página label_calendar: Calendario label_months_from: meses de @@ -307,7 +307,7 @@ label_comment_delete: Suprimir comentarios label_query: Pregunta personalizada label_query_plural: Preguntas personalizadas label_query_new: Nueva pregunta -label_filter_add: Agregar el filtro +label_filter_add: Añadir el filtro label_filter_plural: Filtros label_equals: igual label_not_equals: no igual @@ -321,7 +321,7 @@ label_ago: hace label_contains: contiene label_not_contains: no contiene label_day_plural: días -label_repository: Depósito SVN +label_repository: Repositorio label_browse: Hojear label_modification: %d modificación label_modification_plural: %d modificaciones @@ -356,7 +356,7 @@ label_current_version: Versión actual label_preview: Previo label_feed_plural: Feeds label_changes_details: Detalles de todos los cambios -label_issue_tracking: Petición tracking +label_issue_tracking: Peticiones label_spent_time: Tiempo dedicado label_f_hour: %.2f hora label_f_hour_plural: %.2f horas @@ -367,7 +367,7 @@ label_commits_per_month: Commits por mes label_commits_per_author: Commits por autor label_view_diff: Ver diferencias label_diff_inline: inline -label_diff_side_by_side: side by side +label_diff_side_by_side: cara a cara label_options: Opciones label_copy_workflow_from: Copiar workflow desde label_permissions_report: Informe de permisos @@ -387,7 +387,7 @@ label_end_to_start: fin a principio label_end_to_end: fin a fin label_start_to_start: principio a principio label_start_to_end: principio a fin -label_stay_logged_in: Stay logged in +label_stay_logged_in: Recordar conexión label_disabled: deshabilitado label_show_completed_versions: Muestra las versiones completas label_me: me @@ -405,7 +405,7 @@ label_month: Mes label_week: Semana label_date_from: Desde label_date_to: Hasta -label_language_based: Idioma basado +label_language_based: Badado en el idioma button_login: Conexión button_submit: Aceptar @@ -442,11 +442,11 @@ status_active: activo status_registered: registrado status_locked: bloqueado -text_select_mail_notifications: Seleccionar las actividades que necesitan la activación de la notificación por mail. +text_select_mail_notifications: Seleccionar los eventos a notificar text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 para ninguna restricción text_project_destroy_confirmation: ¿ Estás seguro de querer eliminar el proyecto ? -text_workflow_edit: Seleccionar un workflow para actualizar +text_workflow_edit: Seleccionar un flujo de trabajo para actualizar text_are_you_sure: ¿ Estás seguro ? text_journal_changed: cambiado de %s a %s text_journal_set_to: fijado a %s @@ -487,65 +487,65 @@ default_activity_development: Desarrollo enumeration_issue_priorities: Prioridad de las peticiones enumeration_doc_categories: Categorías del documento enumeration_activities: Actividades (tiempo dedicado) -label_index_by_date: Index by date -field_column_names: Columns -button_rename: Rename -text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? -label_feeds_access_key_created_on: RSS access key created %s ago -label_default_columns: Default columns -setting_cross_project_issue_relations: Allow cross-project issue relations +label_index_by_date: Índice por fecha +field_column_names: Columnas +button_rename: Renombrar +text_issue_category_destroy_question: Algunas peticiones (%d) están asignadas a esta categoría. ¿Qué desea hacer? +label_feeds_access_key_created_on: Clave de acceso por RSS creada hace %s +label_default_columns: Columnas por defecto +setting_cross_project_issue_relations: Permitir relacionar peticiones de distintos proyectos label_roadmap_overdue: %s late -label_module_plural: Modules +label_module_plural: Módulos label_this_week: this week label_index_by_title: Index by title -label_jump_to_a_project: Jump to a project... -field_assignable: Issues can be assigned to this role +label_jump_to_a_project: Ir al proyecto... +field_assignable: Se pueden asignar peticiones a este perfil label_sort_by: Sort by %s -setting_issue_list_default_columns: Default columns displayed on the issue list -text_issue_updated: Issue %s has been updated. -notice_feeds_access_key_reseted: Your RSS access key was reseted. -field_redirect_existing_links: Redirect existing links -text_issue_category_reassign_to: Reassign issues to this category -notice_email_sent: An email was sent to %s -text_issue_added: Issue %s has been reported. -field_comments: Comment -label_file_plural: Files -text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? -notice_email_error: An error occurred while sending mail (%s) -label_updated_time: Updated %s ago -text_issue_category_destroy_assignments: Remove category assignments -label_send_test_email: Send a test email +setting_issue_list_default_columns: Columnas por defecto para la lista de peticiones +text_issue_updated: La petición %s ha sido actualizada. +notice_feeds_access_key_reseted: Su clave de acceso para RSS ha sido reiniciada +field_redirect_existing_links: Redireccionar enlaces existentes +text_issue_category_reassign_to: Reasignar las peticiones a la categoría +notice_email_sent: Se ha enviado un correo a %s +text_issue_added: Petición añadida +field_comments: Comentario +label_file_plural: Archivos +text_wiki_destroy_confirmation: ¿Seguro que quiere borrar el wiki y todo su contenido? +notice_email_error: Ha ocurrido un error mientras enviando el correo (%s) +label_updated_time: Actualizado hace %s +text_issue_category_destroy_assignments: Dejar las peticiones sin categoría +label_send_test_email: Enviar un correo de prueba button_reset: Reset -label_added_time_by: Added by %s %s ago -field_estimated_hours: Estimated time +label_added_time_by: Añadido por %s hace %s +field_estimated_hours: Tiempo estimado label_changeset_plural: Changesets -setting_repositories_encodings: Repositories encodings -notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +setting_repositories_encodings: Codificaciones del repositorio +notice_no_issue_selected: "Ninguna petición seleccionada. Por favor, compruebe la petición que quiere modificar" label_bulk_edit_selected_issues: 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 into Redmine. -mail_body_account_information: Your Redmine 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 +notice_failed_to_save_issues: "Imposible salvar %s peticion(es) en %d seleccionado: %s." +label_theme: Tema +label_default: Por defecto +label_search_titles_only: Buscar sólo en títulos +label_nobody: nadie +button_change_password: Cambiar contraseña +text_user_mail_option: "En los proyectos no seleccionados, sólo recibirá notificaciones sobre elementos monitorizados o elementos en los que esté involucrado (por ejemplo, peticiones de las que usted sea autor o asignadas a usted)." +label_user_mail_option_selected: "Para cualquier evento del proyecto seleccionado..." +label_user_mail_option_all: "Para cualquier evento en todos mis proyectos" +label_user_mail_option_none: "Sólo para elementos monitorizados o relacionados conmigo" +setting_emails_footer: Pie de mensajes +label_float: Flotante +button_copy: Copiar +mail_body_account_information_external: Puede usar su cuenta "%s" para conectarse a Redmine. +mail_body_account_information: Información sobre su cuenta de Redmine +setting_protocol: Protocolo +text_caracters_minimum: Must be at least %d characters long. +field_time_zone: Time zone label_registration_activation_by_email: account activation by email +label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" mail_subject_account_activation_request: Redmine 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_time_format: Time format From c383486d711d12a2a9e716bedf3a444e140c00d6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 27 Nov 2007 17:04:13 +0000 Subject: [PATCH 021/710] Added custom fields marked as "For all projects" to the csv export of the cross project issue list. git-svn-id: http://redmine.rubyforge.org/svn/trunk@933 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/issues_helper.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index f9a88f6dd..4727fdf96 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -125,10 +125,10 @@ module IssuesHelper l(:field_created_on), l(:field_updated_on) ] - # only export custom fields if project is given - for custom_field in project.all_custom_fields - headers << custom_field.name - end if project + # Export project custom fields if project is given + # otherwise export custom fields marked as "For all projects" + custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields + custom_fields.each {|f| headers << f.name} csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } # csv lines issues.each do |issue| @@ -148,9 +148,7 @@ module IssuesHelper l_datetime(issue.created_on), l_datetime(issue.updated_on) ] - for custom_field in project.all_custom_fields - fields << (show_value issue.custom_value_for(custom_field)) - end if project + custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) } csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } end end From 233990dac389ad7b6924703a3752fcfb858e5060 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 27 Nov 2007 17:20:57 +0000 Subject: [PATCH 022/710] =?UTF-8?q?*=20Updated=20German=20translation=20(T?= =?UTF-8?q?homas=20L=C3=B6ber)=20*=20Updated=20Spanish=20translation=20(Gu?= =?UTF-8?q?mer=20Coronel=20P=C3=A9rez)=20*=20Fixed:=20test=20in=20method?= =?UTF-8?q?=20project=20in=20Attachment=20Model=20useless=20(Cyril=20Mouge?= =?UTF-8?q?l)=20*=20Fixed:=20public/.htaccess=20lacks=20support=20for=20mo?= =?UTF-8?q?d=5Ffcgid=20and=20adds=20handlers=20without=20checking=20for=20?= =?UTF-8?q?their=20existance=20(Nils=20Adermann)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@934 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/attachment.rb | 2 +- lang/de.yml | 100 +++++++++++++++++++-------------------- lang/es.yml | 88 +++++++++++++++++----------------- public/.htaccess | 93 +++++++++++++++++++++--------------- 4 files changed, 149 insertions(+), 134 deletions(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 7262498c4..927aa1735 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -84,7 +84,7 @@ class Attachment < ActiveRecord::Base end def project - container.is_a?(Project) ? container : container.project + container.project end def image? diff --git a/lang/de.yml b/lang/de.yml index ab3e47013..1fc6f6b69 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -6,16 +6,16 @@ actionview_datehelper_select_month_names_abbr: Jan,Feb,Mär,Apr,Mai,Jun,Jul,Aug, actionview_datehelper_select_month_prefix: actionview_datehelper_select_year_prefix: actionview_datehelper_time_in_words_day: 1 Tag -actionview_datehelper_time_in_words_day_plural: %d Tage -actionview_datehelper_time_in_words_hour_about: ungefähr eine Stunde +actionview_datehelper_time_in_words_day_plural: %d Tagen +actionview_datehelper_time_in_words_hour_about: ungefähr einer Stunde actionview_datehelper_time_in_words_hour_about_plural: ungefähr %d Stunden -actionview_datehelper_time_in_words_hour_about_single: ungefähr eine Stunde +actionview_datehelper_time_in_words_hour_about_single: ungefähr einer Stunde actionview_datehelper_time_in_words_minute: 1 Minute -actionview_datehelper_time_in_words_minute_half: halbe Minute -actionview_datehelper_time_in_words_minute_less_than: weniger als eine Minute +actionview_datehelper_time_in_words_minute_half: einer halben Minute +actionview_datehelper_time_in_words_minute_less_than: weniger als einer Minute actionview_datehelper_time_in_words_minute_plural: %d Minuten actionview_datehelper_time_in_words_minute_single: 1 Minute -actionview_datehelper_time_in_words_second_less_than: Weniger als eine Sekunde +actionview_datehelper_time_in_words_second_less_than: weniger als einer Sekunde actionview_datehelper_time_in_words_second_less_than_plural: weniger als %d Sekunden actionview_instancetag_blank_option: Bitte auswählen @@ -74,10 +74,10 @@ notice_email_sent: Eine E-Mail wurde an %s gesendet. notice_email_error: Beim Senden einer E-Mail ist ein Fehler aufgetreten (%s). notice_feeds_access_key_reseted: Ihr RSS-Zugriffsschlüssel wurde zurückgesetzt. -mail_subject_lost_password: Ihr Redmine Kennwort -mail_body_lost_password: 'Benutzen Sie folgenden Link, um das Password zu Ãndern:' +mail_subject_lost_password: Ihr Redmine-Kennwort +mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:' mail_subject_register: Redmine Kontoaktivierung -mail_body_register: 'Um Ihren Account zu aktivieren, benutzen Sie folgenden Link:' +mail_body_register: 'Um Ihr Konto zu aktivieren, benutzen Sie folgenden Link:' gui_validation_error: 1 Fehler gui_validation_error_plural: %d Fehler @@ -88,7 +88,7 @@ field_summary: Zusammenfassung field_is_required: Erforderlich field_firstname: Vorname field_lastname: Nachname -field_mail: Email +field_mail: E-Mail field_filename: Datei field_filesize: Größe field_downloads: Downloads @@ -146,7 +146,7 @@ field_onthefly: On-the-fly-Benutzererstellung field_start_date: Beginn field_done_ratio: %% erledigt field_auth_source: Authentifizierungs-Modus -field_hide_mail: Email-Adresse nicht anzeigen +field_hide_mail: E-Mail-Adresse nicht anzeigen field_comments: Kommentar field_url: URL field_start_page: Hauptseite @@ -421,7 +421,7 @@ label_feeds_access_key_created_on: RSS-Zugriffsschlüssel vor %s erstellt label_module_plural: Module label_added_time_by: Von %s vor %s hinzugefügt label_updated_time: Vor %s aktualisiert -label_jump_to_a_project: Jump to a project... +label_jump_to_a_project: Zu einem Projekt springen... button_login: Anmelden button_submit: OK @@ -470,8 +470,8 @@ text_journal_changed: geändert von %s zu %s text_journal_set_to: gestellt zu %s text_journal_deleted: gelöscht text_tip_task_begin_day: Aufgabe, die an diesem Tag beginnt -text_tip_task_end_day: Aufgabe, die an diesem Tag beendet -text_tip_task_begin_end_day: Aufgabe, die an diesem Tag beginnt und beendet +text_tip_task_end_day: Aufgabe, die an diesem Tag endet +text_tip_task_begin_end_day: Aufgabe, die an diesem Tag beginnt und endet text_project_identifier_info: 'Kleinbuchstaben (a-z), Ziffern und Bindestriche erlaubt.
    Einmal gespeichert, kann die Kennung nicht mehr geändert werden.' text_caracters_maximum: Max. %d Zeichen. text_length_between: Länge zwischen %d und %d Zeichen. @@ -482,9 +482,9 @@ text_issues_ref_in_commit_messages: Ticket-Beziehungen und -Status in Commit-Log text_issue_added: Ticket %s wurde erstellt. text_issue_updated: Ticket %s wurde aktualisiert. text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sämtlichem Inhalt löschen möchten? -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: Einige Tickets (%d) sind dieser Kategorie zugeodnet. Was möchten Sie tun? +text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen +text_issue_category_reassign_to: Tickets dieser Kategorie zuordnen default_role_manager: Manager default_role_developper: Developer @@ -511,38 +511,38 @@ default_activity_development: Development enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) -label_file_plural: Files +label_file_plural: Dateien 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 +field_column_names: Spalten +label_default_columns: Default-Spalten +setting_issue_list_default_columns: Default-Spalten in der Ticket-Auflistung +setting_repositories_encodings: Repository-Kodierung +notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten." +label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten +label_no_change_option: (Keine Änderung) +notice_failed_to_save_issues: "%d von %d ausgewählten Tickets konnte(n) nicht gespeichert werden: %s." +label_theme: Stil 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 into Redmine. -mail_body_account_information: Your Redmine 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: Redmine 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. +label_search_titles_only: Nur Titel durchsuchen +label_nobody: Niemand +button_change_password: Kennwort ändern +text_user_mail_option: "Für nicht ausgewählte Projekte werden Sie nur Benachrichtigungen für Dinge erhalten, die Sie beobachten oder an denen Sie beteiligt sind (z.B. Tickets, deren Autor Sie sind oder die Ihnen zugewiesen sind)." +label_user_mail_option_selected: "Für alle Ereignisse in den ausgewählten Projekten..." +label_user_mail_option_all: "Für alle Ereignisse in all meinen Projekten" +label_user_mail_option_none: "Nur für Dinge, die ich beobachte oder an denen ich beteiligt bin" +setting_emails_footer: E-Mail-Fußzeile +label_float: Fließkommazahl +button_copy: Kopieren +mail_body_account_information_external: Sie können sich mit Ihrem Konto "%s" an Redmine anmelden. +mail_body_account_information: Ihre Redmine Konto-Informationen +setting_protocol: Protokoll +label_user_mail_no_self_notified: "Ich möchte nicht über Änderungen benachrichtigt werden, die ich selbst durchführe." +setting_time_format: Zeitformat +label_registration_activation_by_email: Kontoaktivierung durch E-Mail +mail_subject_account_activation_request: Antrag auf Redmine Kontoaktivierung +mail_body_account_activation_request: 'Ein neuer Benutzer (%s) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:' +label_registration_automatic_activation: Automatische Kontoaktivierung +label_registration_manual_activation: Manuelle Kontoaktivierung +notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators." +field_time_zone: Zeitzone +text_caracters_minimum: Muss mindestens %d Zeichen lang sein. diff --git a/lang/es.yml b/lang/es.yml index ab4309ca1..b10bbd929 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -29,7 +29,7 @@ activerecord_error_blank: no puede estar en blanco activerecord_error_too_long: es demasiado largo activerecord_error_too_short: es demasiado corto activerecord_error_wrong_length: la longitud es incorrecta -activerecord_error_taken: has already been taken +activerecord_error_taken: ya está siendo usado activerecord_error_not_a_number: no es un número activerecord_error_not_a_date: no es una fecha válida activerecord_error_greater_than_start_date: debe ser la fecha mayor que del comienzo @@ -60,7 +60,7 @@ notice_account_wrong_password: Contraseña incorrecta. notice_account_register_done: Cuenta creada correctamente. notice_account_unknown_email: Usuario desconocido. notice_can_t_change_password: Esta cuenta utiliza una fuente de autenticación externa. No es posible cambiar la contraseña. -notice_account_lost_email_sent: Un correo con instrucciones para elegir una nueva contraseña le ha sido enviado. +notice_account_lost_email_sent: Se le ha enviado un correo con instrucciones para elegir una nueva contraseña. notice_account_activated: Su cuenta ha sido activada. Ahora se encuentra conectado. notice_successful_create: Creación correcta. notice_successful_update: Modificación correcta. @@ -226,7 +226,7 @@ label_administration: Administración label_login: Conexión label_logout: Desconexión label_help: Ayuda -label_reported_issues: Peticiones registradas +label_reported_issues: Peticiones registradas por mí label_assigned_to_me_issues: Peticiones que me están asignadas label_last_login: Última conexión label_last_updates: Actualizado @@ -257,7 +257,7 @@ label_change_status: Cambiar el estado label_history: Histórico label_attachment: Fichero label_attachment_new: Nuevo fichero -label_attachment_delete: Suprimir el fichero +label_attachment_delete: Borrar el fichero label_attachment_plural: Ficheros label_report: Informe label_report_plural: Informes @@ -303,10 +303,10 @@ label_comment: Comentario label_comment_plural: Comentarios label_comment_add: Añadir un comentario label_comment_added: Comentario añadido -label_comment_delete: Suprimir comentarios -label_query: Pregunta personalizada -label_query_plural: Preguntas personalizadas -label_query_new: Nueva pregunta +label_comment_delete: Borrar comentarios +label_query: Consulta personalizada +label_query_plural: Consultas personalizadas +label_query_new: Nueva consulta label_filter_add: Añadir el filtro label_filter_plural: Filtros label_equals: igual @@ -334,7 +334,7 @@ label_latest_revision: La revisión más actual label_latest_revision_plural: Las revisiones más actuales label_view_revisions: Ver las revisiones label_max_size: Tamaño máximo -label_on: en +label_on: de label_sort_highest: Primero label_sort_higher: Subir label_sort_lower: Bajar @@ -344,7 +344,7 @@ label_roadmap_due_in: Realizado en label_roadmap_no_issues: No hay peticiones para esta versión label_search: Búsqueda label_result: %d resultado -label_result_plural: %d resultados +label_result_plural: Resultados label_all_words: Todas las palabras label_wiki: Wiki label_wiki_edit: Wiki edicción @@ -353,7 +353,7 @@ label_wiki_page: Wiki página label_wiki_page_plural: Wiki páginas label_page_index: Índice label_current_version: Versión actual -label_preview: Previo +label_preview: Previsualizar label_feed_plural: Feeds label_changes_details: Detalles de todos los cambios label_issue_tracking: Peticiones @@ -391,10 +391,10 @@ label_stay_logged_in: Recordar conexión label_disabled: deshabilitado label_show_completed_versions: Muestra las versiones completas label_me: me -label_board: Forum -label_board_new: Nuevo forum -label_board_plural: Forums -label_topic_plural: Topics +label_board: Foro +label_board_new: Nuevo foro +label_board_plural: Foros +label_topic_plural: Temas label_message_plural: Mensajes label_message_last: Último mensaje label_message_new: Nuevo mensaje @@ -409,10 +409,10 @@ label_language_based: Badado en el idioma button_login: Conexión button_submit: Aceptar -button_save: Validar +button_save: Guardar button_check_all: Seleccionar todo button_uncheck_all: No seleccionar nada -button_delete: Suprimir +button_delete: Borrar button_create: Crear button_test: Testar button_edit: Modificar @@ -455,30 +455,30 @@ text_tip_task_begin_day: tarea que comienza este día text_tip_task_end_day: tarea que termina este día text_tip_task_begin_end_day: tarea que comienza y termina este día text_project_identifier_info: 'Letras minúsculas (a-z), números y signos de puntuación permitidos.
    Una vez guardado, el identificador no puede modificarse.' -text_caracters_maximum: %d caracteres máximo. -text_length_between: Longitud entre %d y %d caracteres. +text_caracters_maximum: %d carácteres como máximo. +text_length_between: Longitud entre %d y %d carácteres. text_tracker_no_workflow: No hay ningún workflow definido para este tracker -text_unallowed_characters: Caracteres no permitidos +text_unallowed_characters: Carácteres no permitidos text_comma_separated: Múltiples valores permitidos (separados por coma). text_issues_ref_in_commit_messages: Referencia y petición de corrección en los mensajes -default_role_manager: Manager +default_role_manager: Jefe de proyecto default_role_developper: Desarrollador default_role_reporter: Informador -default_tracker_bug: Anomalía -default_tracker_feature: Evolución -default_tracker_support: Asistencia -default_issue_status_new: Nuevo +default_tracker_bug: Errores +default_tracker_feature: Tareas +default_tracker_support: Soporte +default_issue_status_new: Nueva default_issue_status_assigned: Asignada default_issue_status_resolved: Resuelta -default_issue_status_feedback: Comentario +default_issue_status_feedback: Comentarios default_issue_status_closed: Cerrada default_issue_status_rejected: Rechazada -default_doc_category_user: Documentación del usuario +default_doc_category_user: Documentación de usuario default_doc_category_tech: Documentación tecnica -default_priority_low: Bajo +default_priority_low: Baja default_priority_normal: Normal -default_priority_high: Alto +default_priority_high: Alta default_priority_urgent: Urgente default_priority_immediate: Inmediata default_activity_design: Diseño @@ -494,13 +494,13 @@ text_issue_category_destroy_question: Algunas peticiones (%d) están asignadas a label_feeds_access_key_created_on: Clave de acceso por RSS creada hace %s label_default_columns: Columnas por defecto setting_cross_project_issue_relations: Permitir relacionar peticiones de distintos proyectos -label_roadmap_overdue: %s late +label_roadmap_overdue: %s tarde label_module_plural: Módulos -label_this_week: this week -label_index_by_title: Index by title +label_this_week: esta semana +label_index_by_title: Índice por título label_jump_to_a_project: Ir al proyecto... field_assignable: Se pueden asignar peticiones a este perfil -label_sort_by: Sort by %s +label_sort_by: Ordenar por %s setting_issue_list_default_columns: Columnas por defecto para la lista de peticiones text_issue_updated: La petición %s ha sido actualizada. notice_feeds_access_key_reseted: Su clave de acceso para RSS ha sido reiniciada @@ -518,7 +518,7 @@ label_send_test_email: Enviar un correo de prueba button_reset: Reset label_added_time_by: Añadido por %s hace %s field_estimated_hours: Tiempo estimado -label_changeset_plural: Changesets +label_changeset_plural: Cambios setting_repositories_encodings: Codificaciones del repositorio notice_no_issue_selected: "Ninguna petición seleccionada. Por favor, compruebe la petición que quiere modificar" label_bulk_edit_selected_issues: Bulk edit selected issues @@ -539,13 +539,13 @@ button_copy: Copiar mail_body_account_information_external: Puede usar su cuenta "%s" para conectarse a Redmine. mail_body_account_information: Información sobre su cuenta de Redmine setting_protocol: Protocolo -text_caracters_minimum: Must be at least %d characters long. -field_time_zone: Time zone -label_registration_activation_by_email: account activation by email -label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" -mail_subject_account_activation_request: Redmine 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." -setting_time_format: Time format +text_caracters_minimum: %d carácteres como mínimo +field_time_zone: Zona horaria +label_registration_activation_by_email: activación de cuenta por correo +label_user_mail_no_self_notified: "No quiero ser avisado de cambios hechos por mí" +mail_subject_account_activation_request: Petición de activación de cuenta Redmine +mail_body_account_activation_request: "Un nuevo usuario (%s) ha sido registrado. Esta cuenta está pendiende de aprobación" +label_registration_automatic_activation: activación automática de cuenta +label_registration_manual_activation: activación manual de cuenta +notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte de administrador" +setting_time_format: Formato de hora diff --git a/public/.htaccess b/public/.htaccess index d3c998345..3d3fb88bc 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -1,40 +1,55 @@ -# General Apache options -AddHandler fastcgi-script .fcgi -AddHandler cgi-script .cgi -Options +FollowSymLinks +ExecCGI - -# If you don't want Rails to look in certain directories, -# use the following rewrite rules so that Apache won't rewrite certain requests -# -# Example: -# RewriteCond %{REQUEST_URI} ^/notrails.* -# RewriteRule .* - [L] - -# Redirect all requests not available on the filesystem to Rails -# By default the cgi dispatcher is used which is very slow -# -# For better performance replace the dispatcher with the fastcgi one -# -# Example: -# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] -RewriteEngine On - -# If your Rails application is accessed via an Alias directive, -# then you MUST also set the RewriteBase in this htaccess file. -# -# Example: -# Alias /myrailsapp /path/to/myrailsapp/public -# RewriteBase /myrailsapp - -RewriteRule ^$ index.html [QSA] -RewriteRule ^([^.]+)$ $1.html [QSA] -RewriteCond %{REQUEST_FILENAME} !-f -RewriteRule ^(.*)$ dispatch.cgi [QSA,L] - -# In case Rails experiences terminal errors -# Instead of displaying this message you can supply a file here which will be rendered instead -# -# Example: -# ErrorDocument 500 /500.html - +# General Apache options + + AddHandler fastcgi-script .fcgi + + + AddHandler fcgid-script .fcgi + + + AddHandler cgi-script .cgi + +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f + + RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] + + + RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] + + + RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + ErrorDocument 500 "

    Application error

    Rails application failed to start properly" \ No newline at end of file From bf6e02c7394a468ac4a385c179e2eb7c3769eb52 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 Nov 2007 18:33:42 +0000 Subject: [PATCH 023/710] Search engine: search can be restricted to an exact phrase by using quotation marks (eg. hello "bye bye" can be used to search for "hello" and "bye bye" strings). git-svn-id: http://redmine.rubyforge.org/svn/trunk@935 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/search_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 7c50d4dcb..ee4f863aa 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -52,8 +52,11 @@ class SearchController < ApplicationController @object_types = @scope = %w(projects) end + # extract tokens from the question + # eg. hello "bye bye" => ["hello", "bye bye"] + @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} # tokens must be at least 3 character long - @tokens = @question.split.uniq.select {|w| w.length > 2 } + @tokens = @tokens.uniq.select {|w| w.length > 2 } if !@tokens.empty? # no more than 5 tokens to search for @@ -93,7 +96,6 @@ class SearchController < ApplicationController # if only one project is found, user is redirected to its overview redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 end - @question = @tokens.join(" ") else @question = "" end From 2cf11bd64e7b272171f72cac1572e26224daf4ca Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 Nov 2007 20:08:14 +0000 Subject: [PATCH 024/710] Fixed Mantis importer: projects trackers and modules assignment Fixed Trac and Mantis importers: roles assignments git-svn-id: http://redmine.rubyforge.org/svn/trunk@936 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_mantis.rake | 10 +++++++--- lib/tasks/migrate_from_trac.rake | 11 ++++++----- test/functional/search_controller_test.rb | 5 +++++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/tasks/migrate_from_mantis.rake b/lib/tasks/migrate_from_mantis.rake index fff668b64..593d59d82 100644 --- a/lib/tasks/migrate_from_mantis.rake +++ b/lib/tasks/migrate_from_mantis.rake @@ -53,9 +53,10 @@ task :migrate_from_mantis => :environment do TRACKER_BUG = Tracker.find_by_position(1) TRACKER_FEATURE = Tracker.find_by_position(2) - DEFAULT_ROLE = Role.find_by_position(3) - manager_role = Role.find_by_position(1) - developer_role = Role.find_by_position(2) + roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC') + manager_role = roles[0] + developer_role = roles[1] + DEFAULT_ROLE = roles.last ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer 25 => DEFAULT_ROLE, # reporter 40 => DEFAULT_ROLE, # updater @@ -268,6 +269,9 @@ task :migrate_from_mantis => :environment do p.identifier = project.identifier next unless p.save projects_map[project.id] = p.id + p.enabled_module_names = ['issue_tracking', 'news', 'wiki'] + p.trackers << TRACKER_BUG + p.trackers << TRACKER_FEATURE print '.' # Project members diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 6467a5430..429b39623 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -54,10 +54,11 @@ namespace :redmine do 'task' => TRACKER_FEATURE, 'patch' =>TRACKER_FEATURE } - - DEFAULT_ROLE = Role.find_by_position(3) - manager_role = Role.find_by_position(1) - developer_role = Role.find_by_position(2) + + roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC') + manager_role = roles[0] + developer_role = roles[1] + DEFAULT_ROLE = roles.last ROLE_MAPPING = {'admin' => manager_role, 'developer' => developer_role } @@ -173,7 +174,7 @@ namespace :redmine do elsif TracPermission.find_by_username_and_action(username, 'developer') role = ROLE_MAPPING['developer'] end - Member.create(:user => u, :project => @target_project, :role => DEFAULT_ROLE) + Member.create(:user => u, :project => @target_project, :role => role) u.reload end u diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index 4ed7931f6..5e3673a4e 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -46,4 +46,9 @@ class SearchControllerTest < Test::Unit::TestCase assert_response :success assert_template 'index' end + + def test_tokens_with_quotes + get :index, :id => 1, :q => '"good bye" hello "bye bye"' + assert_equal ["good bye", "hello", "bye bye"], assigns(:tokens) + end end From 3baf086e2d8e5d969ce12217e1e143e5b4ec971a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 Nov 2007 20:14:01 +0000 Subject: [PATCH 025/710] Fixed: 'View all issues' link doesn't work on issues/show. git-svn-id: http://redmine.rubyforge.org/svn/trunk@937 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_sidebar.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml index 6e61755e8..e6c63896b 100644 --- a/app/views/issues/_sidebar.rhtml +++ b/app/views/issues/_sidebar.rhtml @@ -4,7 +4,7 @@ <% end %>

    <%= l(:label_issue_plural) %>

    -<%= link_to l(:label_issue_view_all), { :set_filter => 1 } %>
    +<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %>
    <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %>
    <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %> From db002edabdec9050e6d512ae8759783894b9624a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 1 Dec 2007 17:15:42 +0000 Subject: [PATCH 026/710] * Added links to previous and next revisions on revision view (patch by Cyril Mougel slightly edited) * Fixed TimelogController#report december error * Fixed ProjectsControllerTest#test_activity 1st and 2nd day of the month failure git-svn-id: http://redmine.rubyforge.org/svn/trunk@938 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 14 ++++-- app/controllers/timelog_controller.rb | 2 +- app/models/changeset.rb | 10 ++++ app/views/repositories/revision.rhtml | 22 +++++++-- public/stylesheets/application.css | 2 + test/fixtures/repositories.yml | 7 +++ test/functional/projects_controller_test.rb | 2 +- .../repositories_controller_test.rb | 46 +++++++++++++++++++ test/unit/changeset_test.rb | 20 ++++++++ test/unit/repository_test.rb | 4 +- 10 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 test/functional/repositories_controller_test.rb diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 8ff464c5b..b332c7213 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -19,6 +19,9 @@ require 'SVG/Graph/Bar' require 'SVG/Graph/BarHorizontal' require 'digest/sha1' +class ChangesetNotFound < Exception +end + class RepositoriesController < ApplicationController layout 'base' before_filter :find_repository, :except => :edit @@ -94,14 +97,19 @@ class RepositoriesController < ApplicationController def revision @changeset = @repository.changesets.find_by_revision(@rev) - show_error and return unless @changeset + raise ChangesetNotFound unless @changeset @changes_count = @changeset.changes.size @changes_pages = Paginator.new self, @changes_count, 150, params['page'] @changes = @changeset.changes.find(:all, :limit => @changes_pages.items_per_page, :offset => @changes_pages.current.offset) - - render :action => "revision", :layout => false if request.xhr? + + respond_to do |format| + format.html + format.js {render :layout => false} + end + rescue ChangesetNotFound + show_error end def diff diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 1a1bace3a..f90c4527e 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -54,7 +54,7 @@ class TimelogController < ApplicationController begin; @date_to = params[:date_to].to_date; rescue; end end @date_from ||= Date.civil(Date.today.year, 1, 1) - @date_to ||= Date.civil(Date.today.year, Date.today.month+1, 1) - 1 + @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1 unless @criterias.empty? sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 355a5754c..1b79104c4 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -91,4 +91,14 @@ class Changeset < ActiveRecord::Base self.issues = referenced_issues.uniq end + + # Returns the previous changeset + def previous + @previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC') + end + + # Returns the next changeset + def next + @next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC') + end end diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 32f8583a7..64d1668bc 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -1,8 +1,22 @@
    -<% form_tag do %> -<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> -<%= submit_tag 'OK' %> -<% end %> + « + <% unless @changeset.previous.nil? -%> + <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %> + <% else -%> + <%= l(:label_previous) %> + <% end -%> +| + <% unless @changeset.next.nil? -%> + <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %> + <% else -%> + <%= l(:label_next) %> + <% end -%> + »  + + <% form_tag do %> + <%= text_field_tag 'rev', @rev, :size => 5 %> + <%= submit_tag 'OK' %> + <% end %>

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

    diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 1e3200135..acdb40cd0 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -111,6 +111,8 @@ div.square { } .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;} +.contextual input {font-size:0.9em;} + .splitcontentleft{float:left; width:49%;} .splitcontentright{float:right; width:49%;} form {display: inline;} diff --git a/test/fixtures/repositories.yml b/test/fixtures/repositories.yml index 6d288b192..46afed245 100644 --- a/test/fixtures/repositories.yml +++ b/test/fixtures/repositories.yml @@ -6,3 +6,10 @@ repositories_001: root_url: svn://localhost password: "" login: "" +repositories_002: + project_id: 2 + url: svn://localhost/test + id: 11 + root_url: svn://localhost + password: "" + login: "" diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index c41adaa6c..a48fa26bc 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -89,7 +89,7 @@ class ProjectsControllerTest < Test::Unit::TestCase end def test_activity - get :activity, :id => 1 + get :activity, :id => 1, :year => 2.days.ago.to_date.year, :month => 2.days.ago.to_date.month assert_response :success assert_template 'activity' assert_not_nil assigns(:events_by_day) diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb new file mode 100644 index 000000000..d5ccc660d --- /dev/null +++ b/test/functional/repositories_controller_test.rb @@ -0,0 +1,46 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'repositories_controller' + +# Re-raise errors caught by the controller. +class RepositoriesController; def rescue_action(e) raise e end; end + +class RepositoriesControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers + + def setup + @controller = RepositoriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_revision_with_before_nil_and_afer_normal + get :revision, {:id => 1, :rev => 1} + assert_response :success + assert_template 'revision' + assert_no_tag :tag => "div", :attributes => { :class => "contextual" }, + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/1?rev=0'} + } + assert_tag :tag => "div", :attributes => { :class => "contextual" }, + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/1?rev=2'} + } + end + +end diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb index ee53f18ff..2442a8b8c 100644 --- a/test/unit/changeset_test.rb +++ b/test/unit/changeset_test.rb @@ -39,4 +39,24 @@ class ChangesetTest < Test::Unit::TestCase assert fixed.closed? assert_equal 90, fixed.done_ratio end + + def test_previous + changeset = Changeset.find_by_revision(3) + assert_equal Changeset.find_by_revision(2), changeset.previous + end + + def test_previous_nil + changeset = Changeset.find_by_revision(1) + assert_nil changeset.previous + end + + def test_next + changeset = Changeset.find_by_revision(2) + assert_equal Changeset.find_by_revision(3), changeset.next + end + + def test_next_nil + changeset = Changeset.find_by_revision(4) + assert_nil changeset.next + end end diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb index 843b0b42c..5e0432c60 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -25,14 +25,14 @@ class RepositoryTest < Test::Unit::TestCase end def test_create - repository = Repository::Subversion.new(:project => Project.find(2)) + repository = Repository::Subversion.new(:project => Project.find(3)) assert !repository.save repository.url = "svn://localhost" assert repository.save repository.reload - project = Project.find(2) + project = Project.find(3) assert_equal repository, project.repository end From 81ada666bb54eeb26c207284e6369f371bb9147a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 1 Dec 2007 17:42:26 +0000 Subject: [PATCH 027/710] 'Assigned to' drop down list is now sorted by user's lastname. git-svn-id: http://redmine.rubyforge.org/svn/trunk@939 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 2 +- app/models/query.rb | 2 +- app/models/user.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index be46d6189..03ada035c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -132,7 +132,7 @@ class Project < ActiveRecord::Base # Users issues can be assigned to def assignable_users - members.select {|m| m.role.assignable?}.collect {|m| m.user} + members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort end # Returns the mail adresses of users that should be always notified on project events diff --git a/app/models/query.rb b/app/models/query.rb index 4cc5a63a5..4133abd88 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -147,7 +147,7 @@ class Query < ActiveRecord::Base user_values = [] user_values << ["<< #{l(:label_me)} >>", "me"] if executed_by if project - user_values += project.users.collect{|s| [s.name, s.id.to_s] } + user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } elsif executed_by # members of the user's projects user_values += executed_by.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } diff --git a/app/models/user.rb b/app/models/user.rb index 37512fda0..737a8cc8e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -163,7 +163,7 @@ class User < ActiveRecord::Base end def <=>(user) - lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname + user.nil? ? -1 : (lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname) end def to_s From f5d68cf688dabb41fc81b9658969dd994bffbc8d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 1 Dec 2007 21:33:15 +0000 Subject: [PATCH 028/710] Fixed a broken link in the SCM browser git-svn-id: http://redmine.rubyforge.org/svn/trunk@940 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/_navigation.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/repositories/_navigation.rhtml b/app/views/repositories/_navigation.rhtml index 823b4f44f..9f40e0abd 100644 --- a/app/views/repositories/_navigation.rhtml +++ b/app/views/repositories/_navigation.rhtml @@ -13,7 +13,7 @@ dirs.each do |dir| / <%= link_to h(dir), :action => 'browse', :id => @project, :path => link_path, :rev => @rev %> <% end %> <% if filename %> - / <%= link_to h(filename), :action => 'revisions', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %> + / <%= link_to h(filename), :action => 'changes', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %> <% end %> <%= "@ #{revision}" if revision %> From 3f2f7153a90272539e58016228db6c83b661feaf Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 1 Dec 2007 21:34:16 +0000 Subject: [PATCH 029/710] Fixed: Date and time formats defined in settings not applied to the issues CSV export. git-svn-id: http://redmine.rubyforge.org/svn/trunk@941 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/issues_helper.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 4727fdf96..197760b53 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -18,6 +18,7 @@ require 'csv' module IssuesHelper + include ApplicationHelper def render_issue_tooltip(issue) @cached_label_start_date ||= l(:field_start_date) @@ -142,11 +143,11 @@ module IssuesHelper issue.category, issue.fixed_version, issue.author.name, - issue.start_date ? l_date(issue.start_date) : nil, - issue.due_date ? l_date(issue.due_date) : nil, + format_date(issue.start_date), + format_date(issue.due_date), issue.done_ratio, - l_datetime(issue.created_on), - l_datetime(issue.updated_on) + format_time(issue.created_on), + format_time(issue.updated_on) ] custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) } csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } From 457c9a8e727dff3167065954ef7269f2a6edb296 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 1 Dec 2007 22:03:45 +0000 Subject: [PATCH 030/710] Fixed: svn or ldap password can be found in clear text in the html source in editing mode. git-svn-id: http://redmine.rubyforge.org/svn/trunk@942 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/repositories_helper.rb | 5 ++++- app/views/auth_sources/_form.rhtml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 41218fa79..333b30b1c 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -62,7 +62,10 @@ module RepositoriesHelper content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) + '
    (http://, https://, svn://, file:///)') + content_tag('p', form.text_field(:login, :size => 30)) + - content_tag('p', form.password_field(:password, :size => 30)) + content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore', + :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='repository[password]';", + :onchange => "this.name='repository[password]';")) end def darcs_field_tags(form, repository) diff --git a/app/views/auth_sources/_form.rhtml b/app/views/auth_sources/_form.rhtml index 24d2913e3..3d148c11f 100644 --- a/app/views/auth_sources/_form.rhtml +++ b/app/views/auth_sources/_form.rhtml @@ -15,7 +15,10 @@ <%= text_field 'auth_source', 'account' %>

    -<%= password_field 'auth_source', 'account_password' %>

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

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

    From aebcfb1eda843b90851e0facdc0a386bf06c5d29 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 2 Dec 2007 12:58:07 +0000 Subject: [PATCH 031/710] When creating a new role, permissions are pre-filled with 'Non member' role permissions. git-svn-id: http://redmine.rubyforge.org/svn/trunk@943 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/roles_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index a8f21ff65..3b5766aaf 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -33,7 +33,8 @@ class RolesController < ApplicationController end def new - @role = Role.new(params[:role]) + # Prefills the form with 'Non member' role permissions + @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions}) if request.post? && @role.save flash[:notice] = l(:notice_successful_create) redirect_to :action => 'list' From bc060b31ae086db5a6baf6e3a892c525b2b6f0cd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 2 Dec 2007 13:52:16 +0000 Subject: [PATCH 032/710] Email notifications are now sent as Blind carbon copy by default. This can be changed in email notifications settings (new setting added). Emission email address setting moved to the email notifications settings view. git-svn-id: http://redmine.rubyforge.org/svn/trunk@944 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/admin_controller.rb | 5 +++-- app/models/mailer.rb | 17 +++++++++++++---- app/views/admin/mail_options.rhtml | 13 +++++++++++-- app/views/settings/edit.rhtml | 7 +------ config/settings.yml | 2 ++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh.yml | 1 + public/stylesheets/application.css | 4 ++-- 25 files changed, 51 insertions(+), 16 deletions(-) diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index b448affcc..58d6115ee 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -48,8 +48,9 @@ class AdminController < ApplicationController def mail_options @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted) if request.post? - Setting.notified_events = (params[:notified_events] || []) - Setting.emails_footer = params[:emails_footer] if params[:emails_footer] + settings = (params[:settings] || {}).dup + settings[:notified_events] ||= [] + settings.each { |name, value| Setting[name] = value } flash[:notice] = l(:notice_successful_update) redirect_to :controller => 'admin', :action => 'mail_options' end diff --git a/app/models/mailer.rb b/app/models/mailer.rb index fe432e9a6..9639e1a9c 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -129,11 +129,20 @@ class Mailer < ActionMailer::Base default_url_options[:protocol] = Setting.protocol end - # Overrides the create_mail method to remove the current user from the recipients and cc - # if he doesn't want to receive notifications about what he does + # Overrides the create_mail method def create_mail - recipients.delete(User.current.mail) if recipients && User.current.pref[:no_self_notified] - cc.delete(User.current.mail) if cc && User.current.pref[:no_self_notified] + # Removes the current user from the recipients and cc + # if he doesn't want to receive notifications about what he does + if User.current.pref[:no_self_notified] + recipients.delete(User.current.mail) if recipients + cc.delete(User.current.mail) if cc + end + # Blind carbon copy recipients + if Setting.bcc_recipients? + bcc([recipients, cc].flatten.compact.uniq) + recipients [] + cc [] + end super end diff --git a/app/views/admin/mail_options.rhtml b/app/views/admin/mail_options.rhtml index 3c95ebd71..997cc3b22 100644 --- a/app/views/admin/mail_options.rhtml +++ b/app/views/admin/mail_options.rhtml @@ -6,16 +6,25 @@ <% form_tag({:action => 'mail_options'}, :id => 'mail-options-form') do %> +
    <%=l(:label_settings)%> +

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

    + +

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

    +
    +
    <%=l(:text_select_mail_notifications)%> <% @notifiables.each do |notifiable| %> -
    <%= l(:setting_emails_footer) %> -<%= text_area_tag 'emails_footer', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %> +<%= text_area_tag 'settings[emails_footer]', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %>
    <%= submit_tag l(:button_save) %> diff --git a/app/views/settings/edit.rhtml b/app/views/settings/edit.rhtml index 9b4cc2d57..4a0a400a3 100644 --- a/app/views/settings/edit.rhtml +++ b/app/views/settings/edit.rhtml @@ -1,8 +1,7 @@

    <%= l(:label_settings) %>

    -
    <% form_tag({:action => 'edit'}) do %> -
    +

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

    @@ -34,9 +33,6 @@

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

    -

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

    -

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

    @@ -101,5 +97,4 @@ <%= submit_tag l(:button_save) %> -
    <% end %> diff --git a/config/settings.yml b/config/settings.yml index e9b9eebfd..9f6671f0d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -39,6 +39,8 @@ issues_export_limit: default: 500 mail_from: default: redmine@somenet.foo +bcc_recipients: + default: 1 text_formatting: default: textile wiki_compression: diff --git a/lang/bg.yml b/lang/bg.yml index 5ec4577d8..6f5f527c7 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/cs.yml b/lang/cs.yml index 09394ef71..1bc80c9dc 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/de.yml b/lang/de.yml index 1fc6f6b69..b26129263 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -546,3 +546,4 @@ label_registration_manual_activation: Manuelle Kontoaktivierung notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators." field_time_zone: Zeitzone text_caracters_minimum: Muss mindestens %d Zeichen lang sein. +setting_bcc_recipients: Blind carbon copy recipients (bcc) diff --git a/lang/en.yml b/lang/en.yml index 201ba1601..fb99bbb82 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -180,6 +180,7 @@ setting_self_registration: Self-registration setting_attachment_max_size: Attachment max. size setting_issues_export_limit: Issues export limit setting_mail_from: Emission email address +setting_bcc_recipients: Blind carbon copy recipients (bcc) setting_host_name: Host name setting_text_formatting: Text formatting setting_wiki_compression: Wiki history compression diff --git a/lang/es.yml b/lang/es.yml index b10bbd929..1b5638eb5 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -549,3 +549,4 @@ label_registration_automatic_activation: activación automática de cuenta label_registration_manual_activation: activación manual de cuenta notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte de administrador" setting_time_format: Formato de hora +setting_bcc_recipients: Blind carbon copy recipients (bcc) diff --git a/lang/fr.yml b/lang/fr.yml index e319db81c..b2466f1e1 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -180,6 +180,7 @@ setting_self_registration: Inscription des nouveaux utilisateurs setting_attachment_max_size: Taille max des fichiers setting_issues_export_limit: Limite export demandes setting_mail_from: Adresse d'émission +setting_bcc_recipients: Destinataires en copie cachée (cci) setting_host_name: Nom d'hôte setting_text_formatting: Formatage du texte setting_wiki_compression: Compression historique wiki diff --git a/lang/he.yml b/lang/he.yml index 7b20240ab..0f75a6ec1 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/it.yml b/lang/it.yml index ceb3102aa..a3a858cef 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/ja.yml b/lang/ja.yml index a1a0d968e..2233c2a38 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -547,3 +547,4 @@ 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) diff --git a/lang/ko.yml b/lang/ko.yml index 9dd8951fe..8962d8cd5 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/nl.yml b/lang/nl.yml index 16686d756..ca6ad66f3 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -547,3 +547,4 @@ 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) diff --git a/lang/pl.yml b/lang/pl.yml index cf5494895..f478df8fb 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -546,3 +546,4 @@ label_registration_manual_activation: manualna aktywacja kont notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdzenie administratora." field_time_zone: Strefa czasowa text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 9e7bd9b4c..df893ff6d 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/pt.yml b/lang/pt.yml index 78d739b87..1a499a2c5 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/ro.yml b/lang/ro.yml index a81f61aec..a57330cc0 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -546,3 +546,4 @@ 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) diff --git a/lang/ru.yml b/lang/ru.yml index 30ab5dddc..2f73a43b9 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -546,3 +546,4 @@ label_registration_manual_activation: активировать аккаунты notice_account_pending: "Ваш аккаунт уже создан и ожидает подтверждения администратора." field_time_zone: Часовой пояс text_caracters_minimum: Must be at least %d characters long. +setting_bcc_recipients: Blind carbon copy recipients (bcc) diff --git a/lang/sr.yml b/lang/sr.yml index d08d0172a..49e2d5f3a 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -547,3 +547,4 @@ 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) diff --git a/lang/sv.yml b/lang/sv.yml index 1b00eee1f..11a8ce059 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -547,3 +547,4 @@ 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) diff --git a/lang/zh.yml b/lang/zh.yml index b20dd1b75..48176ceba 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -549,3 +549,4 @@ 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) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index acdb40cd0..1059d960b 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -153,8 +153,8 @@ width: 200px; #preview fieldset {margin-top: 1em; background: url(../images/draft.png)} -#settings .tabular p{ padding-left: 300px; } -#settings .tabular label{ margin-left: -300px; width: 295px; } +.tabular.settings p{ padding-left: 300px; } +.tabular.settings label{ margin-left: -300px; width: 295px; } .required {color: #bb0000;} .summary {font-style: italic;} From 6e74a068082faea3cfbbbeb5b44e4798722eb3c0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 2 Dec 2007 14:20:28 +0000 Subject: [PATCH 033/710] Fixed: error on admin/info if there's more than 1 plugin installed. git-svn-id: http://redmine.rubyforge.org/svn/trunk@945 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/admin/info.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/info.rhtml b/app/views/admin/info.rhtml index d84d2ad32..179fda1a8 100644 --- a/app/views/admin/info.rhtml +++ b/app/views/admin/info.rhtml @@ -12,7 +12,7 @@  

    Plugins

    <%= l(:field_subject) %> <%= l(:field_author) %>
    <%= link_to h(topic.subject), :controller => 'messages', :action => 'show', :board_id => @board, :id => topic %><%= link_to_user topic.author %><%= format_time(topic.created_on) %><%= topic.replies_count %> - +
    <%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %><%= topic.author %><%= format_time(topic.created_on) %><%= topic.replies_count %> <% if topic.last_reply %> - <%= topic.last_reply.author.name %>, <%= format_time(topic.last_reply.created_on) %>
    + <%= authoring topic.last_reply.created_on, topic.last_reply.author %>
    <%= link_to_message topic.last_reply %> <% end %> -
    <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>

    <%= issue.project.name %> - <%= issue.tracker.name %>
    - <%= issue.status.name %> - <%= format_time(issue.updated_on) %>

    -

    <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %>

    +
    <%= issue.project.name %> - <%= issue.tracker.name %>
    + <%= issue.status.name %> - <%= format_time(issue.updated_on) %>
    + <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %>
    - <% @plugins.keys.sort.each do |plugin| %> + <% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %> From e4724c7626a329cee82875a7e1bbec450eab4667 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 2 Dec 2007 15:09:43 +0000 Subject: [PATCH 034/710] Trac importer: * should now support mysql and postgresql Trac database * minor fixes git-svn-id: http://redmine.rubyforge.org/svn/trunk@946 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 88 +++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 429b39623..37514df01 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -24,7 +24,7 @@ namespace :redmine do task :migrate_from_trac => :environment do module TracMigrate - TICKET_MAP = []; + TICKET_MAP = [] DEFAULT_STATUS = IssueStatus.default assigned_status = IssueStatus.find_by_position(2) @@ -187,7 +187,7 @@ namespace :redmine do # External Links text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"} # Internal Links - text = text.gsub(/[[BR]]/, "\n") # This has to go before the rules below + text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"} @@ -197,7 +197,7 @@ namespace :redmine do # Ticket number re-writing text = text.gsub(/#(\d+)/) do |s| TICKET_MAP[$1.to_i] ||= $1 - "\##{TICKET_MAP[$1.to_i]}" + "\##{TICKET_MAP[$1.to_i] || $1}" end # Preformatted blocks text = text.gsub(/\{\{\{/, '
    ')
    @@ -213,14 +213,12 @@ namespace :redmine do
             text = text.gsub(/,,/, '~')        
             # Lists
             text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
    -        
     
             text
           end
         
           def self.migrate
    -        establish_connection({:adapter => trac_adapter, 
    -                              :database => trac_db_path})
    +        establish_connection
     
             # Quick database test
             TracComponent.count
    @@ -418,26 +416,55 @@ namespace :redmine do
           end
           
           def self.set_trac_directory(path)
    -        @trac_directory = path
    +        @@trac_directory = path
             raise "This directory doesn't exist!" unless File.directory?(path)
    -        raise "#{trac_db_path} doesn't exist!" unless File.exist?(trac_db_path)
             raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
    -        @trac_directory
    +        @@trac_directory
           rescue Exception => e
             puts e
             return false
           end
     
           def self.trac_directory
    -        @trac_directory
    +        @@trac_directory
           end
     
           def self.set_trac_adapter(adapter)
    -        return false unless %w(sqlite sqlite3).include?(adapter)
    -        @trac_adapter = adapter
    +        return false if adapter.blank?
    +        raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
    +        # If adapter is sqlite or sqlite3, make sure that trac.db exists
    +        raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
    +        @@trac_adapter = adapter
    +      rescue Exception => e
    +        puts e
    +        return false
           end
           
    -      def self.trac_adapter; @trac_adapter end
    +      def self.set_trac_db_host(host)
    +        return nil if host.blank?
    +        @@trac_db_host = host
    +      end
    +
    +      def self.set_trac_db_port(port)
    +        return nil if port.to_i == 0
    +        @@trac_db_port = port.to_i
    +      end
    +      
    +      def self.set_trac_db_name(name)
    +        return nil if name.blank?
    +        @@trac_db_name = name
    +      end
    +
    +      def self.set_trac_db_username(username)
    +        @@trac_db_username = username
    +      end
    +      
    +      def self.set_trac_db_password(password)
    +        @@trac_db_password = password
    +      end
    +      
    +      mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_username, :trac_db_password
    +      
           def self.trac_db_path; "#{trac_directory}/db/trac.db" end
           def self.trac_attachments_directory; "#{trac_directory}/attachments" end
           
    @@ -451,17 +478,31 @@ namespace :redmine do
               puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
               # enable issues and wiki for the created project
               project.enabled_module_names = ['issue_tracking', 'wiki']
    -          project.trackers << TRACKER_BUG
    -          project.trackers << TRACKER_FEATURE          
             end        
    +        project.trackers << TRACKER_BUG
    +        project.trackers << TRACKER_FEATURE          
             @target_project = project.new_record? ? nil : project
           end
           
    -      def self.establish_connection(params)
    +      def self.connection_params
    +        if %w(sqlite sqlite3).include?(trac_adapter)
    +          {:adapter => trac_adapter, 
    +           :database => trac_db_path}
    +        else
    +          {:adapter => trac_adapter,
    +           :database => trac_db_name,
    +           :host => trac_db_host,
    +           :port => trac_db_port,
    +           :username => trac_db_username,
    +           :password => trac_db_password}
    +        end
    +      end
    +      
    +      def self.establish_connection
             constants.each do |const|
               klass = const_get(const)
               next unless klass.respond_to? 'establish_connection'
    -          klass.establish_connection params
    +          klass.establish_connection connection_params
             end
           end
           
    @@ -474,7 +515,7 @@ namespace :redmine do
         end
         
         puts
    -    puts "WARNING: Your Redmine install will have a new project added during this process."
    +    puts "WARNING: a new project will be added to Redmine during this process."
         print "Are you sure you want to continue ? [y/N] "
         break unless STDIN.gets.match(/^y$/i)  
         puts
    @@ -489,8 +530,17 @@ namespace :redmine do
           end
         end
         
    +    DEFAULT_PORTS = {'mysql' => 3306, 'postgresl' => 5432}
    +    
         prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory}
    -    prompt('Trac database adapter (sqlite, sqlite3)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
    +    prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
    +    unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
    +      prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
    +      prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
    +      prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
    +      prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
    +      prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
    +    end
         prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
         prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
         puts
    
    From 8c65cc47122b802150bf3591cb4eba4f8c4fb4b4 Mon Sep 17 00:00:00 2001
    From: Jean-Philippe Lang 
    Date: Sun, 2 Dec 2007 20:58:02 +0000
    Subject: [PATCH 035/710] Added Annotate/Blame view for Subversion, CVS and
     Mercurial repositories.
    
    git-svn-id: http://redmine.rubyforge.org/svn/trunk@947 e93f8b46-1217-0410-a6f0-8f06a7374b81
    ---
     app/controllers/repositories_controller.rb    |  5 ++
     app/models/repository.rb                      |  4 ++
     app/views/repositories/annotate.rhtml         | 26 +++++++++++
     app/views/repositories/changes.rhtml          | 11 +++--
     app/views/repositories/entry.rhtml            |  5 --
     config/routes.rb                              |  1 +
     lang/bg.yml                                   |  1 +
     lang/cs.yml                                   |  1 +
     lang/de.yml                                   |  1 +
     lang/en.yml                                   |  1 +
     lang/es.yml                                   |  1 +
     lang/fr.yml                                   |  1 +
     lang/he.yml                                   |  1 +
     lang/it.yml                                   |  1 +
     lang/ja.yml                                   |  1 +
     lang/ko.yml                                   |  1 +
     lang/nl.yml                                   |  1 +
     lang/pl.yml                                   |  1 +
     lang/pt-br.yml                                |  1 +
     lang/pt.yml                                   |  1 +
     lang/ro.yml                                   |  1 +
     lang/ru.yml                                   |  1 +
     lang/sr.yml                                   |  1 +
     lang/sv.yml                                   |  1 +
     lang/zh.yml                                   |  1 +
     lib/redmine.rb                                |  2 +-
     lib/redmine/scm/adapters/abstract_adapter.rb  | 32 +++++++++++--
     lib/redmine/scm/adapters/cvs_adapter.rb       | 20 +++++++-
     lib/redmine/scm/adapters/mercurial_adapter.rb | 19 ++++++++
     .../scm/adapters/subversion_adapter.rb        | 17 +++++++
     public/stylesheets/scm.css                    | 46 ++++++++++++++++---
     31 files changed, 186 insertions(+), 21 deletions(-)
     create mode 100644 app/views/repositories/annotate.rhtml
    
    diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
    index b332c7213..dfd5d0a6f 100644
    --- a/app/controllers/repositories_controller.rb
    +++ b/app/controllers/repositories_controller.rb
    @@ -95,6 +95,11 @@ class RepositoriesController < ApplicationController
         end
       end
       
    +  def annotate
    +    @annotate = @repository.scm.annotate(@path, @rev)
    +    show_error and return if @annotate.nil? || @annotate.empty?
    +  end
    +  
       def revision
         @changeset = @repository.changesets.find_by_revision(@rev)
         raise ChangesetNotFound unless @changeset
    diff --git a/app/models/repository.rb b/app/models/repository.rb
    index 35dd6803f..be31ac2e5 100644
    --- a/app/models/repository.rb
    +++ b/app/models/repository.rb
    @@ -33,6 +33,10 @@ class Repository < ActiveRecord::Base
       def supports_cat?
         scm.supports_cat?
       end
    +
    +  def supports_annotate?
    +    scm.supports_annotate?
    +  end
       
       def entries(path=nil, identifier=nil)
         scm.entries(path, identifier)
    diff --git a/app/views/repositories/annotate.rhtml b/app/views/repositories/annotate.rhtml
    new file mode 100644
    index 000000000..b8f481ae5
    --- /dev/null
    +++ b/app/views/repositories/annotate.rhtml
    @@ -0,0 +1,26 @@
    +

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

    + +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + +
    +
    <%=h @plugins[plugin].name %> <%=h @plugins[plugin].description %>
    + + <% line_num = 1 %> + <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %> + <% revision = @annotate.revisions[line_num-1] %> + + + + + + + <% line_num += 1 %> + <% end %> + +
    <%= line_num %> + <%= (revision.identifier ? link_to(revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier) : revision.revision) if revision %><%= h(revision.author) if revision %>
    <%= line %>
    + + +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/repositories/changes.rhtml b/app/views/repositories/changes.rhtml index 5d1db96ba..f843983db 100644 --- a/app/views/repositories/changes.rhtml +++ b/app/views/repositories/changes.rhtml @@ -2,14 +2,17 @@

    <%=h @entry.name %>

    -<% if @repository.supports_cat? %>

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

    -<% end %> <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%> diff --git a/app/views/repositories/entry.rhtml b/app/views/repositories/entry.rhtml index 94db240ab..9927601d7 100644 --- a/app/views/repositories/entry.rhtml +++ b/app/views/repositories/entry.rhtml @@ -2,11 +2,6 @@
    - - - - - <% line_num = 1 %> <% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %> diff --git a/config/routes.rb b/config/routes.rb index c3637304a..5048da2f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ ActionController::Routing::Routes.draw do |map| omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes' omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff' omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry' + omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate' end # Allow downloading Web Service WSDL as a file with an extension diff --git a/lang/bg.yml b/lang/bg.yml index 6f5f527c7..5f2dc6a8d 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/cs.yml b/lang/cs.yml index 1bc80c9dc..49697dda8 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/de.yml b/lang/de.yml index b26129263..870f45014 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -547,3 +547,4 @@ notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Geneh field_time_zone: Zeitzone text_caracters_minimum: Muss mindestens %d Zeichen lang sein. setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate diff --git a/lang/en.yml b/lang/en.yml index fb99bbb82..cdecb6693 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -489,6 +489,7 @@ button_reset: Reset button_rename: Rename button_change_password: Change password button_copy: Copy +button_annotate: Annotate status_active: active status_registered: registered diff --git a/lang/es.yml b/lang/es.yml index 1b5638eb5..5f25de7cf 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -550,3 +550,4 @@ label_registration_manual_activation: activación manual de cuenta notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte de administrador" setting_time_format: Formato de hora setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate diff --git a/lang/fr.yml b/lang/fr.yml index b2466f1e1..63f367f06 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -489,6 +489,7 @@ button_reset: Réinitialiser button_rename: Renommer button_change_password: Changer de mot de passe button_copy: Copier +button_annotate: Annoter status_active: actif status_registered: enregistré diff --git a/lang/he.yml b/lang/he.yml index 0f75a6ec1..4ebbecf7a 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/it.yml b/lang/it.yml index a3a858cef..fa13513cb 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/ja.yml b/lang/ja.yml index 2233c2a38..d2f7d579f 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/ko.yml b/lang/ko.yml index 8962d8cd5..dd50ce2bc 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/nl.yml b/lang/nl.yml index ca6ad66f3..895feb5ed 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/pl.yml b/lang/pl.yml index f478df8fb..381dc5e26 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -547,3 +547,4 @@ notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdze field_time_zone: Strefa czasowa text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate diff --git a/lang/pt-br.yml b/lang/pt-br.yml index df893ff6d..4c4d862d3 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/pt.yml b/lang/pt.yml index 1a499a2c5..1aabc8ac7 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/ro.yml b/lang/ro.yml index a57330cc0..0c10c7727 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/ru.yml b/lang/ru.yml index 2f73a43b9..c6e27ca9f 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -547,3 +547,4 @@ notice_account_pending: "Ваш аккаунт уже создан и ожида field_time_zone: Часовой пояс text_caracters_minimum: Must be at least %d characters long. setting_bcc_recipients: Blind carbon copy recipients (bcc) +button_annotate: Annotate diff --git a/lang/sr.yml b/lang/sr.yml index 49e2d5f3a..42aa3cc3e 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/sv.yml b/lang/sv.yml index 11a8ce059..7cbcc7a5f 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lang/zh.yml b/lang/zh.yml index 48176ceba..ad18cecc3 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -550,3 +550,4 @@ notice_account_pending: "Your account was created and is now pending administrat 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 diff --git a/lib/redmine.rb b/lib/redmine.rb index ffc1cc27e..b74f00aec 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -76,7 +76,7 @@ Redmine::AccessControl.map do |map| map.project_module :repository do |map| map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member - map.permission :browse_repository, :repositories => [:show, :browse, :entry, :changes, :diff, :stats, :graph] + map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph] map.permission :view_changesets, :repositories => [:show, :revisions, :revision] end diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 4b524c538..720a4e9d9 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -38,6 +38,10 @@ module Redmine def supports_cat? true end + + def supports_annotate? + respond_to?('annotate') + end def root_url @root_url @@ -76,7 +80,7 @@ module Redmine def cat(path, identifier=nil) return nil end - + def with_leading_slash(path) path ||= '' (path[0,1]!="/") ? "/#{path}" : path @@ -237,7 +241,7 @@ module Redmine # 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") + def initialize(type="inline") @parsing = false @nb_line = 1 @start = false @@ -312,7 +316,7 @@ module Redmine CGI.escapeHTML(line) end - def parse_line (line, type="inline") + def parse_line(line, type="inline") if line[0, 1] == "+" diff = sbs? type, 'add' @before = 'add' @@ -348,6 +352,28 @@ module Redmine end end end + + class Annotate + attr_reader :lines, :revisions + + def initialize + @lines = [] + @revisions = [] + end + + def add_line(line, revision) + @lines << line + @revisions << revision + end + + def content + content = lines.join("\n") + end + + def empty? + lines.empty? + end + end end end end diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index e84c1eea3..5c6c1775b 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -268,7 +268,25 @@ module Redmine rescue Errno::ENOENT => e raise CommandFailed end - + + def annotate(path, identifier=nil) + identifier = (identifier) ? identifier : "HEAD" + logger.debug " annotate path:'#{path}',identifier #{identifier}" + path_with_project="#{url}#{with_leading_slash(path)}" + cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}" + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$} + blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip)) + end + end + return nil if $? && $?.exitstatus != 0 + blame + rescue Errno::ENOENT => e + raise CommandFailed + end + private # convert a date/time into the CVS-format diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index a631916c5..3cbf01f91 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -157,6 +157,25 @@ module Redmine rescue Errno::ENOENT => e raise CommandFailed end + + def annotate(path, identifier=nil) + path ||= '' + cmd = "#{HG_BIN} -R #{target('')}" + cmd << " annotate -n -u" + cmd << " -r #{identifier.to_i}" if identifier + cmd << " #{target(path)}" + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^([^:]+)\s(\d+):(.*)$} + blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip)) + end + end + return nil if $? && $?.exitstatus != 0 + blame + rescue Errno::ENOENT => e + raise CommandFailed + end end end end diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index 9e8acce4c..d55b8712e 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -173,6 +173,23 @@ module Redmine raise CommandFailed end + def annotate(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}" + cmd << credentials_string + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} + blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip)) + end + end + return nil if $? && $?.exitstatus != 0 + blame + rescue Errno::ENOENT => e + raise CommandFailed + end + private def credentials_string diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css index 3794db366..c3dc307d6 100644 --- a/public/stylesheets/scm.css +++ b/public/stylesheets/scm.css @@ -2,20 +2,52 @@ 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; } -div.action_M { background: #fd8 } -div.action_D { background: #f88 } -div.action_A { background: #bfb } - table.filecontent tr.spacing { border: 1px solid #d7d7d7; } - -table.filecontent .line-num { +table.filecontent th.line-num { border: 1px solid #d7d7d7; font-size: 0.8em; text-align: right; - width: 3em; + width: 2%; padding-right: 3px; } +/* 12 different colors for the annonate view */ +table.annotate tr.bloc-0 {background: #FFFFBF;} +table.annotate tr.bloc-1 {background: #EABFFF;} +table.annotate tr.bloc-2 {background: #BFFFFF;} +table.annotate tr.bloc-3 {background: #FFD9BF;} +table.annotate tr.bloc-4 {background: #E6FFBF;} +table.annotate tr.bloc-5 {background: #BFCFFF;} +table.annotate tr.bloc-6 {background: #FFBFEF;} +table.annotate tr.bloc-7 {background: #FFE6BF;} +table.annotate tr.bloc-8 {background: #FFE680;} +table.annotate tr.bloc-9 {background: #AA80FF;} +table.annotate tr.bloc-10 {background: #FFBFDC;} +table.annotate tr.bloc-11 {background: #BFE4FF;} + +table.annotate td.revision { + text-align: center; + width: 2%; + padding-left: 1em; + background: inherit; +} + +table.annotate td.author { + text-align: center; + border-right: 1px solid #d7d7d7; + white-space: nowrap; + padding-left: 1em; + padding-right: 1em; + width: 3%; + background: inherit; +} + +table.annotate td.line-code { background-color: #fafafa; } + +div.action_M { background: #fd8 } +div.action_D { background: #f88 } +div.action_A { background: #bfb } + /************* Coderay styles *************/ table.CodeRay { From 7d8af70a6362517cab9aa2dc2054fa8dd8581ad8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 2 Dec 2007 22:07:56 +0000 Subject: [PATCH 036/710] Changed the maximum length of LDAP account to 255 characters. Added length validations on AuthSource model. git-svn-id: http://redmine.rubyforge.org/svn/trunk@948 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/auth_source.rb | 3 +++ db/migrate/084_change_auth_sources_account_limit.rb | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 db/migrate/084_change_auth_sources_account_limit.rb diff --git a/app/models/auth_source.rb b/app/models/auth_source.rb index 47eec106d..2f651ade5 100644 --- a/app/models/auth_source.rb +++ b/app/models/auth_source.rb @@ -20,6 +20,9 @@ class AuthSource < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name + validates_length_of :name, :host, :account_password, :maximum => 60 + validates_length_of :account, :base_dn, :maximum => 255 + validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30 def authenticate(login, password) end diff --git a/db/migrate/084_change_auth_sources_account_limit.rb b/db/migrate/084_change_auth_sources_account_limit.rb new file mode 100644 index 000000000..6a933b894 --- /dev/null +++ b/db/migrate/084_change_auth_sources_account_limit.rb @@ -0,0 +1,9 @@ +class ChangeAuthSourcesAccountLimit < ActiveRecord::Migration + def self.up + change_column :auth_sources, :account, :string + end + + def self.down + change_column :auth_sources, :account, :string, :limit => 60 + end +end From 92a23c05bb71e62426cec2298d90b39fadd0eb52 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 3 Dec 2007 10:28:08 +0000 Subject: [PATCH 037/710] Project name format limitation removed (name can now contain any character). Project identifier maximum length changed from 12 to 20. git-svn-id: http://redmine.rubyforge.org/svn/trunk@949 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 8 ++++++-- app/helpers/queries_helper.rb | 2 +- app/models/project.rb | 3 +-- app/views/admin/projects.rhtml | 2 +- app/views/issues/_list_simple.rhtml | 2 +- app/views/layouts/_project_selector.rhtml | 4 ++-- app/views/my/account.rhtml | 2 +- app/views/projects/_form.rhtml | 2 +- app/views/projects/destroy.rhtml | 2 +- app/views/projects/gantt.rhtml | 4 ++-- app/views/projects/list.rhtml | 4 ++-- app/views/projects/show.rhtml | 4 ++-- lib/tasks/migrate_from_mantis.rake | 4 ++-- public/themes/alternate/stylesheets/application.css | 1 + test/functional/projects_controller_test.rb | 9 ++++++++- 15 files changed, 32 insertions(+), 21 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 37a788690..658e9d232 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -58,7 +58,9 @@ class ProjectsController < ApplicationController def add @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @trackers = Tracker.all - @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}") + @root_projects = Project.find(:all, + :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", + :order => 'name') @project = Project.new(params[:project]) @project.enabled_module_names = Redmine::AccessControl.available_project_modules if request.get? @@ -90,7 +92,9 @@ class ProjectsController < ApplicationController end def settings - @root_projects = Project::find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id]) + @root_projects = Project.find(:all, + :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], + :order => 'name') @custom_fields = IssueCustomField.find(:all) @issue_category ||= IssueCategory.new @member ||= @project.members.new diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index f92787278..3011d3aec 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -38,7 +38,7 @@ module QueriesHelper else case column.name when :subject - ((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + + h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) when :done_ratio progress_bar(value, :width => '80px') diff --git a/app/models/project.rb b/app/models/project.rb index 03ada035c..5788732d7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -57,10 +57,9 @@ class Project < ActiveRecord::Base validates_associated :custom_values, :on => :update validates_associated :repository, :wiki validates_length_of :name, :maximum => 30 - validates_format_of :name, :with => /^[\w\s\'\-]*$/i validates_length_of :description, :maximum => 255 validates_length_of :homepage, :maximum => 60 - validates_length_of :identifier, :in => 3..12 + validates_length_of :identifier, :in => 3..20 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ def identifier=(identifier) diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index d231be102..423d56ebe 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -26,7 +26,7 @@ <% for project in @projects %> "> - - <% for version in @project.versions.sort %> - + diff --git a/app/views/versions/_issue_counts.rhtml b/app/views/versions/_issue_counts.rhtml new file mode 100644 index 000000000..4bab5c659 --- /dev/null +++ b/app/views/versions/_issue_counts.rhtml @@ -0,0 +1,35 @@ + +
    + +<%= l(:label_issues_by, + select_tag('status_by', + status_by_options_for_select(criteria), + :id => 'status_by_select', + :onchange => remote_function(:url => { :action => :status_by, :id => version }, + :with => "Form.serialize('status_by_form')"))) %> + +<% if counts.empty? %> +

    <%= l(:label_no_data) %>

    +<% else %> +
    <%= @path %>
    <%= project.active? ? link_to(project.name, :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> + <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> <%= textilizable project.description, :project => project %> <%= image_tag 'true.png' if project.is_public? %> <%= project.children.size %> diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml index 517055e3a..eb93f8ea1 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -11,7 +11,7 @@ <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> <%= issue.project.name %> - <%= issue.tracker.name %>
    +
    <%=h issue.project.name %> - <%= issue.tracker.name %>
    <%= issue.status.name %> - <%= format_time(issue.updated_on) %>
    <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %> diff --git a/app/views/layouts/_project_selector.rhtml b/app/views/layouts/_project_selector.rhtml index 499879c8c..ce2f15e03 100644 --- a/app/views/layouts/_project_selector.rhtml +++ b/app/views/layouts/_project_selector.rhtml @@ -3,10 +3,10 @@ <% user_projects_by_root.keys.sort.each do |root| %> - <%= content_tag('option', root.name, :value => url_for(:controller => 'projects', :action => 'show', :id => root)) %> + <%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root)) %> <% user_projects_by_root[root].sort.each do |project| %> <% next if project == root %> - <%= content_tag('option', ('» ' + project.name), :value => url_for(:controller => 'projects', :action => 'show', :id => project)) %> + <%= content_tag('option', ('» ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project)) %> <% end %> <% end %> diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index e64051771..2dda62d70 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -29,7 +29,7 @@ :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %> <% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %>

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

    <%= l(:text_user_mail_option) %>

    <% end %> diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index 885ccf4bd..e29777af4 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -9,7 +9,7 @@ <% end %>

    <%= f.text_area :description, :required => true, :cols => 60, :rows => 5 %><%= l(:text_caracters_maximum, 255) %>

    -

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

    +

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

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

    <%= f.check_box :is_public %>

    <%= wikitoolbar_for 'project_description' %> diff --git a/app/views/projects/destroy.rhtml b/app/views/projects/destroy.rhtml index 8ef23197d..4531cb845 100644 --- a/app/views/projects/destroy.rhtml +++ b/app/views/projects/destroy.rhtml @@ -1,7 +1,7 @@

    <%=l(:label_confirmation)%>

    -

    <%= @project_to_destroy.name %>
    +

    <%=h @project_to_destroy.name %>
    <%=l(:text_project_destroy_confirmation)%>

    diff --git a/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml index 21ef600a8..a66754842 100644 --- a/app/views/projects/gantt.rhtml +++ b/app/views/projects/gantt.rhtml @@ -72,8 +72,8 @@ top = headers_height + 8 @events.each do |i| %>

    <% if i.is_a? Issue %> - <%= link_to_issue i %><%= " (#{i.project.name})" unless @project && @project == i.project %>: - <%=h i.subject %> + <%= h("#{i.project.name} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%=h i.subject %> <% else %> <%= link_to_version i, :class => "icon icon-package" %> <% end %> diff --git a/app/views/projects/list.rhtml b/app/views/projects/list.rhtml index 51c1b544a..c6e5b4dec 100644 --- a/app/views/projects/list.rhtml +++ b/app/views/projects/list.rhtml @@ -1,13 +1,13 @@

    <%=l(:label_project_plural)%>

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

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

    +

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

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

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

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

    <% end %> <% end %> diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index 458e7975e..bb01df1f4 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -5,10 +5,10 @@
      <% unless @project.homepage.blank? %>
    • <%=l(:field_homepage)%>: <%= auto_link @project.homepage %>
    • <% end %> <% if @subprojects.any? %> -
    • <%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(p.name, :action => 'show', :id => p)}.join(", ") %>
    • +
    • <%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %>
    • <% end %> <% if @project.parent %> -
    • <%=l(:field_parent)%>: <%= link_to @project.parent.name, :controller => 'projects', :action => 'show', :id => @project.parent %>
    • +
    • <%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %>
    • <% end %> <% for custom_value in @custom_values %> <% if !custom_value.value.empty? %> diff --git a/lib/tasks/migrate_from_mantis.rake b/lib/tasks/migrate_from_mantis.rake index 593d59d82..6d8d55e7c 100644 --- a/lib/tasks/migrate_from_mantis.rake +++ b/lib/tasks/migrate_from_mantis.rake @@ -115,7 +115,7 @@ task :migrate_from_mantis => :environment do has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id def name - read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-') + read_attribute(:name)[0..29] end def description @@ -123,7 +123,7 @@ task :migrate_from_mantis => :environment do end def identifier - read_attribute(:name).underscore[0..11].gsub(/[^a-z0-9\-]/, '-') + read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-') end end diff --git a/public/themes/alternate/stylesheets/application.css b/public/themes/alternate/stylesheets/application.css index ced63a418..af787ae71 100644 --- a/public/themes/alternate/stylesheets/application.css +++ b/public/themes/alternate/stylesheets/application.css @@ -63,6 +63,7 @@ input[type="button"]:hover, input[type="submit"]:hover, input[type="reset"]:hove input[type="text"], textarea, select { padding: 2px; border: 1px solid #d7d7d7; } input[type="text"] { padding: 3px; } input[type="text"]:focus, textarea:focus, select:focus { border: 1px solid #888866; } +option { border-bottom: 1px dotted #d7d7d7; } /* Misc */ .box { background-color: #fcfcfc; } diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index a48fa26bc..d98e0d97b 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -103,9 +103,16 @@ class ProjectsControllerTest < Test::Unit::TestCase } } } + + get :activity, :id => 1, :year => 3.days.ago.to_date.year, :month => 3.days.ago.to_date.month + assert_response :success + assert_template 'activity' + assert_not_nil assigns(:events_by_day) + assert_tag :tag => "h3", :content => /#{3.day.ago.to_date.day}/, - :sibling => { :tag => "ul", :child => { :tag => "li", + :sibling => { :tag => "ul", + :child => { :tag => "li", :child => { :tag => "p", :content => /#{Issue.find(1).subject}/, } From 056e3703da194ade315004e7d91ce318eaf1e6b2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 3 Dec 2007 17:40:43 +0000 Subject: [PATCH 038/710] Added Bazaar adapter. Fixed 'quick jump to a revision' form on the revisions list. git-svn-id: http://redmine.rubyforge.org/svn/trunk@950 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/repositories_helper.rb | 4 + app/models/repository/bazaar.rb | 86 +++++++++ app/views/repositories/revisions.rhtml | 6 +- lib/redmine.rb | 2 +- lib/redmine/scm/adapters/bazaar_adapter.rb | 204 +++++++++++++++++++++ 5 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 app/models/repository/bazaar.rb create mode 100644 lib/redmine/scm/adapters/bazaar_adapter.rb diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 333b30b1c..d2d04604d 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -80,4 +80,8 @@ module RepositoriesHelper content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) + content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?)) end + + def bazaar_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) + end end diff --git a/app/models/repository/bazaar.rb b/app/models/repository/bazaar.rb new file mode 100644 index 000000000..6e387f957 --- /dev/null +++ b/app/models/repository/bazaar.rb @@ -0,0 +1,86 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/bazaar_adapter' + +class Repository::Bazaar < Repository + attr_protected :root_url + validates_presence_of :url + + def scm_adapter + Redmine::Scm::Adapters::BazaarAdapter + end + + def self.scm_name + 'Bazaar' + end + + def entries(path=nil, identifier=nil) + entries = scm.entries(path, identifier) + if entries + entries.each do |e| + next if e.lastrev.revision.blank? + c = Change.find(:first, + :include => :changeset, + :conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id], + :order => "#{Changeset.table_name}.revision DESC") + if c + e.lastrev.identifier = c.changeset.revision + e.lastrev.name = c.changeset.revision + e.lastrev.author = c.changeset.committer + end + end + end + end + + def fetch_changesets + scm_info = scm.info + if scm_info + # latest revision found in database + db_revision = latest_changeset ? latest_changeset.revision : 0 + # latest revision in the repository + scm_revision = scm_info.lastrev.identifier.to_i + if db_revision < scm_revision + logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? + identifier_from = db_revision + 1 + while (identifier_from <= scm_revision) + # loads changesets by batches of 200 + identifier_to = [identifier_from + 199, scm_revision].min + revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true) + transaction do + revisions.reverse_each do |revision| + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :committer => revision.author, + :committed_on => revision.time, + :scmid => revision.scmid, + :comments => revision.message) + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :revision => change[:revision]) + end + end + end unless revisions.nil? + identifier_from = identifier_to + 1 + end + end + end + end +end diff --git a/app/views/repositories/revisions.rhtml b/app/views/repositories/revisions.rhtml index 882d5ea4f..2a45fc2ef 100644 --- a/app/views/repositories/revisions.rhtml +++ b/app/views/repositories/revisions.rhtml @@ -1,7 +1,7 @@
      -<% form_tag do %> -

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

      +<% form_tag({:action => 'revision', :id => @project}) do %> +<%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %> +<%= submit_tag 'OK' %> <% end %>
      diff --git a/lib/redmine.rb b/lib/redmine.rb index b74f00aec..32239ce59 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -10,7 +10,7 @@ rescue LoadError # RMagick is not available end -REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs ) +REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar ) # Permissions Redmine::AccessControl.map do |map| diff --git a/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb new file mode 100644 index 000000000..ec9fd741c --- /dev/null +++ b/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -0,0 +1,204 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'redmine/scm/adapters/abstract_adapter' + +module Redmine + module Scm + module Adapters + class BazaarAdapter < AbstractAdapter + + # Bazaar executable name + BZR_BIN = "bzr" + + # Get info about the repository + def info + cmd = "#{BZR_BIN} revno #{target('')}" + info = nil + shellout(cmd) do |io| + if io.read =~ %r{^(\d+)$} + info = Info.new({:root_url => url, + :lastrev => Revision.new({ + :identifier => $1 + }) + }) + end + end + return nil if $? && $?.exitstatus != 0 + info + rescue Errno::ENOENT => e + return nil + end + + # Returns the entry identified by path and revision identifier + # or nil if entry doesn't exist in the repository + def entry(path=nil, identifier=nil) + path ||= '' + parts = path.split(%r{[\/\\]}).select {|p| !p.blank?} + if parts.size > 0 + parent = parts[0..-2].join('/') + entries = entries(parent, identifier) + entries ? entries.detect {|e| e.name == parts.last} : nil + end + end + + # Returns an Entries collection + # or nil if the given path doesn't exist in the repository + def entries(path=nil, identifier=nil) + path ||= '' + entries = Entries.new + cmd = "#{BZR_BIN} ls -v --show-ids" + cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 + cmd << " #{target(path)}" + shellout(cmd) do |io| + prefix = "#{url}/#{path}".gsub('\\', '/') + logger.debug "PREFIX: #{prefix}" + re = %r{^V\s+#{Regexp.escape(prefix)}(\/?)([^\/]+)(\/?)\s+(\S+)$} + io.each_line do |line| + next unless line =~ re + entries << Entry.new({:name => $2.strip, + :path => ((path.empty? ? "" : "#{path}/") + $2.strip), + :kind => ($3.blank? ? 'file' : 'dir'), + :size => nil, + :lastrev => Revision.new(:revision => $4.strip) + }) + end + end + return nil if $? && $?.exitstatus != 0 + logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? + entries.sort_by_name + rescue Errno::ENOENT => e + raise CommandFailed + end + + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + path ||= '' + identifier_from = 'last:1' unless identifier_from and identifier_from.to_i > 0 + identifier_to = 1 unless identifier_to and identifier_to.to_i > 0 + revisions = Revisions.new + cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to.to_i}..#{identifier_from} #{target(path)}" + shellout(cmd) do |io| + revision = nil + parsing = nil + io.each_line do |line| + if line =~ /^----/ + revisions << revision if revision + revision = Revision.new(:paths => [], :message => '') + parsing = nil + else + next unless revision + + if line =~ /^revno: (\d+)$/ + revision.identifier = $1.to_i + elsif line =~ /^committer: (.+)$/ + revision.author = $1.strip + elsif line =~ /^revision-id:(.+)$/ + revision.scmid = $1.strip + elsif line =~ /^timestamp: (.+)$/ + revision.time = Time.parse($1).localtime + elsif line =~ /^(message|added|modified|removed|renamed):/ + parsing = $1 + elsif line =~ /^ (.+)$/ + if parsing == 'message' + revision.message << "#{$1}\n" + else + if $1 =~ /^(.*)\s+(\S+)$/ + path = $1.strip + revid = $2 + case parsing + when 'added' + revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid} + when 'modified' + revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid} + when 'removed' + revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid} + when 'renamed' + new_path = path.split('=>').last + revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path + end + end + end + else + parsing = nil + end + end + end + revisions << revision if revision + end + return nil if $? && $?.exitstatus != 0 + revisions + rescue Errno::ENOENT => e + raise CommandFailed + end + + def diff(path, identifier_from, identifier_to=nil, type="inline") + path ||= '' + if identifier_to + identifier_to = identifier_to.to_i + else + identifier_to = identifier_from.to_i - 1 + end + cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}" + diff = [] + shellout(cmd) do |io| + io.each_line do |line| + diff << line + end + end + #return nil if $? && $?.exitstatus != 0 + DiffTableList.new diff, type + rescue Errno::ENOENT => e + raise CommandFailed + end + + def cat(path, identifier=nil) + cmd = "#{BZR_BIN} cat" + cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 + cmd << " #{target(path)}" + cat = nil + shellout(cmd) do |io| + io.binmode + cat = io.read + end + return nil if $? && $?.exitstatus != 0 + cat + rescue Errno::ENOENT => e + raise CommandFailed + end + + def annotate(path, identifier=nil) + cmd = "#{BZR_BIN} annotate --all" + cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0 + cmd << " #{target(path)}" + blame = Annotate.new + shellout(cmd) do |io| + author = nil + identifier = nil + io.each_line do |line| + next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$} + blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip)) + end + end + return nil if $? && $?.exitstatus != 0 + blame + rescue Errno::ENOENT => e + raise CommandFailed + end + end + end + end +end From 3b4cfe0ba8b4ea2ac36e0db36632afe3d042eb5a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 3 Dec 2007 19:19:36 +0000 Subject: [PATCH 039/710] Added some unit tests for the Bazaar adapter. git-svn-id: http://redmine.rubyforge.org/svn/trunk@951 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/RUNNING_TESTS | 13 +++ .../repositories/bazaar_repository.tar.gz | Bin 0 -> 9382 bytes .../repositories/subversion_repository.dump | Bin 15812 -> 0 bytes .../subversion_repository.dump.gz | Bin 0 -> 11015 bytes test/unit/repository_bazaar_test.rb | 87 ++++++++++++++++++ test/unit/repository_subversion_test.rb | 2 +- 6 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 doc/RUNNING_TESTS create mode 100644 test/fixtures/repositories/bazaar_repository.tar.gz delete mode 100644 test/fixtures/repositories/subversion_repository.dump create mode 100644 test/fixtures/repositories/subversion_repository.dump.gz create mode 100644 test/unit/repository_bazaar_test.rb diff --git a/doc/RUNNING_TESTS b/doc/RUNNING_TESTS new file mode 100644 index 000000000..bd72ac71a --- /dev/null +++ b/doc/RUNNING_TESTS @@ -0,0 +1,13 @@ +Creating test repositories +=================== + +mkdir tmp/test + +Subversion +---------- +svnadmin create tmp/test/subversion_repository +gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository + +Bazaar +------ +gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test \ No newline at end of file diff --git a/test/fixtures/repositories/bazaar_repository.tar.gz b/test/fixtures/repositories/bazaar_repository.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..621c2f145e71c37be837b7bad3f0c9b28096a0ea GIT binary patch literal 9382 zcmV;XBw5=ZiwFn+LsUlq3u0k2T?sj}`LmG@*Sh7F9BUCa z{xgv?jj1tfdB2}(j?cXB^Zfcf-{*HczvmfJ5G4@E{AEn2R4!6TWszMtS_goF1VI2C zKn3u<$5$^Pkbp4|g>Yzf<;(V6iA^|06QIgT~Nb+ zDJ8CZ5TLpLkApDl|LX)b>t7)Vkk`Ep&HaCjz*+xKC#X^Xwz~5Fkq`ZUkk!8~@Y4Uc zb$0jK^S4}zb^0*{ZoMqC6lt~Vyymk0d@LUrGA4&l%g(fpj!Knyl?*r6vSBjuM0R0^#ZDN3`WF526Uf zm4t`*GZKYN#K`RdoC|R@%O7&M9FU`$T70~mo&CK$JbXAbNBNe7T?>m$9Wypym|KaWiZIM715MbM$2eybT};+6PPVjeYaeDYu+swMr|WN zMjC)AZ!Zg=W%9t6-I7waA!22h>=a`Zs#mXHynQtYVzvS#Fp@$-B%nMnn4ki}ZDEd( zBVvb z`}GU8gHhE_h9q&2VGxD~(E^$jA}|K?XpEqQAcSEMg`hBm;Czgur0Ofl2dX3w3RDf- z@MxVl2Ya;_#i}gQ*t^d;vKB9jwXUE=d&RS|i-TV*1 z@7I5!7{Si}I)P81NDe4}1i+h>AWEu?@1((R7K7}#y`_LessN}!fh2$d1d>QVC<JD#x^xu|*I9va(6R6jJ-6H@E>i??p{|L<1f9nLV{Qpn^ zB?jIo{q-UMU?pJ~S`NGn1o$u;2z+Hw&wr7G7Ez464t7BE`JX^p|6e!wbp3-u0gaKA zkfLB17a}wS@=+KUKtdW}AWQ(;i9}RL`ahlE)%@=w6NCl= zt`eFV13+AflxCDeLg=qr5SDj?xnd^Lp9yAE32S*bh#SmElz%^-90f7NmJ&V!>QqtYV)Za5aa7Be>ut!5g`BsLAk8`(Jr*ee_5^hk6|D1A7$%5bb?Re zKlE?%pQBoQh5!U3SnhuYXp#T&+VUSD&=2?zv;5ZyK862ab^HfeI(`k%BL5Y&jIx8{;#(F zuV(+h+UmKt;(vk%z zh|m9GCQl9ck0K~Y^7uT26w(mRClQ+Dkp#mK0wIJlb%_5V{B`~hBnYgM|Lpk>x`FQM z|5Y_TJRVQUXV#Pda-c>2*E;^Mj{h*b|3@d#UHv~t8OcL@2tXhF_|L6qj^)DsmQdO4EUv)-It&RcF`1>DV2*glU|GI#B{j1b&D-nb+ zwf+hS_4R-6*Z(SKZFc{!PEc$AALXjs5URX8L?Ft^oB$941aOe6u7tGm3Rv~^p8$*_ zsw>njI=yZKQMN%;UZKWKRb2t`RjCMN8$8t&-=y8I!l&`SE~h`$%Kk&D@Bd@x|Id$4 z)xX-^->Q#&qzyImKY~N=pZ|p7IJ^HtCwTk(zc+{eiBt!`(%}?Ct4??Y?I4~t*9n3! zF1Ot@y~^wAznFh`pTQ)+u+iS^K?M`8Wo&Ct44Zg6HrYzwWc;%|jn_gARq0Zus~uKszRMxHb=KGIG?(2@9D? zE+&8fob>EolRIO3n9Wyp5k=o^*vrl1ENIw`@Or*K0r9&Uw|PRmkHvDQpn91;i7H;XuOJN+t_O9IEyK0{s?EdT2)MXn^ z#|a;%?CrX*?A*zeXTL~^u)o2?j9pSG`Krv-hQN8T=;*`N@rDhuzKiLffO%h!*gU;~ zEbtJOK3fd;?<@`&Z7}3<7P&vhZ2F+sr(>WS#(6#NHcvg@@Yb&8J12B}SR!2;9i?x7 z+VI3*FP4lt`J|WoZSN@0E?(IJy~16qV*hwn7{jj+cAIqT&yCL>nyvWXxZBb`<2kto zIrWR{D^-ZA;_EjmEc59vZZ2q1F~_%fKx>=CEjwn3wiI~%aBa40a)I+)JUJeZw{W&? zX)kIU(ra#V{Q8E@7Ue;!?Gmke->|$Wj`!tC+TUuwE&1x2zNVqr#T-x1?namFpXkRH zxHWscbbq(dOB?cKJBB28xpH$-QAu>9#KPN(H-{|Dho+}oHCY<|o!Rv*U67kjzJ2vu zCI0D?fi74*<%UmC!f3g}-LOcW`#sRK`=O3=ZHV8uJD(^!#9QPSFga}d+F^wUtviR$ zbJFwcwJ&2~`iPBXfyLV|!~p5jQq6eYZEe$XWu5zzG0=>DB`f113cn*`{&dVLDKIN# z_?z|?#x>0YryRLNJYBQSELm^i`8ETVYleT`9uesaQgQdOGjr`h=nSU`nR^0I9R7)eL<-55C4c*4= z;5?3nIf?bxw>t)|^%ikECwJ2^`(%IPVETuh0Hk;@)@!%=> zi^Eu_B_0(N<~_ci@#CSVtd1S|kilUhjwoUs){E8TzKY4xJUL*Ok9X~rfc55Bskx}ZKf9(gWK~1d!`qfX6 ztvYoaKA1Ip%GtX??Wf{7qL9T~hr6U1jLvyFiU+PYw=cC_?zEw$;?U1zhI?5;S;5uO zKV+XhCGb8NOPdwnd=kPS359X@X2VDA_m$b`KPGLCoeElJQ4(-F)H|-xS)U`t3$|pgymR{8a=!fqeh0f* zzb4hI$eY$>&2{SoE~&*cjf%Yc@X8udWp|u_{G6#-X!bl0WX{1(IktZB-|qQb!DTP=Mnx0$gxY71@*q09=)xn z6|}3}1J~ku{g1b{ow_i4T@67orW%|}+$7ThBEn!LLL-t;Hw0~`RBUxdd z{iuFoZP3q&H>2AE|z-{S@Uk+vcQ+!Y2?LTa9#>9)y%SFk}La;JHuyTgf)>-?S z;#2)UwY&dn@Bcxn4c_m@|4RRlt^d;rH1Yos@SSPkNTE><(3Dd!K5Z1|)+DrVxqLon zU5RLW=Nm3-MmN4t|L!mU3mlhq^uFXl*qNw3G{0rTgtmf*kYNd||JQ*{E%Tdm=Vv>o z#Q)|o+;7gvUdD%wegCM>Ke2Xb@8;%KZAh0xk>0d@*R;b&!5t@ixIAcZ<+eriI#rKh z9{x^0X6H$AKtt!pPq=xd@iYB1Y>kigaE$pO%XK(^a(#dQ`c7n?aFyR#C0XtoqcjCdbD_P~;8toJZ_8%-8K=mx$ z<{cez`PX&VpK>{dO9cCKGenaalhs|#y2cg;r-dddlA89}lXezux5_7W*bINy7K#Ns z^Kx5tc|J4d$!zkcZYeuQG&mlb{)_?XX_=ls2VR|DxT4khfu$3}#C!X7=@n`j=x)9? zz-ol_(VPp9l1A)5vwM=|;pa#E@fW@5hn@{o{uYA1<8RHM)nB4CfboyJHzH>*X%04{ za<@iJTxy=Lo62@All@F&C1wdxIS!> zUb^YV^{^*8BupNBX4cAE+=c}UJDfOncwj8rc5~4}7#+BZZ`^O1<%_`9&)vEYFE*{;ro_U#iD|?9O$XK(-d(`V+ZLC(*>{MqXE&?-g!RGB zvobS*#dDMI_aPU@>FY(@$-Vhw`NG_b}ub1zC*)j zD~}X8k92#{);HC$ZM{-SEOdWan7Oo z^{Ff=zr|VD4Gjeom)ccWvWH)8QLT3_sFJ(C0_>R7Rec)K-GAR=Mm@6s{-A8tk2Yv_ zLy4rfsveKqziF<&Is94s|JCg8ziku%apevwTUQVCel{p6&5=Gp!g_NdI~2?Sb}%=BpZt2gtU zl{(7huOC7FZXzf7CWx`0@?^!NNM8hEr-H^qpOF(P_q%$LEy~!#1)S{j>5HG!Hu%*f z7aFz>y63xHvMN;+>8RjDB~J1L*kjH#@j#;0(8Ycxunc3!2ro42HHHPIZ;$;R_W zVfBa|!M52ZyIOO$P3c*by_EExs2pyIQeigXU!yU1N_)ESPgVw|S_J;^rt$p9<+L)H zq9+9NB7R;QgP(Nya^tSt$LiWIIhx&)>?i0}7k;qA(yFLN`j&<2(w_piS0_@FicN~U ziN)#x7wmu6-NzR?>96J)Nn4u9Urx_7LQ~(aj3ALUDW> zQR9ceN2%9z=q)Q(;|r2TLk(VrOesEyTon0eK7NM?|IjMLEytWwRjU_`<+vS?8JsW2 zd^uz(uh}NG*Z!fQl8KS*nlVACGsxq|qt|ph(6^GD&;s7Q%q<5^N@re=9F*-HXUkG6 z-Q4<+yMEM~GMrvbyO{f!kM5PBfmAJfnz80->R9-o=!_BeDs|m3l$7^TFp;)*fqsGAMgz{ukut&yPduKOpza-g{VH5kku;`Sq#KGI z>HTHcE0-^qt;)9?RNw4b$s4eAk^F&xCBc%VMKZ#a*cEwGF{E`D6Mbz$WNR`OiSx>w zjaaPHs(dqLG|<4t7#S8Ht`MfGe_*M+T6o^sSpAe9{p-T@fM!$&no}PanID1fv|38f|8X;>Sx?>`0*D3)D-YSc6d=u)Y82|lwePrG<<$D4I z^Ybs2t)K*@;2%%DoifpQT~)=>oH8&Jmml&>zTlA@e@Kyh}z<1c!|}w z+p4rs`LU^fUvbTox7sN3(H1>7&8H9};@5520@SurW)Zgi! zM|U4G-!yc_vo2z!K5;?Jrk8=1Gmo$HJ;%<7)#0qG)%x6A4o^=UyR~xHJ@p&p?>EBu z)BgW!*ok{gL({Zdnqx&{xR{gu;r+^|MRHy|))zS%no-}_p@Y4{tLIyKBG%sx~SNXFX`^7<}9w@rtV6Bjnl|^2P zKy%ZzHZ5s;hj#a1od5Y2;8eqHv7)ydesikJBy~3S6`$XcR?<+odZ@xuhj;AcDT{~R zfwacoVzvtH-?y>5|E55(y#iQh*^#rnP-P^Ul-<9eHBD=0GC6UhMKGfW?DSl)URN+n zY4gF1iZU#9rILheEy{YHvUXYH=(RY$N8)ZCOo2Z8jrj@ug{bT}sy+J|I%+|>l5qBe z%&c`1hf3mauJ*mU)+VN?IH5ZQ%-j_3bJUJ+!}<e4zJU!mXnC*&I@X2d@&%T#6tsv-s;QH_=Yc*H-X_= z-p|;3Y@L95_zy!dwk_h0v^N6|<&Uk0!$%{i!vTIurOa5W&XzmtP=Tj6I<&or^n4cS zOgdgSaOC#YXZlakjMd~siC9*ZT405#{5c0#_b}UP)Do)nRaCluH>s@#O>;$Q=N~zm zQ12CRO1wZ;BBu&zGO}pCR9#b@pOFyH*UawG zhlh`5aCEOlnigf1p%cxMSKjMMi{%XKDT*|N0*XY+q&xiMh8zmz%why9t z*I$Xr#DrE~liD%Bsc4Eu{1DIkyb~TAVst02u&CFU8 z$ZuPs4cvX?W`tiqfU~@?>G??3_JVQ6bZ2SI>-}$BYR|3_6%oKUt7xSMu8){Nfw zns2cATvmKjjHyJBvF8z4Lh_2k;{1F_{xa=0cb{%lV0ZGCz0ViXm$hJbHutS}?>)9c zQT9g2GwsBX>49h4oSD7@isy4=e^}a zk>BjC-c!w8iI2P3^eK;l|X(dk~UGckj8;OD2@xuD-51 z%6b&FEm!|&{}m(j>9E&twOh_%Gi&lRTxlz;l@BZ#9Fd+}d@zH-JZzd`y2qS|KOe_$ zB+a|=>d^`Z>DU^HvL{YCfnB?_M{Y-9cRWN|w%qJSrjsbIpB=2zeZZ@Xw~?h;C@Z6f zH16-0ZG9~YS~T`~D9RL`DwaN3Bg}^s-o!l8?T{XSQ+jV9;eqTSuh{LK7FMCMEYaw( z0C(+b-=+I}OZP?|%GdC*Fzb{WPYK=K71T6ic<#wv^SlDzw(;oO$norZ2`c(|Q`Oru z5z!hNLQO%#{5qS7)Vo5N=|ZJzE>zwekKX$udP+{a{S5yT5z$u#a;j^2{G{aGyyp4p zIO6O6KO~4m#S#8V0?6OU|G;X_<^O#b_@n+mEbIOacK)1&zoLJPjt2LMfDUF(|K9`t zoBjz@yxMGr|39PgwdX(BkLy3HYhtjo`QLNTf4&Ql=wiYSAv8$)pq`MJu$Zt7kc{=07*f!Sk3{|Fo1hV2I;WGVVqzXCpd%%z7wiFg|roF$}Ec@jYg%>0UU(@gyCou zBE^4}IE6}4gBlwGu@wr63BPL>m9mln%of89gET=8@qI59;yb?S5EE)d0{s(~AXJb> z!I1!}?-mFbfX>cR2_cbY(cEZ62D~vifTQDyM8M9~23V!80?q5DXixA{5yjnpuiQ+`?K^Q5U&6St^fgK z;D{ucEf@<&qElz@Ry>XYX`6v#g#7LN_5Wq~iv9nv^~yh?@iqIe{>l1Z>N@J&{a@$y z|NA}wb&oF`z-&*3tumepb$bfqolS*1F4Vhc5BwpkLix}OVUrGtkwU=H2mm#N5fsAs zqsxFO0p?aVW_HH5W}kbFZ_wxeq7nalCKX2p-#NH(}-ND%*xEl51>##ikL`;4#`9cjsX%>xdcc!I^(nR&@D_r!dJ$N^8NcDd(tfV=6 z`ab~w&HlgN5bK}P_}cuxCfELJX{p1@zG8IX1!1%_=hlDvF0j+V4l=dU@PDAhtSn8P zpznq7TaXv}T&1?a8Tvvq%r-IB(?G8I1YC$@5JAV0(SSCv1xE=*bN{XfU^MhJFnU@T z0HqG;d#R9hBXp$z!^+l7pqEELR6{fE_#rQ-NGQnC9kLw4&)_$Y$qCK^>OpAqB;b2`dWI7a!+F-j^?}2~ z!z`zTEV>Dc>BQ=ZV6j*eOp^(w^~A&k+arOU7r|~}vb(a`U3u&-Cc7t<-ILAk$z%7F zuqT*oHk(tD%Be?iTA>XVk7g6F77y$DL>>O6y>D_ z;uO!RNxacJX5C~ite_z5!?T_jtEL=nENg6B7L#z`Bm#*N$S4$a5lJ!7!7ffJoHtY= znrdSpyvok)(o^K-c?dCP^?ET^DGF>et<3C9D@=Ui{w0(BP2oTIe=1(>!-^ZvZQoJgenMAX(AzB1+Y*-c)Ycks+h{B>+ zSQDub8P5tLQP}XTb+JbltUC0+$u=o*TC_-$(gCc^(Auu%x#`)JK z%mR>#5k*GWvYBEC;{y_HFho4w#sW{XAmF_T6k9UI7EiV$TaySxYkv%V=@R-f3)jUi zf54aU$9xF{;xCNH+hEQ%6P$VcV3;w%8Z$qFE9NqJAU9mV7m6+60R;TS2&P0F!WWK9 zhY1z@ONp(?wj`=0nM%gv@jpqtn9qhR1k7<|h$KPZ^}SjQiNu(Rt}t!{+ZJSVh2LJ8 zEb)Xrm~8f+tPKC_!tj_Mb@Z!oh<`%re?dvsL`x!`^zSRl%2FWYheE8Mw8i1_pnqRo z-cXeI7sn(LXqcZm!r9igAce)Gk*Pr>GCl}TWU{Ow$ePTekZo8j8imZJv42rLkx2Vd z_jdE5!g=6#BhNJ;SmF&pD1yajb0dQ18Z7sAv!EKxvscB~eP5IdBw`7t!TRl zW;kU34{l;-HPJfW&EkiL!@JAg&g%E4@z(FOL=zOOetHX%aCz)y0*Ljk@8{PDrdT3` zzEAu6i{0-eeijb%P2qp&$yOXbj}3kI;Qt1P{_{QbowHT25Q4;z&{7!m}J{pu$i3S`5iQSjC_Bz6#iLm}FbAu^SPXOf9Q)S3jZHjK>}VX(F^h ze4d3!91T0M4@kkls^>;RKkQ&b&^rVI-M=jfWbv3H5oY4|_cja&vLO!0{T8~o>mZYH z7cqsKLC|6R5u6K-|FdK<1za(c2akaJpa?bw{61C0{X9jv?qQgy2vOGQkLFEyxd=*pR_6m+wMa!p$ZIYxzPR+W-~=V+byhh+)FN z(_Jy%0}~Cl{Lc8_@FpvGAN;TT)!$^W5=w%i;iSR04CZqFqK{esL&*8v$Eemels|B? zAP$v64q{moZKxbJnM39?^LqlWz zPrg3x8-4O*?7@T4hYw%%_Kw}X{r2ANhYuf)c6N?+v=6tpy=iM5ZEYEAZXRm6F><|W zw5f5dp>C|U>ScZXPs6z0Fo&mYXsAIoa)Kaz7l|H$dnr^m9ghH`THbFzDK zvIq9<8QZb#;l4dXyLb2R-gSTXuF-8x#==0RfO{iJn6aqIeHV>{wEjEO||W1>by zq9@UjU9s!Z($Zc^gtEAp0g>>bL{JqQJH!hg<8WF3xfxs-r%%pod&uWO^w??@T{q63#i>SRi|v`^cFq68`E%AAXqzH zXWqPd^?G_+%*=`mbenb5l1xn-b#TXK=(cF6l}yoU(A4b0DPGc=)T63;blRjI3{a0# zs#I6sI&Dg>HjYFhUB;qIaJUvU(5V2NR8wn213dsxucUMYhdZhUTtul?Dk-hg)!mQ7 zRHA@=I9wSDbqb9xM**cuYI|_#W&qf&rnVi2D?$PJXmqWd+#W1&R0+5Y0Q*s>I2L>uL!D5q8s6;f71pu)q)b#1oMJUu(crg450Ac{( z0DzjB8u0Vqj1Te(ng9VXA8#Jt698%qV4yPZd4uzFKmu^`Ju{W}0W| z@tcP|cABlcR$9<@jDFZN$FuB2N1<;?=>2PDge(bE2k_sTRuCWS&ATaS+274 zUMBz@7Wx+e3fgv26;hx&L61JwnIlGfKk?ana=x=oOG)q|2bI10Sj`$GlNif_XAPC= zlNC@xZ^Ua~oFRQbWjDq+{9gH*_S(_0Q!A|K@_mCMOaG;QAw2d11(W z=iszlw;h#z+txnP49O)w;w?*?dp0EfK_U?i$W0llQF=&t_2t!qxFd9jYE|x$HH!LM zbqo*0n`}?S+IHWr>xsrS9?t;u9@lK8_60_%Y}DaY^b%uG4uDGV8>T1a;pD+ZsS2mw zV{L|dkedObkKa-xgg?BU)@MT6M!XhM@@V_38Ham`my%#}S?bg+A}z2qy^&#hDy3!4 zz?H-_b)R=1b^x@BTP=IqG(X~TY-ejA2VobD+)|En1J+%2*NPn!i9Y(6SYoR}vxSO-DAa!m3 zW;NWRnzOAXGW+L>h4$!Tq}7Z16Wfcr+Sfhbdp>pZj5yBwXZ!T`__Iv&f1_wTpmk?E zV`*>X4YHR`-DSI^^wYuxFBW5(5n*%g^TM_l^=q27*f0{fGqPSun3jON-Halet5G`( z$oJ2PT4*oRccz&u*tNTyFgaF4yp~kqCAXl(>G@C<)i2khTtPc_TT%DjkE#_-RFx4T z9ROz{)>t?I>gNJ0pJv?46}8NHeB}{#?#-0pbiL1!FW#1X#PvCN8oA6N-LQ@;_Oz2P z+$lQO9eHthZ;$Z>R;(4kh+kz{F(z}y8am7{&DV)3(X-JHc(M3Oto>s^HA#HK6}@ZO zOGU!xXKgN?oWtQeF<0x`7PZ(7**$;yAkuF%`R;E)2syg9SF#o(Hr{RO!pt+NZJVl` z-*!B%&}eia4}UuANFJ;k>YpWf`C%bZOu29`qAhj!X=&$2(xKI<1vh4)vSmus9=EM& z(vQ)UQ_??L^xV?ATR`GpnbGCAV+6HE_IAXGHfFcbesCdGagfrsd%F+%@a|S@vWapY z0tjIwfyOB)7rO-Yj7%3p74#(PQJWFCMay{Ktdg;sB-E{C?$qNe zE+AV%r(oW6zMG_wptLneGozy3cq|l|YH-47^8QTu*_H9zqjC@H4+(b6eY_?uX0Uht z&Ax+#@jQ^3>FTO(Co?S5a;q*sixu$VHFQ%2rAHZ?k9&~<4BRMe~6meLr};i-+m zA!2NEg~rxUzQ^(p-Wwt22fHzY z&WcOqkicEW_F&Hv9a;4(`#i#!YKTso&#C64l-778|0Ob8z9z8LvmAGgP>(R`ad7%P z09Y~%mM8h{rr89=%jOGn+QW_+EomxAoMx_t@s*l3BW|pJ*Oqz5xo1!LOsphiXL`Hr z?hFy_X2^|4V8Pwhjwpoklaaz>tHn}kUjF%j_2Jp_(I*C`FgCBFK04gaI)*=YfwVKC zFvQ~c%y-R>omuNT3ZES>m^)n489=ViMNRe?iKM0HJ4wL3q|nmzyQhr@JHj#<#~mti zJ=WhqYOoBiJR!QHN);T`Mg$w#@wB-)Yu=u{7wT^0@4V45r-Q-CDF{*9csS}Ya{6&P z5U63WI1beb22%4J_X)g{#$1#|uS1-IXgwRNCLyCYg!7y-MvJP~MJOFGZCai~u)THZ z#n~6;x679TfV^*QN!}N8zjN1D?z6n2arn;DmCBO>j(U2}NRovGvcSccUpiMP^g0Ms zmr_Db(r2_JG`yaIyCAGTJhhi$DayHMu>(U2z8~VQCl5#`OCvAdxvy2ExX0olK-5nK zt3#R3+g_U;x%|k&19Pxp#rr8Pm$xhoDV?+h_vGX`Px)czxv_27C)*>c{jA+>RCv|d zWp$HJsyhT5+2Jvt8f&Gc?qL3H>^v1l9WoDL`L=gsm$7`k(xNLVqcvS6y6o#8El}at zY4_qjcZw`inznMgRj*R^j#V3UoY{VVPftV?W09Pcmz>r-VA!gY{@Cl)+-Enp)4Y>b z*L&OvKhJogcI^H2m@`^@`qE<_x!Xxedo$H_=k#d$wf)7A@w zft%0*%w>$+J=?Yc{}@Ht0*;<8_KWvxH~H(&?+f>8PojmasR0|-?{QPEb&a!Y#dzvV z3hu>jL(DnXQE*_D-75!RH8)o`|E0>w@NJKc?imKG>ai~M=a@aD+`VZxH>KF# zxi2pUeYk`}lfClv5Hj?V8&-ze&+<#1!f#TsJpd(?cGjuJm8P80 z@0hBH6 zK<61GBjbbKEAdy1rK~WDN;Gk z#YQ6QGY{TKF&;+c$j}FFt2~NN9BT-M+(TP>bT{Mz^2JJ*q^PqVTXxc2T)i=e@#qHW z7VTqOPOn8xIhwLd+v55je&Ze-s1u9YksyWiHlk+;;eA z659d5NKsV+v`gvsT}*dLDY_h=QgL((LTR@1fRfgX0OW@iAJaE)8#VuA%&A)|X+DlK z)Y^tQvz0N1O!VoKi<2oO7N@I5Clb1Xzb;pMfRhL4hzzO9y!!1^{oD(3G12wz7w{2J z%E9FK>ymxcKWbQf+Ka2LQ2U5mLH!_x-mOqH#eIIYdj#_SNKaNmYz!En8Rser)0Hq+ z)6WC5Y^ag$!RRRfFkOJTIJ|3JJ?2P~-ti<)A6?OiiR)7bF^?6ZQG>uVcypuER{ADS zUjY&5z|wcC0*=zQby_Q4MWmKysCoGcu9_K06(E^nlqq^0IyGoCU73i5uViLYgoR8f7AAZztuwc_fGcWZO3bdg{tZT};& z<=Ex~M&g1@fgM&B(CVaemnQrq%OPtZT%-!V;Qo2sZH08xCxZQLz?Pgqq2kngdLI}CS(^TDL3>iM42sW zKe9wtts?8mRDEXfv>4li7i&2P6-Zrov(%7FV6h!lO`FyN1O#DFsdwhG~((!2spMGq#C zp|Jkh>?Jt0x0;JePpEuOD(sM1WP1DbRTui2W_W)rTs9>uF-Y|p1A`?_ZOPc$Zh(lC zqAm%LZW@T$JxJ1B#qeSUS~24OI}^EK!L?&+?@VVIDO(eot{z~<7qdeb~VddNlIY!%ql@j1{I3L^Jcq$E3q@$eIF|2Z7wo;g5y6N2mW4DkVp#}>;Knsxb54bRw zE`P>(4(`3eW?w6SDl} zSW%Ow0?O)@u9lo8(NmlPU2e!Npq@_KAZO^{ffH zMp`vVEAGRbMoXf{`*gJ=xBTweb*0K`EjZ;1IAybR^{kDnKULFrAS)Mn0T;V)911Q| zL%rc}4#Wx)b*n?m>ajC`({fiIRI8qnD(x{xJ097dORr_iH}21_()DO`?mBfZ?AmiW z_6$zBNOn!f5oM05{3wkLbZ(yC3uHQ6-$LE%>YCRQi96$Z$gswGN<6SdhTWAxK%Q0I z%0THP?Ra*3$4O}wdzT+FF}}aiXVMDskbzy#6LrRx6sT2M8vt2Uu$`<*_X zbzaX?WwWtwH1Ae~18Qn<-jE;Aa>$FE9#TIhI8SguesZFHB3fL^S1ZhD`f|uyxTWp& zEZlJ?mDjRcJCMrwOKOEniF&E%*VPA<90NWzxspWf^S@-dSa(cKZ~7?BS+#e=C&8+= zzFP~YwF$G&yQwOBeCaUASsm!!W}MS{K!!~Z1iNv{JLs|{ewYTS?EU^)v#n}sD?^9< zv1;osmd(k3o~wL}j@FQ>$1kTi2HJf+H1EK9weylIyBqDajc~6T>@Efp-+BhL^{tq+ zJK^KxD?{ha%uOgoU$Trd0~Bxp1G>nI=D5d!#Ck^Hv6byfj6I~F8Uf0rwtE*Y zIn{0JZaBHevGU&3VyC0`SA3D%m)z7{wUU@U7RPJ4FP@=%pY5TpELizv>$XSL*c-*@ za;AIr9n38A+w1p6C+<=kt-i|E#CFhCQ*T@U)&JhX^mbg6Y{4G2f$|G&j&(_jQ8-kc5+=c@it{j`!tP!v|zxURd#a+(T9+ll@J7xuzO6=lS;a;Z)V0K&= z&571&>GSh>N)H}X8o7ROGx`AKY31D3{RbZTxedDAfF5j)tt(QO7m&U*yUZ41Z_qJy zrr8a-%H@D+;+H4u`>%#ASDUqPZl^eP#=$J< zt9hgxH%he%+wdqyWN>zuFp?PSYO69{u&R34|(}){bKy~bXnhm1B0Tl zojSb=p94*=)EW%QRNO9%wAZUtURj%Q^|*oVE4DX&=4#a*<;ymo9|q6C9voKCazy!) zuU}c#zHv0;ec;P&$*;G}8?jrxLiEz=lHI$EH11>l03*?xOk=OK({Bcr+xfQ6D{204 z@G62_jR-UNXiU9tzxuVj`tzf!fk_G@4wmoKTi5HU%fHAa+)f!8kYaDPAL(7EDx5yr zX}3Pftv|)0z~$4WO57K%Y03|B`^#f6%jxPy4(|fTG#>Y^#BKQeIMMon(`vboxNRx+ zxJ&`3bTTJ!yn5 zKD=Z&psG_E=5o3qY%iH>m;WNOF>%W22i`HupA*9OdFPa!?5NaI)}myd^1a}j<5`Yf zbwToUF}gsv3$ZXRNxt61A>@I59NE0(zVh`USL~;K0^Xrk)x4ciZ*SDqLkCu7Akoha z6$`Jfa>Dx94ptBp&%|YVYT8dVJmTt9pwXGyQs!VdoPCU$d9aGAoc{d*+z!esxwatk_Ye6O!7StbJBY zcwV$@@#dL(B)|DJ?N;AfPg;_Yy4BHY?wCB8?xe$K?o&mT z%|g=!IsTptfU+b^bHs@nw;INQo31ykr;A_jT4K+gxl8<;Uw)!fu+j12)Kiak`m9|0 zq^!#>AU$-yaZc<~{jlSen%8F^-iJ9LX%}|h32vJ|>wvHi-J};*L@+Ko`yf_h*8=;^ zN4ni6?P%Pq8mn3g_~OI1uZ$kZKi37+qSE)fJ&C@Rvu37iXcgk%$05H&H44=~N{e%w z5n5AYs>`~?ZrHudc1L}>rw=V}RoXm78aw^%@n_kh_xS_!h0W@-{1%`OJoDprI8|`= z={D*PNN!&>)>|Fnm!IJ-AMMCKoaScBwX35J9EiEHW?=rhvF?$hP&@(h;p{(WPt7?H zqtD4a7{97vQl4@Ra=Uf?6}fFLcyHT?^8<_hlN^R?_Um7|AKf(d>*vH&i<4n)1J8N2 z@16`Scey%KV}YjEK5n^3y!w$l_h2LTt{XI*-;N+D#1ir=A|0sMr+ zQ<{d0`FSff8}(MR)vtc&amG^m8HuXIc2IvCsk6c^`iWKR=TwtwVAXbl3I5W$=ck)? z0?JaEvj+2V-oOct(6K?&eX8z}H2YV@hFIIjp#A#3O$X;iQDoH{wmsS#tS@Tb|8T0# zMw?1PE)b*S!jQ8#6mM8r6VFD~X%vAQt&hx5 zrRj_*Np~tgW1;LP%kHOaQm~RTF;KOWRwxcocqiK=Qm$6%cTlvl>qRfXADY~`u|~x| zeX}<2kitFLMq+7A(rR3tcJr`;rSOdM>LX6t*$xV`b`j8C0=Y?t(vwVmnHYY3t=6qf z>GaN<+d*Xj+s{Zgd~-9!y*)waGHN3^HZOgXe4X~BtSe+`p2~J}=b1(Y$yW5G%6p5P zv`;uFEEW>ci#?o7c1V+En)ISQ9r7m~%}tu_G6T6b2{Mm?5@;q&C6_^#W!kW-g*YDL z4aBK6-hl;&PRs7IzJ>i<@)WG#!ws^l)t*dunHWm2oUt|xK#M~4HB zxCD(%yj#|@W&3Ql8EXpt6-?i>ADbJOp!xN9(hL=0L zdFQ{XR-_c4PzmgjtATHN<_t$wr=I1F!ZO5|RWU+^CqQ0X8mUN=A#AL#-`IF<`6*Y0 zlU~ls{st<{{A1Q(p=+dxRbSsKSmAk?K=7VsduRD92ZS;mywLbgD@Q|G6Sf*a_FaQN zDgj)N-GzZNaXw4DSF;OsL81?j#`$LNppTvWq`=O7C?~{f>80t z_fF1sxTM`Utmsf2k7ef~O>RGnb?vTGI$m9GyfHp1U|6oc{B}c14sWYkEaM7hMa4!* z4JHuZi{3AlpE(Sya~zIW)x2}XwR}vfm=TLmVdU1?O!F=V+m z>$>&&ld3!(>r-l1{cO79o)VrMB_+{rd{u#qnzG>)IAIN=`}UCTOHr-&&y`f4XpC!i){zB?Iu)ek)nLGWnC7 z<`vR$L;MRRyIe$9Bc)t-SSoJ^$Q3J&c262k!20&cxjX>y`S&E%|wKZ)XBHCQnQ;z}+)e6Yu%YIP8M zYJ_HwAVvENOVhzo$ry~s*fD@5Hr0R%UUEVY99(e_IJm+du_gG@!xyW{nssH;GbGrd zmPED2*aLE)^o1e_bRX6aIoy@AN@);KG+~4zsqz2DwFyP z0EGzSoBl(T6Neejmx#b`@S_Qs5DXrIF>Jv?W;nA728p z`4O|kFc=A9+hPpi9y1u4WDw5eMi_wM5RBVoPqe|PBpx>c0%4RLQviu3+C)$|m&NDt zBj8uK!2=RPvtTwb;0lK6LAEe>=|@1+@4%&hY8f#MFdT=PS&F0nt6_>%Yw8~u22X;R z)=Ux+3ZjviG!ol}#Id$v(b!}%nG!^0aegrjl|uayrnrzVgkbFoM3Qh=lRUl<6mi8M z3>Xue!&-y%rc2c0As|s zA`t}g;AyZa#W0MPFEocCt-%p+J2(6X;y=)ffLP|Bga_M~#f3zm2x7xi#$|!2It!U1 zm?1O{c7#RzHVPiZ<}$-A!9`4n1x3Ikc-&wKj|qmu_&BCH7|9fJVfOG7Y+?Z{JiL-% z31kjJ60RtmKOwg{7|w<1(}jF)1UQcO6>}$K6YhB>*wyuZt12~NB)g)GVMB8yls zQCO90BoG)01NC5b6Y5_hVTxgNpEm!90IV24=8ve+WT;6~YeV?qLW!h58?$vwxQ32SEF;&|6jrGa?xJIhy^BfFxKG zf9{w-V+WDh6o>=iiDWjLz+pkwY={N}v6(~ykwhl2X}{_ZCIiRxarEr}W?uPkO(FkG zjW%$(#jjvx0>y^S!CSMb94ZWCCqj5~P!Nn!W|EjekPV6OE2w>(4Cepmi2tb^@&BXu z|HILn{3p>HE)sJ7hr%WT-1ZOA8$J&^PegC{B*li|0v?Xmf3NK_o#ISHd&o71@FY@_0Et80tMc>mg$cJwj=M#Ds4l6l)Ue7q!7uF#lDV?2q!; F{{c%Xbua(` diff --git a/test/fixtures/repositories/subversion_repository.dump.gz b/test/fixtures/repositories/subversion_repository.dump.gz new file mode 100644 index 0000000000000000000000000000000000000000..997835048625569ed43e475c88348790d5f2d56d GIT binary patch literal 11015 zcmV+iEBMqOiwFpyedR^~3v+d1c4cyNX>V>{a%FIDb7^#Ma(OOfb!~6}?R$A#jBVTh zHBHORRMVuih+LvnT4rmOB<&^JL}L~DU8A{Y|egLn)>EEHL@g#sxgkXrL0L5MUImPWz2{@8?>57ID_NP#_v zC514{K_V4H#N(;fc)B$K?@OTAlPUIivJKgeL?GG)U>HjlGnQJrFLL{dUc%4y5(vZ} z6pyvRnyKTO`N9xbF~JTqPr#G%SbUHdArgwEHt+xfetZN=CJhye$E?GIiT+cI?a1~d znhlvo#^dq7vv`q^16hk$V+KfM!Qb&-Erle~Z(ZR$0mmNX@WkI>cJtMexDz_RldP*x*e- zNWd0yc!H2QCd+(1tZ6259W^ly-<@)UBy2HHH0A@`igpmNA|S_qK)}IvymhRbEsThO zwL3c4{_Sb3^}8&|_>1lD-hyR3K4+;2Vt?!V{i}c_m5HJ6)BfY-?;?J0+&>KxSIFl; z;{P^6|EHR5L&Ojyg~T@E;J?Td@*nX8Ch?Cv;Zh(fi%!AYQAwO&0+&Lhk|8pUjc1XG z!FEJE>qnkYsAJ2DKpI~kkdSXJkw){!76}CdL(hwZe&g_T&^HtUJ-@jGviU5D1T+5o zZ*3S5C@4N4@Vw7dfn4AdhgEL`*$8adNk6}G1}fX)YkfIYs*MW^Jr7k zVDru48;v844Wsq7qcvAv)zuBwRW&s=y}xqlZDr|jW#!AtbED_V`Y&A?DLXqL=#e9jPMz#OQShpuV4$F2G`p$qaPEVG!>3Q59?i}k z%+2k~&FRj~>EFA1bou5sq`^3aYsR=DxQ-_k0va_>CHmtjqx~VTQaWp38VPfKgq;-RF>-u72+v7Kk zN+b_rqDCZ=r_qs}vFp;)(_hKNiny45iTIIBbR{-+kRLJ1<+e!p9c$L~@xunfI1j?P zkHW&*BA{P5kRl?uGc0Vwh7C0W!9eipha6T1htG_C2xI-Y^x3xTLZ{I{BU8LaeTUst&ytv$M)^RHJ!A#>0 zi)jiAi&9(L&KXt@r%h{`IlaW%HvsMs1Yhlq0>UT~yD4Q|0*~sYT z6xUQE(FX>I_*3>*Q zWkNRwsKcpMYHO!WnUtrGBauj#vFH*Ut{DwMPrHvrVBsU61Qj%WcFQQDPi zYU>OQ_u(*=C}1xRSB65JLZi!3K&hJ6ZXCJ^0Cs6<@aenP4+;s08 zuQLbhw-}JZx~`r%)R+#srcR!B_VBeG=1Y$UT{~NFV-KEvJ!f9Yk(>KylILC5N{+T1 za7x5X@h&}f>yXzD%azwl3tNve4teK#mmP0E;a3v&;Ck7K&eP1=)G71Ji|&?$wjN)3 zqujb4K16 zjv#f~ta9*OCjy-o1QY@)`VLVQa-b@~h%wofD@FT0UB2h!JXdOSNytJcjXlO#-D)-S z7@NZ9^_AKaRZwDI#2Y`{bjCi)E{s{k{qi+!H6x>^R@gF>dj}+#3cwv_6LBwNTA;^) z^@p*_;?Q}nA?bM@+beswu6?Wd7!OhP@`(k|^FmlzM3j%n= zUEIBescZW-YvC4FpKU2oIKEIl;fO9q+PsME&H{MWdkTX zOe>;Zi`r30esD(8On;TJBi%~Hq0Q~M`OzZc^`r_PrTNV+F9xsB{PVoZRrF)G7IoeG zs9DiS(-gST(6V2soQMQ zO}n^aZwKWQJ0$12A}k5E52My7-VU46M;#V84lJOl4p3TmZCj2$ zw5tW1Y_6V<0798bpjisa%^^WMGs|tdhV~X&^2}AQ#hconu&!+G)|wWyMbB*S%#zXS zB-HJtp0r~tE+Cu3CSiWU*`dQD3CsNX9?Zt1w+|@Vv)aimhHA6mZ(5)ef#FGvVXO?;U8P)qJ zBOF|e3PO`DMH0in?i#&vg$ka5PK(rdz3Y8Nqfs%++IrFqZW(#==;Y|k0rw@DP*IOz zYf6Kl-CG}nL&W+OdIyFcF{lSr$=N;{Vdbu=XDZbkpR7?1aJ;cwrTQsVcGqjw-Y3cr z-5MmC@t|w;VWd0a&=ItcRB7l zp$=i%?d0;gAFyGXEKBm+MW+VGE9QxF+rp2UE^aJIoMNSi@snFLA#SdJ*P3mzcMqmTDbVs2hXdwi&k zeH4H00%?cfM5y(#8Sk2$JF?fcpLl+(aL!P5M4>i$iIFg=G;35O}kitqc?wvLp zXb;a~9&@V5^ICrssl%Rr^(oO4RjT5oH7wf5iKoxWUGw(r{V-3{0N0Jqx$R7DZegg_ z#zRq0kW-H_fFK={MRBMOFo>4#yjSF#H0q`O?bEK$hoj`5B zMdPwug8l7FFVDWTx>LRc0F?b|O7g#0`JcP7a<9!*okMq@tyG^7c*NUxT9P6>hz%~f z{K~aLrN>F6xr7pWk}<70q5jPz+y!ynp~*c=8%gd(>+Kj)$b(Q%BV|B7Q672m?gPCd z)!o*Q0HSd!SQW;4(fY>n@a4zWUYG;*E8b6XyS!yVXz7G4xTh!2c`FaO&WUZsKHVm$ z^0)J(YVfOa%4#Q`)OHFnb--gjHPpyUJ;8!I*tr_aT4X-L=55c$PBY~?wS`wxMyfkY z3^_MGTB9Ov(C^26?vU7|G^X*oG_O(ij8>VnpV{_ccefylxll>YPfl;@pWb4S@xm@?~Z%_Z_HXZ^rk8#@+j_geMcT7WoK(YXD__LGsQ ziu3-O7A+SD{kNd`n9CTY`}VE<0WqqI`CKDI>=)lR9?CafJP_~EpFj^?Qw`Ry-|eAZ z;~wYGg7G$%72c2CikN+_y>S03hu2QPYF?gU!7GiE5nG>_-k%=0s@twKfNS}Pa_^SI zoRnhIFYd`{=PLr!mS=6cXWO!jY4EOFvh`@(^Xq*E)zWRArg}{BG&B>jGiB5E`kUpB z$bf4$Ceqpa^IlyH{%{G0DBnFj4tL@9NV(JK$}`S8*Yd(Hu@qsBM8q{3vyVIbe(suN zWph0Q+Rq88KU7;(JLOgCC%R%uj?6iKa-#KMZL5Yh9WzgW+p!||Ir2R9D%$1#r`t_R zHH_B>;#|X)JTAH!>KRqdrBy{4JXQii0kkiGG`QoWB4!#T*RKq7oavuBN!X}izaL5{ z?Wom^D@{3L+&*tl&aje2QMl&nsJ+qE?qOyv+iU0G!|qS6P7J$9aLhYr{3{dXDp%4? z%GfBvU8-@vVt0GTs^@yRhr6>k>Jc8sy^eC76i~a$D`LZ_)#4Yz)&uX9Pj%*Wbv~lK zOf*?%;{?ccgQ;wQ9nMfU0s$&hEtIKxPDkx2QtcEzyr+Nd z2G5wgMD>ZH7`5&E>w2=(qkeDok;^@CDtSV5Jo=W2&&`IIWhMZ`(7Xm}W*?2-=P4!X zG6j8M!^N0MPQXha$F(BI{zS*zZo?R}^^cEodNyK&^-{|QU+d18X&wo~BT;W7pN9#i%&#>lh!wq?Wa3=x4yAk*svQ18pNWuCn%I;YHK#!GRbJ`RZ3={cHuCc zS&Q@=wjMf?#Bl;Ja?}+O+O2fkPL`*v6kU!_sW`F)p*G94UrldXAo9bCj~Sb{j#zy% zxh*Jg_h)lW0+`4U({XGlw zFwu3M7x03o%joz>;^X0*}zQcG#(2L!_2wYWetyu34JMRUn0Glm&VnIyHDCL!F3) zKglfR>dEw7yPef*=BaPF=G~m6{&j75oJHolV(c?;jcX_NnX2|eQTFOXTE$hF@7Ctp z8Y00g`o70fo6*e)%*6RwA_uG@u*F5=9$oxNkxSM=xXD%gz;UXX;*_~3iOrr=n3l2hxDr_a5dXG15N4dV{6HTpd#L8R#YhkyXQGX|U{vR8=! z)#esrD!MU=OqKP|XD!BQz13YuPb{63ofxe7oQc5_ zCpTxNwV5Cy<)}*{q=yb-RyUG#Pc@=gg>?MXyejlX1x61~Ky4vwG7v^iN;XbP z(G2Nh503*AS!NJ+on_`P&vQZovF&nAw-=}@QF`Y`_A6B<49z21ii{pPDcgjX`Kc>O z*COc(q(d<#^L$e5Ia4UPB&Rw@FQz>Gc)6RAM&b~MH@qeYM;D?!Y#H7PIYuL-Y(gGl z{U;?`Tt%V&>`*+5tRAE9#ZC!aey9N3*KjHwQ)Hl?-#(;qVU}9BbB4ve{iC;$USTHl zK|l{s3JAO~hoOANbvEw3%4T4}>COxK9v;0z{%`SKbGMY=--kVKq)|P}P~{Kj9)IS= zz%6XZ(74}?j%HmrepF?3aoid({>dB6t-@XNv!dmVlrH!3WzzC(F=v*BX`ziEH ziwn~9v@1=|5~~q=Ewyp=DhrIX{i+t%pVf@0LY~Qw_th)MQlfv`h`68CB@ncVl0nlru$YWyh}}R;9J_h3wMpRsm1rJwQdy0^(kDrZO+jtMVa)h7?i*{U$F}gxYQp|l^f?nK**$p;CulE^RNgf4VvuaD#wVHA27jWv98QR$!SAVKvY)4iu z^Z_n*;T(=0aE?da7JIU~e<3 z+I`g~^eq3=$Fk2Gd24Jo^NZ%+7C50M7v~T91I-70$Qhw^qoVT!C*&s=`X{3Gr2?%J znT=l#`ii%-zL|+T=A!XNaeF&b9e+vd#1f)WD*8>;el_R7PmS&*N!z?H*=~02lQSAW z%5zuk+3-oUsRw;kO>$QUdA6G6w(M76GlIY_oceZ#VzED_Uaok* zuf{S>OKWA=PykkI-Nmxm1uyc{k226Ya_#tK6z3p^uLtMuKd*IOc6C>SgT5*5b-lyI zAmUr^z}DUs6LuwhoOpHcyrq>nrRYnxSyrG5F0kJadC>~@B#2nY3_7~9Es1&XQ*q;_ z$w*T`oz!~&!X=m5tzGpecRN?!pIq#67eWE0PxS*8^9gcrDi(o}1wu`3j369gUd**+b& zep&B`iu&YL(Tl9_<-FUEzg$I7^ePSCH#_FjUoh8EqX)#a5WW&iO?~Vic7<64)xRMI zNgcN2w4~y+40jL~)@Yt)7%nZ$O0&2V*Ql7kTdTkPLTkD8ogs~$Vr=u~hN`_VnkeCH z?N4UsEJ7?Vylvk9uIKq$MrnO8i-g;-f5X+IbDMMmHy8BW9<{#5z1FR<%X0h7pi-Gb z{3_g=j6lrx8w3S7q zFHLT<#Mqk*Osz#ueV%$bpqcpP>H5BF;mfpUE|}BdRN8{m=qSbxfwz!NXt~pQ-FQoL zNwc+HljnZfr>fvf3s6@Z-B(O`fQ;{XdG6&ydJ6$JaOTwWe$#fGqqW~w%a>NTUgPK2 zOWJ2r_CU2NZ?Nibw{?A3zv@{s?k&HqUp9y?a~U-r8hAB8$at=CSv$SHC7C_bYDv!R zq`WsT%a$7!Y>KEEx?cX}(l07SH(y-9J{&?vifku1bTro-WREWFnBj#}(Hf?%j9O^C z?8eLI7p8DAi+d|yzS(4ovsm;nVF7OT1{^E(CG>7r)|VUe(i7H~@yx!)LlJ{M%hSG? zy**vlJAeOxBz%WKkILsDi>oyzg9;6g3&U-78kJYqW?nmHV)&Zli=VMtvs?W#_4A{U z+1LX^DtgYS0P>BiOWQV%WWEo2wKe(8mbt?Yt5--~*FpL9RlCn|w5*J#bw8Mp^sCk=4Kim0>5FciJuMjkJ|t<`M3s4EM{i zx7rT(tkV=v9qDjbpXAY(VqNI==~5-`i{2FVhk1SFF_>ixZBwUrL8CfPdRF2#e14K> z_t0gv(ns9Z6h~Z^h+8_5n=|_UPIRxw>L@GJfRmOwd&gIw`mdxbkIy*x0cX+~X$Bve z+EU;~wPP>|OgMPer*FYt zW$UyzyZK!->6}NGruS{}JzIn>H0(qyh)YtgGj|Go=om+~YJQ-8W6&M@X|ITXuthU}N7UP! zwRO<`m6=HNi|MK-uB~#xF1H`3AgG>+%ktKBoIL%oyGxeV%>8QWdZU}JlUyf=ktYDp zNyYDmlPAWAH13?`J>piz>K%bX4v2u`gWCL2D0z&$L9&# z_I*7lcYV2c-iv2C<`;uok7U=TPdu^Y%cA^6CyyTgayIPfzLUj;ec2&ZlTKJIH+D$g z!TiO4)|3@d3|m)CYFc&t1Vza)`2>t#C~dD>*Fv|qzP`db+~QV-+dW$B8jm#Q=Ns;m zpkBbuFub?QLwELMUSmaUO5?tW-cQe_M0cI(Gm;It9xwcwvt*`ap%TYC*K*hEvqs^? z&I*Ij)ShJhvr@u~qNR&A&)6;d#lLZvc3K^2aYAaEv+bNwWirEA&6Mn_JZipq)1=5d zALbe7b+J4rODdbh7V~ohyypXDNtheutzgivlQ^dhi_XM-CuC76R1ID?DKdUeLHu}4EL}rhyx!7 z{S&n)w16l*?j2@Wb+v^d`!=V3*HZiKbs65v>G`YD=c>{<8E=n0&yl<@=$|KU(w^x* zAHDy%Kd;@Tg1gtS!LVO;=bD+(YJq=2rl)eWGv`pchds}sme#*N=IWaMdFw{IhL1q; z1ZX*T-#JHG?*15KZq|YLRrM3{)vJ-)?CP#6ZFR%@+6&J2FA7L<8miuBeCa`S0+62%meSKS;8T=1NE>DN^% zq0=<{58etCCLEg7I8-dmU#Z(*w3?%R?L)UKmeR*e)Fifn##>1p6%Nr)ZCgI4npXj< zwh_$nm)5;F-M9l#mn&R#SWoi%k8{P&^}3#u4G*U~zAm1QwSNLSuJ7G+U~UveQMF;~ z<2@n9lBRu+CL3&|Rub}n7&SMhlJ&v(>6O*-98|4N5xCLr@H9_KcvwD;isz_WD ziv0ZKsOsmQ7LA#@u9%Vxm+~{#>i&wHKFTH)TR95@Rk`Se;Q*C)icJ#rDvdrTRa=K1 z^nComi5(lOH3Bj=>+=t)+*fQQmR2XN#?|UK4XN0O H?4qCJq%w0S0qrAFns6{9 z$-<9?5!Th{-OiFv?YOlKR0ptq%;f36-b(RoOE9>M+DMMg&)B3~t3M(8Dp{VdvCYbL zhG}84En|uLo+20h<4!7z#6h)Hr*pioK?<;a4p8!-W@uSsyJ9J$pWDOw& zy=PHw@6EJR@|2CieATmVA@;}ux#gWqlrxy1l^{}%;^yzTVs%9Cc*f!d&zGvNQ6A7! z4pT`8aKf2mTlQ^aF6@)l+8mb%U~ zuhCrBow)0x(|%`Mf=(9RBm4Q%z0|E{y5ay8i(lK0&WTIV{dz2EnuasVeWHt2RI#sd z8>mLhaK>^(NJHPUoK}I=a`HEZHUR4#*P*8wtHY2&b{iI+{sCM4a zLerGiQ=U;FIKS(@1+S}ADaFS%g1VKe;V(Tarn9C?_p%0Y8DiA77@@%zA+IlqRHZ8r zRJ$8DH(p9wk;3A8?ZzV&Hc{~yZeXC! z;cjI!zL4KD>DK*LUYsy>BA4lr)$}Y|6^yxxZ*K zL1b?;ph^lwXawMUCgwO@(r*}2bt;a>a`KSocb><(ch#yLtEw~G7#|flq*Paar#>Z@ zpQaVdyoy;-u~Aly3BvcF_sNxK3<2w$hvGGL?_PB;AC;?S#v(MBd9@Cc*X-0vkxyOm zL3yTMJepAq*{sdJVYmLICZEs#l-gN0i{ZSxgs()2$wLZX$x-Ayj%i?3^Tc9c{~JC$ zzN*F0pm3+k%1c13TzTWWs`OhgR(Vh_+?sCwChkUTu+Hia*B!Z}eVa5wXsw1V6OYN2 z-K^tL8GGBbG60ns00L9DR&m<+fr@W&yjI(~)`bnSQ?fY;IvG{BR~W~iKCvKS+6M5F z3HW-Sts-8X{7FgoD(TpC{7W^5JVa*$rQC2xuIvCP6|0VPO&ChR`gJS0Jp@$zUWP=T zyZB(E%5D@ss#+%xP#vOlX;A|J z4}5~UZ?}>+w+b;K15lNAJ)Rokq-0*j;;wZ<>f>M6+cNv(N-jQnxZA00YH>sYm5IRk zIW=n+OZE?T0VrA(LbqF#qW^`h>*TCv2F7C?n80Fc6`+Ba9XA38RvZ8htZ+nZ3Ayy> z<*KqKLxubd2`1FysFoNm%|cy(Y;Cvfj1aAnWKUyrKE5yx|1 zZaugDF$VXs{e+{eaQ>Iq`#L}Oso+=2AE<-C<kA7InEg*HQbNZnX~s)z{{2->1S;t#1t??^i$KAXXp~?o1d(YZHj&P; zBhlG-I)M~SW6^%Baw3raY?Tw26(N*Kz~7V!!5t#8FhtCX0C^HnD&|FwR~`!B_9e35 zXdBR9C<8e{!AvPAfFO=NW;)zs39>+w2o_IZ0!Bd6P$6f$O#rRs^8^qGN!TnABpGj$ zKoLB)kS`QS#wU6~VrV9;1~!EaLuiiyF~2Q{`di`BKaGqOMs6$!HqxkncbOv1j`owl z;7Jh6jzuCu!E_RfPU28WTstb8&LNY@lwca0`;*Eb+HWhL76`= zCk0tBa#k=30tqx$9wdXru)RDHPr~L2LO_VmliGkwAr2@I3P3SWB9qu?VwTDzB1pjD zNhA=+ho`}zlnTZ0KP!;K3lYHWyolcj|BXBW#I^!ud^o;r9wdRo!m;{h3oO-D%#y$g zp|KK2*u-z6;6WT7E5Zg`$b#6A03N~Tg~<3UFoGqKu&lsHmY4^tho9gOi^dA)gn|$m zWCcPpo+LszZnqT}!Gq<)A>awXu~JqkZ`@X)n9UorXl#ruA|e`$5c1*3^2TNhU=QHu zF?)Di*jHGD)i?%RNc`QvF}pz6S^nQ$1tBp@3TtwMb$_?V1{{B3iP^HhnJi($LSZP^ z$RIG1CuKq3;a?+TNnyrY!3zPB@?Zt5Z+6ǁR$PH23}HwR%Y%f?23iwPJh#vQmB9wi2A%ml z7{#%u1c$@u0)@)q;_WyzE{)1&6Cpe~IGBiMu}G|7h)N>-Skyiy z=Ktr2|FInL|99{I!@D>6kFL)Aq`G{py!;=o&X6#!<7;^A%z{MdKIw+jP*G1gRj zJi#7Mv%ypUOd9@^w_yHR4jH@A^kZ;o>|i2|O9;kuU=q?P1OkoCX2JP{g{K6Q$>pr*rTp76+0XLW{{sr2X3oSt004RX BzVrY9 literal 0 HcmV?d00001 diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb new file mode 100644 index 000000000..68a1ef5e3 --- /dev/null +++ b/test/unit/repository_bazaar_test.rb @@ -0,0 +1,87 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class RepositoryBazaarTest < Test::Unit::TestCase + fixtures :projects + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + 'tmp/test/bazaar_repository' + + def setup + @project = Project.find(1) + assert @repository = Repository::Bazaar.create(:project => @project, :url => "file:///#{REPOSITORY_PATH}") + end + + if File.directory?(REPOSITORY_PATH) + def test_fetch_changesets_from_scratch + @repository.fetch_changesets + @repository.reload + + assert_equal 4, @repository.changesets.count + assert_equal 9, @repository.changes.count + assert_equal 'Initial import', @repository.changesets.find_by_revision(1).comments + end + + def test_fetch_changesets_incremental + @repository.fetch_changesets + # Remove changesets with revision > 5 + @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy) + @repository.reload + assert_equal 2, @repository.changesets.count + + @repository.fetch_changesets + assert_equal 4, @repository.changesets.count + end + + def test_entries + entries = @repository.entries + assert_equal 2, entries.size + + assert_equal 'dir', entries[0].kind + assert_equal 'directory', entries[0].name + + assert_equal 'file', entries[1].kind + assert_equal 'doc-mkdir.txt', entries[1].name + end + + def test_entries_in_subdirectory + entries = @repository.entries('directory') + assert_equal 3, entries.size + + assert_equal 'file', entries.last.kind + assert_equal 'edit.png', entries.last.name + end + + def test_cat + cat = @repository.scm.cat('directory/document.txt') + assert cat =~ /Write the contents of a file as of a given revision to standard output/ + end + + def test_annotate + annotate = @repository.scm.annotate('doc-mkdir.txt') + assert_equal 17, annotate.lines.size + assert_equal 1, annotate.revisions[0].identifier + assert_equal 'jsmith@', annotate.revisions[0].author + assert_equal 'mkdir', annotate.lines[0] + end + else + puts "Bazaar test repository NOT FOUND. Skipping tests !!!" + def test_fake; assert true end + end +end diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb index dd1c5eb6e..592eb4ffa 100644 --- a/test/unit/repository_subversion_test.rb +++ b/test/unit/repository_subversion_test.rb @@ -50,6 +50,6 @@ class RepositorySubversionTest < Test::Unit::TestCase end else puts "Subversion test repository NOT FOUND. Skipping tests !!!" - def test_fake; assert false end + def test_fake; assert true end end end From d00014221ed1a5666b2eb5952c15242613421562 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 3 Dec 2007 23:05:45 +0000 Subject: [PATCH 040/710] Changesets retrieval optimization on the activity view. Prevents additional query from being executed for each displayed changeset. git-svn-id: http://redmine.rubyforge.org/svn/trunk@952 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 658e9d232..c4d1b53fc 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -442,7 +442,7 @@ class ProjectsController < ApplicationController end if @scope.include?('changesets') - @events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to]) end if @scope.include?('messages') From b536f4c65794d8c2c5e5ae0c0a63166106bc3303 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 4 Dec 2007 18:14:09 +0000 Subject: [PATCH 041/710] Restored wiki syntax quick ref pop-up (accessible from wiki/edit). git-svn-id: http://redmine.rubyforge.org/svn/trunk@953 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/wiki/edit.rhtml | 4 +-- public/help/wiki_syntax.html | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 public/help/wiki_syntax.html diff --git a/app/views/wiki/edit.rhtml b/app/views/wiki/edit.rhtml index 96295004a..94581104f 100644 --- a/app/views/wiki/edit.rhtml +++ b/app/views/wiki/edit.rhtml @@ -5,8 +5,8 @@ <%= error_messages_for 'content' %>
      <%= l(:setting_text_formatting) %>: -<%= link_to l(:label_help), {:controller => 'help', :ctrl => 'wiki', :page => 'syntax' }, - :onclick => "window.open('#{ url_for :controller => 'help', :ctrl => 'wiki', :page => 'syntax' }', '', 'resizable=yes, location=no, width=300, height=500, menubar=no, status=no, scrollbars=yes'); return false;" %> +<%= link_to l(:label_help), '/help/wiki_syntax.html', + :onclick => "window.open('#{ url_for '/help/wiki_syntax.html' }', '', 'resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes'); return false;" %>

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


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

      diff --git a/public/help/wiki_syntax.html b/public/help/wiki_syntax.html new file mode 100644 index 000000000..f02eb62ab --- /dev/null +++ b/public/help/wiki_syntax.html @@ -0,0 +1,57 @@ + + + +Wiki formatting + + + + + +

      Wiki Syntax Quick Reference

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Font Styles
      *Strong*Strong
      _Italic_Italic
      +Underline+Underline
      -Deleted-Deleted
      ??Quote??Quote
      @Code@Code
      <pre>
       lines
       of code
      </pre>
      +
      + lines
      + of code
      +
      +
      Lists
      * Item 1
      * Item 2
      • Item 1
      • Item 2
      # Item 1
      # Item 2
      1. Item 1
      2. Item 2
      Headings
      h1. Title 1

      Title 1

      h2. Title 2

      Title 2

      h3. Title 3

      Title 3

      Links
      http://foo.barhttp://foo.bar
      [[Wiki page]]Wiki page
      Issue #12Issue #12
      Revision r43Revision r43
      Inline images
      !image_url!
      !attached_image!
      + + + \ No newline at end of file From 355289fa677f6a9a26e252e640861497a34cada6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 5 Dec 2007 19:21:25 +0000 Subject: [PATCH 042/710] Roadmap progress bars now differentiate the progress due to closed issues from the open issues progress (2 different colors). git-svn-id: http://redmine.rubyforge.org/svn/trunk@954 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 10 +++++++--- app/models/version.rb | 8 ++++++++ app/views/projects/roadmap.rhtml | 2 +- public/stylesheets/application.css | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f4746c627..37c8b1c9b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -319,13 +319,17 @@ module ApplicationHelper link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") end - def progress_bar(pct, options={}) + def progress_bar(pcts, options={}) + pcts = [pcts, pcts] unless pcts.is_a?(Array) + pcts[1] = pcts[1] - pcts[0] + pcts << (100 - pcts[1] - pcts[0]) width = options[:width] || '100px;' legend = options[:legend] || '' content_tag('table', content_tag('tr', - (pct > 0 ? content_tag('td', '', :width => "#{pct.floor}%;", :class => 'closed') : '') + - (pct < 100 ? content_tag('td', '', :width => "#{100-pct.floor}%;", :class => 'open') : '') + (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') + + (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') + + (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '') ), :class => 'progress', :style => "width: #{width};") + content_tag('p', legend, :class => 'pourcent') end diff --git a/app/models/version.rb b/app/models/version.rb index 0547dbabc..266346c7b 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -49,6 +49,14 @@ class Version < ActiveRecord::Base end end + def closed_pourcent + if fixed_issues.count == 0 + 0 + else + closed_issues_count * 100.0 / fixed_issues.count + end + end + # Returns true if the version is overdue: due date reached and some open issues def overdue? effective_date && (effective_date < Date.today) && (open_issues_count > 0) diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml index c2f3cbf34..7c3544a53 100644 --- a/app/views/projects/roadmap.rhtml +++ b/app/views/projects/roadmap.rhtml @@ -16,7 +16,7 @@

      <%=h version.description %>

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

      <%= link_to(version.closed_issues_count, :controller => 'issues', :action => 'index', :project_id => @project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %> <%= lwr(:label_closed_issues, version.closed_issues_count) %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 1059d960b..a10524dbb 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -262,6 +262,7 @@ table.progress { table.progress td { height: 0.9em; } table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } +table.progress td.done { background: #DEF0DE none repeat scroll 0%; } table.progress td.open { background: #FFF none repeat scroll 0%; } p.pourcent {font-size: 80%;} p.progress-info {clear: left; font-style: italic; font-size: 80%;} From 3c44aac1f50cfee83e87baa21d758620749c01e0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 10:26:07 +0000 Subject: [PATCH 043/710] Added version details view accessible from the roadmap. git-svn-id: http://redmine.rubyforge.org/svn/trunk@955 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/versions_controller.rb | 10 ++++ app/helpers/versions_helper.rb | 28 +++++++++ app/models/issue_category.rb | 4 ++ app/models/tracker.rb | 4 ++ app/views/projects/roadmap.rhtml | 66 ++++++++------------- app/views/projects/settings/_versions.rhtml | 2 +- app/views/versions/_issue_counts.rhtml | 35 +++++++++++ app/views/versions/_overview.rhtml | 24 ++++++++ app/views/versions/show.rhtml | 14 +++++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh.yml | 1 + lib/redmine.rb | 1 + public/stylesheets/application.css | 2 + test/functional/versions_controller_test.rb | 42 +++++++++++++ 31 files changed, 208 insertions(+), 43 deletions(-) create mode 100644 app/views/versions/_issue_counts.rhtml create mode 100644 app/views/versions/_overview.rhtml create mode 100644 app/views/versions/show.rhtml create mode 100644 test/functional/versions_controller_test.rb diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index 4e9016ebf..1365c97e8 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -21,6 +21,9 @@ class VersionsController < ApplicationController cache_sweeper :version_sweeper, :only => [ :edit, :destroy ] + def show + end + def edit if request.post? and @version.update_attributes(params[:version]) flash[:notice] = l(:notice_successful_update) @@ -49,6 +52,13 @@ class VersionsController < ApplicationController flash[:notice] = l(:notice_successful_delete) redirect_to :controller => 'projects', :action => 'list_files', :id => @project end + + def status_by + respond_to do |format| + format.html { render :action => 'show' } + format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} } + end + end private def find_project diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb index 452f4d7fb..0fcc6407c 100644 --- a/app/helpers/versions_helper.rb +++ b/app/helpers/versions_helper.rb @@ -16,4 +16,32 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module VersionsHelper + + STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to) + + def render_issue_status_by(version, criteria) + criteria ||= 'category' + raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria) + + h = Hash.new {|k,v| k[v] = [0, 0]} + begin + # Total issue count + Issue.count(:group => criteria, + :conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s} + # Open issues count + Issue.count(:group => criteria, + :include => :status, + :conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s} + rescue ActiveRecord::RecordNotFound + # When grouping by an association, Rails throws this exception if there's no result (bug) + end + counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}} + max = counts.collect {|c| c[:total]}.max + + render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max} + end + + def status_by_options_for_select(value) + options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value) + end end diff --git a/app/models/issue_category.rb b/app/models/issue_category.rb index 9478504f1..51baeb419 100644 --- a/app/models/issue_category.rb +++ b/app/models/issue_category.rb @@ -35,5 +35,9 @@ class IssueCategory < ActiveRecord::Base destroy_without_reassign end + def <=>(category) + name <=> category.name + end + def to_s; name end end diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 6de2a098c..8d8647747 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -29,6 +29,10 @@ class Tracker < ActiveRecord::Base def to_s; name end + def <=>(tracker) + name <=> tracker.name + end + def self.all find(:all, :order => 'position') end diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml index 7c3544a53..daf7639fc 100644 --- a/app/views/projects/roadmap.rhtml +++ b/app/views/projects/roadmap.rhtml @@ -5,48 +5,28 @@ <% end %> <% @versions.each do |version| %> -

      <%= version.name %>

      - <% if version.completed? %> -

      <%= format_date(version.effective_date) %>

      - <% elsif version.overdue? %> -

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

      - <% elsif version.effective_date %> -

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

      - <% end %> -

      <%=h version.description %>

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

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

      - <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> - <% issues = version.fixed_issues.find(:all, - :include => [:status, :tracker], - :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], - :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty? - issues ||= [] - %> -
        - <% if issues.size > 0 %> - <% issues.each do |issue| %> -
      • - <%= link = link_to_issue(issue) - issue.status.is_closed? ? content_tag("del", link) : link %>: <%=h issue.subject %> - <%= content_tag "em", "(#{l(:label_closed_issues)})" if issue.status.is_closed? %> -
      • - <% end %> - <% end %> -
      - <% else %> -

      <%= l(:label_roadmap_no_issues) %>

      - <% end %> + <%= tag 'a', :name => version.name %> +

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

      + <%= render :partial => 'versions/overview', :locals => {:version => version} %> + <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> + + <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], + :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty? + issues ||= [] + %> +
        + <% if issues.size > 0 %> + <% issues.each do |issue| %> +
      • + <%= link = link_to_issue(issue) + issue.status.is_closed? ? content_tag("del", link) : link %>: <%=h issue.subject %> + <%= content_tag "em", "(#{l(:label_closed_issues)})" if issue.status.is_closed? %> +
      • + <% end %> + <% end %> +
      <% end %> <% content_for :sidebar do %> @@ -66,3 +46,5 @@ <%= link_to version.name, :anchor => version.name %>
      <% end %> <% end %> + +<% set_html_title l(:label_roadmap) %> diff --git a/app/views/projects/settings/_versions.rhtml b/app/views/projects/settings/_versions.rhtml index 63c408b0d..7329c7f3b 100644 --- a/app/views/projects/settings/_versions.rhtml +++ b/app/views/projects/settings/_versions.rhtml @@ -11,7 +11,7 @@
    <%=h version.name %><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %> <%= format_date(version.effective_date) %> <%=h version.description %> <%= link_to(version.wiki_page_title, :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %>
    + <% counts.each do |count| %> + + + + + <% end %> +
    + <%= link_to count[:group], {:controller => 'issues', + :action => 'index', + :project_id => version.project, + :set_filter => 1, + :fixed_version_id => version, + "#{criteria}_id" => count[:group]} %> + + <%= progress_bar((count[:closed].to_f / count[:total])*100, + :legend => "#{count[:closed]}/#{count[:total]}", + :width => "#{(count[:total].to_f / max * 200).floor}px;") %> +
    +<% end %> + + diff --git a/app/views/versions/_overview.rhtml b/app/views/versions/_overview.rhtml new file mode 100644 index 000000000..d3aa6b18f --- /dev/null +++ b/app/views/versions/_overview.rhtml @@ -0,0 +1,24 @@ +<% if version.completed? %> +

    <%= format_date(version.effective_date) %>

    +<% elsif version.overdue? %> +

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

    +<% elsif version.effective_date %> +

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

    +<% end %> + +

    <%=h version.description %>

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

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

    +<% else %> +

    <%= l(:label_roadmap_no_issues) %>

    +<% end %> diff --git a/app/views/versions/show.rhtml b/app/views/versions/show.rhtml new file mode 100644 index 000000000..d8e2d243d --- /dev/null +++ b/app/views/versions/show.rhtml @@ -0,0 +1,14 @@ +
    +<%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %> +
    + +

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

    + +
    +<%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %> +
    + +<%= render :partial => 'versions/overview', :locals => {:version => @version} %> +<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %> + +<% set_html_title h(@version.name) %> diff --git a/lang/bg.yml b/lang/bg.yml index 5f2dc6a8d..590952681 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/cs.yml b/lang/cs.yml index 49697dda8..816f9b92e 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/de.yml b/lang/de.yml index 870f45014..046ed9994 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -548,3 +548,4 @@ field_time_zone: Zeitzone text_caracters_minimum: Muss mindestens %d Zeichen lang sein. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate +label_issues_by: Issues by %s diff --git a/lang/en.yml b/lang/en.yml index cdecb6693..104e7fe6e 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -210,6 +210,7 @@ label_issue: Issue label_issue_new: New issue label_issue_plural: Issues label_issue_view_all: View all issues +label_issues_by: Issues by %s label_document: Document label_document_new: New document label_document_plural: Documents diff --git a/lang/es.yml b/lang/es.yml index 5f25de7cf..d806d066e 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -551,3 +551,4 @@ notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aproba setting_time_format: Formato de hora setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate +label_issues_by: Issues by %s diff --git a/lang/fr.yml b/lang/fr.yml index 63f367f06..36ccc463f 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -210,6 +210,7 @@ label_issue: Demande label_issue_new: Nouvelle demande label_issue_plural: Demandes label_issue_view_all: Voir toutes les demandes +label_issues_by: Demandes par %s label_document: Document label_document_new: Nouveau document label_document_plural: Documents diff --git a/lang/he.yml b/lang/he.yml index 4ebbecf7a..6be1a4c77 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/it.yml b/lang/it.yml index fa13513cb..8a3e954f4 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/ja.yml b/lang/ja.yml index d2f7d579f..1ecfb1ae9 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/ko.yml b/lang/ko.yml index dd50ce2bc..ef081e622 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/nl.yml b/lang/nl.yml index 895feb5ed..24a343eb3 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/pl.yml b/lang/pl.yml index 381dc5e26..ff5f8d828 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -548,3 +548,4 @@ field_time_zone: Strefa czasowa 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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 4c4d862d3..8c903edd4 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/pt.yml b/lang/pt.yml index 1aabc8ac7..10de07b5f 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/ro.yml b/lang/ro.yml index 0c10c7727..f7d3acd56 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -548,3 +548,4 @@ 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 diff --git a/lang/ru.yml b/lang/ru.yml index c6e27ca9f..cad357c0c 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -548,3 +548,4 @@ field_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 diff --git a/lang/sr.yml b/lang/sr.yml index 42aa3cc3e..f9008d890 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/sv.yml b/lang/sv.yml index 7cbcc7a5f..a4f55a17a 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/zh.yml b/lang/zh.yml index ad18cecc3..18fe3fda1 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -551,3 +551,4 @@ 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 diff --git a/lib/redmine.rb b/lib/redmine.rb index 32239ce59..1f1053438 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -27,6 +27,7 @@ Redmine::AccessControl.map do |map| # Issues map.permission :view_issues, {:projects => [:changelog, :roadmap], :issues => [:index, :changes, :show, :context_menu], + :versions => [:show, :status_by], :queries => :index, :reports => :issue_report}, :public => true map.permission :add_issues, {:projects => :add_issue} diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index a10524dbb..f05fb9d91 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -267,6 +267,8 @@ table.progress td.open { background: #FFF none repeat scroll 0%; } p.pourcent {font-size: 80%;} p.progress-info {clear: left; font-style: italic; font-size: 80%;} +div#status_by { margin-left: 16px; margin-bottom: 16px; } + /***** Tabs *****/ #content .tabs{height: 2.6em;} #content .tabs ul{margin:0;} diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb new file mode 100644 index 000000000..e8327938a --- /dev/null +++ b/test/functional/versions_controller_test.rb @@ -0,0 +1,42 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'versions_controller' + +# Re-raise errors caught by the controller. +class VersionsController; def rescue_action(e) raise e end; end + +class VersionsControllerTest < Test::Unit::TestCase + fixtures :projects, :versions, :users, :roles, :members, :enabled_modules + + def setup + @controller = VersionsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:version) + + assert_tag :tag => 'h2', :content => /1.0/ + end +end From a79cf8d574d1bdad8483586014019ed9ee2b3bdf Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 10:27:52 +0000 Subject: [PATCH 044/710] Fixed a parenthesis warning in news/show. git-svn-id: http://redmine.rubyforge.org/svn/trunk@956 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/news/show.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index 2b51c1fee..32a24e85e 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -30,7 +30,7 @@
    <%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
    - <%= simple_format(auto_link(h comment.comments))%> + <%= simple_format(auto_link(h(comment.comments)))%> <% end if @news.comments_count > 0 %>
    From 5e38bd93634b9135e5f294ec1823e8a6b04c3b64 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 11:19:30 +0000 Subject: [PATCH 045/710] Performance improvement on workflow setup screen. git-svn-id: http://redmine.rubyforge.org/svn/trunk@957 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/roles_controller.rb | 2 +- app/views/roles/workflow.rhtml | 14 ++++++-------- ...d_role_tracker_old_status_index_to_workflows.rb | 9 +++++++++ 3 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 3b5766aaf..a8a31cd4d 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -94,7 +94,7 @@ class RolesController < ApplicationController end @roles = Role.find(:all, :order => 'builtin, position') @trackers = Tracker.find(:all, :order => 'position') - @statuses = IssueStatus.find(:all, :include => :workflows, :order => 'position') + @statuses = IssueStatus.find(:all, :order => 'position') end def report diff --git a/app/views/roles/workflow.rhtml b/app/views/roles/workflow.rhtml index 2bc2abd51..0a435744f 100644 --- a/app/views/roles/workflow.rhtml +++ b/app/views/roles/workflow.rhtml @@ -35,18 +35,16 @@ <% for old_status in @statuses %> - <%= old_status.name %> - <% for new_status in @statuses %> + <%= old_status.name %> + <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to(@role, @tracker).collect(&:id) -%> + <% for new_status in @statuses -%> - - checked="checked"<%end%> - > + <%= 'checked="checked"' if new_status_ids_allowed.include? new_status.id %>> - <% end %> - + <% end -%> <% end %> diff --git a/db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb b/db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb new file mode 100644 index 000000000..523cf091b --- /dev/null +++ b/db/migrate/085_add_role_tracker_old_status_index_to_workflows.rb @@ -0,0 +1,9 @@ +class AddRoleTrackerOldStatusIndexToWorkflows < ActiveRecord::Migration + def self.up + add_index :workflows, [:role_id, :tracker_id, :old_status_id], :name => :workflows_role_tracker_old_status + end + + def self.down + remove_index :workflows, :name => :workflows_role_tracker_old_status + end +end From a7c2b63fb61082bb54afe49880ebe0c21566ba3f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 13:35:38 +0000 Subject: [PATCH 046/710] Transaction and performance improvement on workflow copy when creating a new tracker. git-svn-id: http://redmine.rubyforge.org/svn/trunk@958 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/trackers_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/trackers_controller.rb b/app/controllers/trackers_controller.rb index 0fc91b527..46edea548 100644 --- a/app/controllers/trackers_controller.rb +++ b/app/controllers/trackers_controller.rb @@ -37,8 +37,10 @@ class TrackersController < ApplicationController if request.post? and @tracker.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) - copy_from.workflows.each do |w| - @tracker.workflows << w.clone + Workflow.transaction do + copy_from.workflows.find(:all, :include => [:role, :old_status, :new_status]).each do |w| + Workflow.create(:tracker_id => @tracker.id, :role => w.role, :old_status => w.old_status, :new_status => w.new_status) + end end end flash[:notice] = l(:notice_successful_create) From 36c0ab196ca772c123e7327b6829b8387f9e0aeb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 14:43:07 +0000 Subject: [PATCH 047/710] Added Traditional Chinese translation (by Shortie Lo). git-svn-id: http://redmine.rubyforge.org/svn/trunk@959 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 2 +- lang/zh-tw.yml | 551 ++++++++++++++++++ lang/zh.yml | 2 +- .../calendar/lang/calendar-zh-tw.js | 127 ++++ 4 files changed, 680 insertions(+), 2 deletions(-) create mode 100644 lang/zh-tw.yml create mode 100644 public/javascripts/calendar/lang/calendar-zh-tw.js diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 37c8b1c9b..8deed9000 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -299,7 +299,7 @@ module ApplicationHelper def lang_options_for_select(blank=true) (blank ? [["(auto)", ""]] : []) + - GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first } + GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } end def label_tag_for(name, option_tags = nil, options = {}) diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml new file mode 100644 index 000000000..7d9cdc957 --- /dev/null +++ b/lang/zh-tw.yml @@ -0,0 +1,551 @@ +_gloc_rule_default: '|n| n==1 ? "" : "_plural" ' + +actionview_datehelper_select_day_prefix: +actionview_datehelper_select_month_names: 一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 +actionview_datehelper_select_month_names_abbr: 一月,二月,三月,四月,五月,六月,七月,八月,九月,十月,十一月,十二月 +actionview_datehelper_select_month_prefix: +actionview_datehelper_select_year_prefix: +actionview_datehelper_time_in_words_day: 1 日 +actionview_datehelper_time_in_words_day_plural: %d 日 +actionview_datehelper_time_in_words_hour_about: 約 1 小時 +actionview_datehelper_time_in_words_hour_about_plural: 約 %d 小時 +actionview_datehelper_time_in_words_hour_about_single: 約 1 小時 +actionview_datehelper_time_in_words_minute: 1 分鐘 +actionview_datehelper_time_in_words_minute_half: 半分鐘 +actionview_datehelper_time_in_words_minute_less_than: 小於 1 分鐘 +actionview_datehelper_time_in_words_minute_plural: %d 分鐘 +actionview_datehelper_time_in_words_minute_single: 1 分鐘 +actionview_datehelper_time_in_words_second_less_than: 小於 1 秒 +actionview_datehelper_time_in_words_second_less_than_plural: 小於 %d 秒 +actionview_instancetag_blank_option: 請選擇 + +activerecord_error_inclusion: is not included in the list +activerecord_error_exclusion: is reserved +activerecord_error_invalid: is invalid +activerecord_error_confirmation: doesn't match confirmation +activerecord_error_accepted: must be accepted +activerecord_error_empty: 不可為空值 +activerecord_error_blank: 不可為空白 +activerecord_error_too_long: 長度過長 +activerecord_error_too_short: 長度太短 +activerecord_error_wrong_length: is the wrong length +activerecord_error_taken: has already been taken +activerecord_error_not_a_number: is not a number +activerecord_error_not_a_date: is not a valid date +activerecord_error_greater_than_start_date: must be greater than start date +activerecord_error_not_same_project: doesn't belong to the same project +activerecord_error_circular_dependency: This relation would create a circular dependency + +general_fmt_age: %d 年 +general_fmt_age_plural: %d yrs +general_fmt_date: %%m/%%d/%%Y +general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p +general_fmt_datetime_short: %%b %%d, %%I:%%M %%p +general_fmt_time: %%I:%%M %%p +general_text_No: 'No' +general_text_Yes: 'Yes' +general_text_no: 'no' +general_text_yes: 'yes' +general_lang_name: 'Traditional Chinese (繁體中文)' +general_csv_separator: ',' +general_csv_encoding: ISO-8859-1 +general_pdf_encoding: big5 +general_day_names: 星期一,星期二,星期三,星期四,星期五,星期六,星期日 +general_first_day_of_week: '7' + +notice_account_updated: 帳戶更新資訊已儲存 +notice_account_invalid_creditentials: 帳戶或密碼不正確 +notice_account_password_updated: 帳戶新密碼已儲存 +notice_account_wrong_password: 密碼不正確 +notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you. +notice_account_unknown_email: Unknown user. +notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. +notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you. +notice_account_activated: Your account has been activated. You can now log in. +notice_successful_create: Successful creation. +notice_successful_update: Successful update. +notice_successful_delete: Successful deletion. +notice_successful_connection: Successful connection. +notice_file_not_found: The page you were trying to access doesn't exist or has been removed. +notice_locking_conflict: Data have been updated by another user. +notice_scm_error: Entry and/or revision doesn't exist in the repository. +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_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." +notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +notice_account_pending: "Your account was created and is now pending administrator approval." + +mail_subject_lost_password: Your Redmine password +mail_body_lost_password: 'To change your Redmine password, click on the following link:' +mail_subject_register: Redmine account activation +mail_body_register: 'To activate your Redmine account, click on the following link:' +mail_body_account_information_external: You can use your "%s" account to log into Redmine. +mail_body_account_information: Your Redmine account information +mail_subject_account_activation_request: Redmine account activation request +mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' + +gui_validation_error: 1 個錯誤 +gui_validation_error_plural: %d 個錯誤 + +field_name: 名稱 +field_description: 概述 +field_summary: 摘要 +field_is_required: 必填 +field_firstname: 名字 +field_lastname: 姓氏 +field_mail: 電子郵件 +field_filename: 檔案名稱 +field_filesize: 大小 +field_downloads: 下載次數 +field_author: 作者 +field_created_on: 建立日期 +field_updated_on: 更新 +field_field_format: 格式 +field_is_for_all: 給所有專案 +field_possible_values: Possible values +field_regexp: 正規表示式 +field_min_length: 最小長度 +field_max_length: 最大長度 +field_value: 值 +field_category: 分類 +field_title: 標題 +field_project: 專案 +field_issue: 項目 +field_status: 狀態 +field_notes: 筆記 +field_is_closed: 項目結束 +field_is_default: 預設值 +field_tracker: 追蹤標籤 +field_subject: 主旨 +field_due_date: 完成日期 +field_assigned_to: 分派給 +field_priority: 重要性 +field_fixed_version: Fixed version +field_user: 用戶 +field_role: 角色 +field_homepage: 網站首頁 +field_is_public: 公開 +field_parent: 父專案 +field_is_in_chlog: Issues displayed in changelog +field_is_in_roadmap: Issues displayed in roadmap +field_login: 帳戶名稱 +field_mail_notification: 電子郵件提醒選項 +field_admin: 管理者 +field_last_login_on: 最近連線日期 +field_language: 語系 +field_effective_date: 日期 +field_password: 目前密碼 +field_new_password: 新密碼 +field_password_confirmation: 確認新密碼 +field_version: 版本 +field_type: Type +field_host: Host +field_port: 連接埠 +field_account: 帳戶 +field_base_dn: Base DN +field_attr_login: 登入屬性 +field_attr_firstname: 名字屬性 +field_attr_lastname: Lastname attribute +field_attr_mail: Email attribute +field_onthefly: On-the-fly user creation +field_start_date: 開始日期 +field_done_ratio: %% 已完成 +field_auth_source: 認證模式 +field_hide_mail: 隱藏我的電子郵件 +field_comments: 註解 +field_url: URL +field_start_page: Start page +field_subproject: 子專案 +field_hours: 小時 +field_activity: Activity +field_spent_on: 日期 +field_identifier: 代碼 +field_is_filter: Used as a filter +field_issue_to_id: Related issue +field_delay: 逾期 +field_assignable: Issues can be assigned to this role +field_redirect_existing_links: Redirect existing links +field_estimated_hours: 預估工時 +field_column_names: Columns +field_time_zone: 時區 + +setting_app_title: 標題 +setting_app_subtitle: 副標題 +setting_welcome_text: 歡迎詞 +setting_default_language: 預設語系 +setting_login_required: 需要驗證 +setting_self_registration: 註冊選項 +setting_attachment_max_size: 附件大小限制 +setting_issues_export_limit: 項目匯出限制 +setting_mail_from: 寄件者電子郵件 +setting_bcc_recipients: 使用密件副本 (BCC) +setting_host_name: 主機名稱 +setting_text_formatting: 文字格式 +setting_wiki_compression: 壓縮 Wiki 歷史文章 +setting_feeds_limit: Feed content limit +setting_autofetch_changesets: Autofetch commits +setting_sys_api_enabled: Enable WS for repository management +setting_commit_ref_keywords: Referencing keywords +setting_commit_fix_keywords: Fixing keywords +setting_autologin: 自動登入 +setting_date_format: 日期格式 +setting_time_format: 時間格式 +setting_cross_project_issue_relations: Allow cross-project issue relations +setting_issue_list_default_columns: 預設顯示於項目清單的欄位 +setting_repositories_encodings: Repositories encodings +setting_emails_footer: 電子郵件附帶說明 +setting_protocol: 協定 + +label_user: 用戶 +label_user_plural: 用戶清單 +label_user_new: 建立新的帳戶 +label_project: 專案 +label_project_new: 建立新的專案 +label_project_plural: 專案清單 +label_project_all: 全部的專案 +label_project_latest: 最近的專案 +label_issue: 項目 +label_issue_new: 建立新的項目 +label_issue_plural: 項目清單 +label_issue_view_all: 檢視所有項目 +label_issues_by: Issues by %s +label_document: 文件 +label_document_new: 建立新的文件 +label_document_plural: 文件 +label_role: 角色 +label_role_plural: 角色 +label_role_new: 建立新角色 +label_role_and_permissions: 角色與權限 +label_member: 成員 +label_member_new: 建立新的成員 +label_member_plural: 成員 +label_tracker: 追蹤標籤 +label_tracker_plural: 追蹤標籤清單 +label_tracker_new: 建立新的追蹤標籤 +label_workflow: 流程 +label_issue_status: 項目狀態 +label_issue_status_plural: 項目狀態清單 +label_issue_status_new: 建立新的狀態 +label_issue_category: 項目分類 +label_issue_category_plural: 項目分類清單 +label_issue_category_new: 建立新的分類 +label_custom_field: 自訂欄位 +label_custom_field_plural: 自訂欄位清單 +label_custom_field_new: 建立新的自訂欄位 +label_enumerations: 列舉值清單 +label_enumeration_new: 建立新的列舉值 +label_information: 資訊 +label_information_plural: 資訊 +label_please_login: 請先登入 +label_register: 註冊 +label_password_lost: 遺失密碼 +label_home: 網站首頁 +label_my_page: 帳戶首頁 +label_my_account: 我的帳戶 +label_my_projects: 我的專案 +label_administration: 網站管理 +label_login: 登入 +label_logout: 登出 +label_help: 說明 +label_reported_issues: 我通報的項目 +label_assigned_to_me_issues: 分派給我的項目 +label_last_login: 最近一次連線 +label_last_updates: 最近更新 +label_last_updates_plural: %d 個最近更新 +label_registered_on: 註冊於 +label_activity: 活動 +label_new: 建立新的... +label_logged_as: 目前登入 +label_environment: 環境 +label_authentication: 認證 +label_auth_source: 認證模式 +label_auth_source_new: 建立新認證模式 +label_auth_source_plural: 認證模式清單 +label_subproject_plural: 子專案 +label_min_max_length: 最小 - 最大 長度 +label_list: 清單 +label_date: 日期 +label_integer: 整數 +label_float: 福點數 +label_boolean: 布林 +label_string: 文字 +label_text: 長文字 +label_attribute: 屬性 +label_attribute_plural: 屬性 +label_download: %d 個下載 +label_download_plural: %d 個下載 +label_no_data: 沒有任何資料可供顯示 +label_change_status: 變更狀態 +label_history: 歷史 +label_attachment: 檔案 +label_attachment_new: 建立新的檔案 +label_attachment_delete: 刪除檔案 +label_attachment_plural: 檔案 +label_report: 報告 +label_report_plural: 報告 +label_news: 新聞 +label_news_new: 建立新的新聞 +label_news_plural: 新聞 +label_news_latest: 最近新聞 +label_news_view_all: 檢視所有新聞 +label_change_log: 變更記錄 +label_settings: 設定 +label_overview: 概觀 +label_version: 版本 +label_version_new: 建立新的版本 +label_version_plural: 版本 +label_confirmation: 確認 +label_export_to: 匯出至 +label_read: Read... +label_public_projects: 公開專案 +label_open_issues: 進行中 +label_open_issues_plural: 進行中 +label_closed_issues: 已結束 +label_closed_issues_plural: 已結束 +label_total: Total +label_permissions: 權限 +label_current_status: Current status +label_new_statuses_allowed: New statuses allowed +label_all: 全部 +label_none: 空值 +label_nobody: nobody +label_next: Next +label_previous: Previous +label_used_by: Used by +label_details: Details +label_add_note: 加入一個新筆記 +label_per_page: 每頁 +label_calendar: 日曆 +label_months_from: months from +label_gantt: 甘特圖 +label_internal: Internal +label_last_changes: 最近 %d 個變更 +label_change_view_all: 檢視所有變更 +label_personalize_page: 自訂版面 +label_comment: 註解 +label_comment_plural: 註解 +label_comment_add: 加入新註解 +label_comment_added: 新註解已加入 +label_comment_delete: 刪除註解 +label_query: 自訂查詢 +label_query_plural: 自訂查詢 +label_query_new: 建立新的查詢 +label_filter_add: 加入新篩選條件 +label_filter_plural: 篩選條件 +label_equals: 等於 +label_not_equals: 不等於 +label_in_less_than: 在小於 +label_in_more_than: 在大於 +label_in: 在 +label_today: 今天 +label_this_week: 本週 +label_less_than_ago: 小於幾天之前 +label_more_than_ago: 大於幾天之前 +label_ago: 天以前 +label_contains: 包含 +label_not_contains: 不包含 +label_day_plural: 天 +label_repository: 版本控管 +label_browse: 瀏覽 +label_modification: %d 變更 +label_modification_plural: %d 變更 +label_revision: 版次 +label_revision_plural: 版次清單 +label_added: 已新增 +label_modified: 已修改 +label_deleted: 已刪除 +label_latest_revision: 最新版次 +label_latest_revision_plural: 最近版次清單 +label_view_revisions: 檢視版次清單 +label_max_size: 最大長度 +label_on: '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: No issues for this version +label_search: 搜尋 +label_result_plural: 結果 +label_all_words: All words +label_wiki: Wiki +label_wiki_edit: Wiki 編輯 +label_wiki_edit_plural: Wiki 編輯 +label_wiki_page: Wiki 網頁 +label_wiki_page_plural: Wiki 網頁 +label_index_by_title: 依標題索引 +label_index_by_date: 依日期索引 +label_current_version: 現行版本 +label_preview: 預覽 +label_feed_plural: Feeds +label_changes_details: Details of all changes +label_issue_tracking: 項目追蹤 +label_spent_time: 耗用時間 +label_f_hour: %.2f hour +label_f_hour_plural: %.2f hours +label_time_tracking: Time tracking +label_change_plural: Changes +label_statistics: Statistics +label_commits_per_month: Commits per month +label_commits_per_author: Commits per author +label_view_diff: View differences +label_diff_inline: inline +label_diff_side_by_side: side by side +label_options: 選項清單 +label_copy_workflow_from: 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: 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: 關閉 +label_show_completed_versions: 顯示已完成的版本 +label_me: 我自己 +label_board: 論壇 +label_board_new: 建立新論壇 +label_board_plural: 論壇 +label_topic_plural: 討論主題 +label_message_plural: 訊息 +label_message_last: 上一封訊息 +label_message_new: 建立新的訊息 +label_reply_plural: 回應 +label_send_information: 寄送帳戶資訊電子郵件給用戶 +label_year: 年 +label_month: 月 +label_week: 週 +label_date_from: 開始 +label_date_to: 結束 +label_language_based: 依用戶之語系決定 +label_sort_by: 按 %s 排序 +label_send_test_email: 寄送測試郵件 +label_feeds_access_key_created_on: RSS 存取鍵建立於 %s 之前 +label_module_plural: 模組 +label_added_time_by: Added by %s %s ago +label_updated_time: Updated %s ago +label_jump_to_a_project: 選擇欲前往的專案... +label_file_plural: 檔案清單 +label_changeset_plural: Changesets +label_default_columns: 預設欄位清單 +label_no_change_option: (No change) +label_bulk_edit_selected_issues: Bulk edit selected issues +label_theme: 畫面主題 +label_default: Default +label_search_titles_only: 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: 自動啟用帳戶 + +button_login: 登入 +button_submit: 送出 +button_save: 儲存 +button_check_all: 全選 +button_uncheck_all: 全不選 +button_delete: 刪除 +button_create: 建立 +button_test: 測試 +button_edit: 編輯 +button_add: 新增 +button_change: 修改 +button_apply: 套用 +button_clear: 清除 +button_lock: 鎖定 +button_unlock: 解除鎖定 +button_download: 下載 +button_list: List +button_view: 檢視 +button_move: 移動 +button_back: Back +button_cancel: 取消 +button_activate: 啟用 +button_sort: 排序 +button_log_time: 記錄時間 +button_rollback: 還原至此版本 +button_watch: 觀察 +button_unwatch: 取消觀察 +button_reply: 回應 +button_archive: 歸檔 +button_unarchive: 取消歸檔 +button_reset: 回復 +button_rename: 重新命名 +button_change_password: 變更密碼 +button_copy: 複製 +button_annotate: 加注 + +status_active: 活動中 +status_registered: 註冊完成 +status_locked: 鎖定中 + +text_select_mail_notifications: 選擇欲寄送提醒通知郵件之動作 +text_regexp_info: eg. ^[A-Z0-9]+$ +text_min_max_length_info: 0 means no restriction +text_project_destroy_confirmation: Are you sure you want to delete this project and all related data ? +text_workflow_edit: Select a role and a tracker to edit the workflow +text_are_you_sure: Are you sure ? +text_journal_changed: changed from %s to %s +text_journal_set_to: set to %s +text_journal_deleted: deleted +text_tip_task_begin_day: task beginning this day +text_tip_task_end_day: task ending this day +text_tip_task_begin_end_day: task beginning and ending this day +text_project_identifier_info: '只允許小寫英文字母(a-z)、阿拉伯數字與連字符號(-)。
    儲存後,代碼不可再被更改。' +text_caracters_maximum: %d characters maximum. +text_caracters_minimum: Must be at least %d characters long. +text_length_between: Length between %d and %d characters. +text_tracker_no_workflow: No workflow defined for this tracker +text_unallowed_characters: Unallowed characters +text_comma_separated: Multiple values allowed (comma separated). +text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages +text_issue_added: 已通報 %s 個項目 +text_issue_updated: 已更新 %s 個項目 +text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? +text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? +text_issue_category_destroy_assignments: Remove category assignments +text_issue_category_reassign_to: Reassign issues to this category +text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." + +default_role_manager: 管理人員 +default_role_developper: 開發人員 +default_role_reporter: 報告人員 +default_tracker_bug: 臭蟲 +default_tracker_feature: 功能 +default_tracker_support: 支援 +default_issue_status_new: 新建立 +default_issue_status_assigned: 已指派 +default_issue_status_resolved: 已解決 +default_issue_status_feedback: 回應 +default_issue_status_closed: 已結束 +default_issue_status_rejected: 已拒絕 +default_doc_category_user: 使用手冊 +default_doc_category_tech: 技術文件 +default_priority_low: 低 +default_priority_normal: 正常 +default_priority_high: 高 +default_priority_urgent: 急 +default_priority_immediate: 特急 +default_activity_design: 設計 +default_activity_development: 開發 + +enumeration_issue_priorities: 項目重要性 +enumeration_doc_categories: 文件分類 +enumeration_activities: 活動 (time tracking) diff --git a/lang/zh.yml b/lang/zh.yml index 18fe3fda1..de4ae1ece 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -49,7 +49,7 @@ general_text_No: '否' general_text_Yes: '是' general_text_no: '否' general_text_yes: '是' -general_lang_name: 'Chinese (简体中文)' +general_lang_name: 'Simplified Chinese (简体中文)' general_csv_separator: ',' general_csv_encoding: gb2312 general_pdf_encoding: Big5 diff --git a/public/javascripts/calendar/lang/calendar-zh-tw.js b/public/javascripts/calendar/lang/calendar-zh-tw.js new file mode 100644 index 000000000..c48d25b0e --- /dev/null +++ b/public/javascripts/calendar/lang/calendar-zh-tw.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Mihai Bazon, +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("星期日", + "星期一", + "星期二", + "星期三", + "星期四", + "星期五", + "星期六", + "星期日"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("日", + "一", + "二", + "三", + "四", + "五", + "六", + "日"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 0; + +// full month names +Calendar._MN = new Array +("一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月"); + +// short month names +Calendar._SMN = new Array +("一月", + "二月", + "三月", + "四月", + "五月", + "六月", + "七月", + "八月", + "九月", + "十月", + "十一月", + "十二月"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "關於 calendar"; + +Calendar._TT["ABOUT"] = +"DHTML 日期/時間 選擇器\n" + +"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) +"最For latest version visit: http://www.dynarch.com/projects/calendar/\n" + +"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"\n\n" + +"Date selection:\n" + +"- Use the \xab, \xbb buttons to select year\n" + +"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + +"- Hold mouse button on any of the above buttons for faster selection."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"時間選擇方式:\n" + +"- 「單擊」時分秒為遞增\n" + +"- 或 「Shift-單擊」為遞減\n" + +"- 或 「單擊且拖拉」為快速選擇"; + +Calendar._TT["PREV_YEAR"] = "前一年 (按住不放可顯示選單)"; +Calendar._TT["PREV_MONTH"] = "前一個月 (按住不放可顯示選單)"; +Calendar._TT["GO_TODAY"] = "選擇今天"; +Calendar._TT["NEXT_MONTH"] = "後一個月 (按住不放可顯示選單)"; +Calendar._TT["NEXT_YEAR"] = "下一年 (按住不放可顯式選單)"; +Calendar._TT["SEL_DATE"] = "請點選日期"; +Calendar._TT["DRAG_TO_MOVE"] = "按住不放可拖拉視窗"; +Calendar._TT["PART_TODAY"] = " (今天)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "以 %s 做為一週的首日"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "0,6"; + +Calendar._TT["CLOSE"] = "關閉視窗"; +Calendar._TT["TODAY"] = "今天"; +Calendar._TT["TIME_PART"] = "(Shift-)加「單擊」或「拖拉」可變更值"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d"; +Calendar._TT["TT_DATE_FORMAT"] = "星期 %a, %b %e 日"; + +Calendar._TT["WK"] = "週"; +Calendar._TT["TIME"] = "時間:"; From 60c82a03dbdf00145a25f3443c72ee646736283e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 14:57:17 +0000 Subject: [PATCH 048/710] Fixed drop down lists overflow on My account. git-svn-id: http://redmine.rubyforge.org/svn/trunk@960 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/my/account.rhtml | 5 ++++- public/stylesheets/application.css | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index 2dda62d70..0badc4b94 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -4,7 +4,10 @@

    <%=l(:label_my_account)%>

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

    <%=l(:label_information_plural)%>

    diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index f05fb9d91..249b20291 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -124,7 +124,7 @@ textarea.wiki-edit { width: 99%; } li p {margin-top: 0;} div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} .autoscroll {overflow-x: auto; padding:1px; width:100%;} -#user_firstname, #user_lastname, #user_mail, #notification_option { width: 90%; } +#user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; } /***** Tabular forms ******/ .tabular p{ From 6239e319ac83e7770bb59a5e09bb7b7dd952d80f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 15:02:57 +0000 Subject: [PATCH 049/710] Fixed versions/show layout with IE. git-svn-id: http://redmine.rubyforge.org/svn/trunk@961 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/versions/show.rhtml | 2 +- public/stylesheets/application.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/versions/show.rhtml b/app/views/versions/show.rhtml index d8e2d243d..a7c4f2b37 100644 --- a/app/views/versions/show.rhtml +++ b/app/views/versions/show.rhtml @@ -4,7 +4,7 @@

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

    -
    +
    <%= render_issue_status_by(@version, params[:status_by]) if @version.fixed_issues.count > 0 %>
    diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 249b20291..8aa63d886 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -267,7 +267,7 @@ table.progress td.open { background: #FFF none repeat scroll 0%; } p.pourcent {font-size: 80%;} p.progress-info {clear: left; font-style: italic; font-size: 80%;} -div#status_by { margin-left: 16px; margin-bottom: 16px; } +div#status_by { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; } /***** Tabs *****/ #content .tabs{height: 2.6em;} From 7fae4b089242526f59a778b0ca799bb86d370864 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 17:51:54 +0000 Subject: [PATCH 050/710] Bazaar adapter: fixed log with partial revisions parsing. git-svn-id: http://redmine.rubyforge.org/svn/trunk@962 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/bazaar_adapter.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb index ec9fd741c..4d7afeacc 100644 --- a/lib/redmine/scm/adapters/bazaar_adapter.rb +++ b/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -110,9 +110,12 @@ module Redmine revision.scmid = $1.strip elsif line =~ /^timestamp: (.+)$/ revision.time = Time.parse($1).localtime + elsif line =~ /^ -----/ + # partial revisions + parsing = nil unless parsing == 'message' elsif line =~ /^(message|added|modified|removed|renamed):/ parsing = $1 - elsif line =~ /^ (.+)$/ + elsif line =~ /^ (.*)$/ if parsing == 'message' revision.message << "#{$1}\n" else From 51adc242a31ef9af4d1857d0c6602bb39fdc6e98 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 17:58:44 +0000 Subject: [PATCH 051/710] Updated Polish translation (Mariusz Olejnik). git-svn-id: http://redmine.rubyforge.org/svn/trunk@963 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/pl.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lang/pl.yml b/lang/pl.yml index ff5f8d828..222c7ea9d 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -545,7 +545,7 @@ label_registration_automatic_activation: automatyczna aktywacja kont label_registration_manual_activation: manualna aktywacja kont notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdzenie administratora." field_time_zone: Strefa czasowa -text_caracters_minimum: Must be at least %d characters long. -setting_bcc_recipients: Blind carbon copy recipients (bcc) -button_annotate: Annotate -label_issues_by: Issues by %s +text_caracters_minimum: Musi być nie krótsze niż %d znaków. +setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) +button_annotate: Adnotuj +label_issues_by: Zagadnienia wprowadzone przez %s From f58db70bdecdbfd0a0d81c0c452d58b88391f9f1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 7 Dec 2007 18:42:40 +0000 Subject: [PATCH 052/710] More detailed html title on several views. git-svn-id: http://redmine.rubyforge.org/svn/trunk@964 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/admin/index.rhtml | 4 +++- app/views/admin/info.rhtml | 2 ++ app/views/admin/mail_options.rhtml | 2 ++ app/views/admin/projects.rhtml | 2 ++ app/views/custom_fields/list.rhtml | 4 +++- app/views/documents/show.rhtml | 6 ++++-- app/views/enumerations/list.rhtml | 2 ++ app/views/issue_statuses/list.rhtml | 4 +++- app/views/my/account.rhtml | 2 ++ app/views/my/page.rhtml | 2 ++ app/views/news/index.rhtml | 2 ++ app/views/news/show.rhtml | 2 ++ app/views/projects/activity.rhtml | 2 ++ app/views/projects/calendar.rhtml | 2 ++ app/views/projects/gantt.rhtml | 2 ++ app/views/projects/list.rhtml | 2 ++ app/views/projects/list_documents.rhtml | 2 ++ app/views/projects/list_files.rhtml | 4 +++- app/views/projects/settings.rhtml | 2 ++ app/views/projects/show.rhtml | 2 ++ app/views/repositories/changes.rhtml | 2 ++ app/views/repositories/revision.rhtml | 2 ++ app/views/repositories/revisions.rhtml | 2 ++ app/views/repositories/show.rhtml | 2 ++ app/views/roles/list.rhtml | 2 ++ app/views/roles/workflow.rhtml | 2 ++ app/views/search/index.rhtml | 2 ++ app/views/settings/edit.rhtml | 2 ++ app/views/trackers/list.rhtml | 4 +++- app/views/users/list.rhtml | 4 +++- 30 files changed, 68 insertions(+), 8 deletions(-) diff --git a/app/views/admin/index.rhtml b/app/views/admin/index.rhtml index 02eb5ae89..933e288a0 100644 --- a/app/views/admin/index.rhtml +++ b/app/views/admin/index.rhtml @@ -42,4 +42,6 @@

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

    \ No newline at end of file +

    + +<% set_html_title l(:label_administration) -%> diff --git a/app/views/admin/info.rhtml b/app/views/admin/info.rhtml index 179fda1a8..4d442f5ad 100644 --- a/app/views/admin/info.rhtml +++ b/app/views/admin/info.rhtml @@ -23,3 +23,5 @@ <% end %> <% end %> + +<% set_html_title(l(:label_information_plural)) -%> diff --git a/app/views/admin/mail_options.rhtml b/app/views/admin/mail_options.rhtml index 997cc3b22..a4b923873 100644 --- a/app/views/admin/mail_options.rhtml +++ b/app/views/admin/mail_options.rhtml @@ -29,3 +29,5 @@ <%= submit_tag l(:button_save) %> <% end %> + +<% set_html_title(l(:field_mail_notification)) -%> diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index 423d56ebe..e9d5e8537 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -47,3 +47,5 @@

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

    + +<% set_html_title l(:label_project_plural) -%> diff --git a/app/views/custom_fields/list.rhtml b/app/views/custom_fields/list.rhtml index 8862b3de1..bbdfeffb4 100644 --- a/app/views/custom_fields/list.rhtml +++ b/app/views/custom_fields/list.rhtml @@ -50,4 +50,6 @@
    <% end %> -<%= javascript_tag "showTab('#{@tab}');" %> \ No newline at end of file +<%= javascript_tag "showTab('#{@tab}');" %> + +<% set_html_title(l(:label_custom_field_plural)) -%> diff --git a/app/views/documents/show.rhtml b/app/views/documents/show.rhtml index 779315b22..8f53f1abe 100644 --- a/app/views/documents/show.rhtml +++ b/app/views/documents/show.rhtml @@ -3,9 +3,9 @@ <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
    -

    <%= @document.title %>

    +

    <%=h @document.title %>

    -

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

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

    <%= textilizable @document.description, :attachments => @document.attachments %>
    @@ -34,3 +34,5 @@ <%= submit_tag l(:button_add) %> <% end %> <% end %> + +<% set_html_title h(@document.title) -%> diff --git a/app/views/enumerations/list.rhtml b/app/views/enumerations/list.rhtml index bab4df830..2e069f392 100644 --- a/app/views/enumerations/list.rhtml +++ b/app/views/enumerations/list.rhtml @@ -24,3 +24,5 @@

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

    <% end %> + +<% set_html_title(l(:label_enumerations)) -%> diff --git a/app/views/issue_statuses/list.rhtml b/app/views/issue_statuses/list.rhtml index aaeec559a..05506f3c7 100644 --- a/app/views/issue_statuses/list.rhtml +++ b/app/views/issue_statuses/list.rhtml @@ -32,4 +32,6 @@ -<%= pagination_links_full @issue_status_pages %> \ No newline at end of file +<%= pagination_links_full @issue_status_pages %> + +<% set_html_title(l(:label_issue_status_plural)) -%> diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index 0badc4b94..fe2e5625b 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -44,3 +44,5 @@ <% content_for :sidebar do %> <%= render :partial => 'sidebar' %> <% end %> + +<% set_html_title l(:label_my_account) -%> diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml index 89d11ddea..db292da37 100644 --- a/app/views/my/page.rhtml +++ b/app/views/my/page.rhtml @@ -38,3 +38,5 @@ <%= javascript_tag 'new ContextMenu({})' %> + +<% set_html_title l(:label_my_page) -%> diff --git a/app/views/news/index.rhtml b/app/views/news/index.rhtml index a956f86d0..0b677d241 100644 --- a/app/views/news/index.rhtml +++ b/app/views/news/index.rhtml @@ -32,3 +32,5 @@ <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %> <% end %> + +<% set_html_title l(:label_news_plural) -%> diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index 32a24e85e..e4d7da4db 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -41,3 +41,5 @@

    <%= submit_tag l(:button_add) %>

    <% end %> <% end %> + +<% set_html_title h(@news.title) -%> diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index c902d60a3..d22443bc0 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -39,3 +39,5 @@

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

    <% end %> <% end %> + +<% set_html_title l(:label_activity) -%> diff --git a/app/views/projects/calendar.rhtml b/app/views/projects/calendar.rhtml index 2c02d59ad..39f171626 100644 --- a/app/views/projects/calendar.rhtml +++ b/app/views/projects/calendar.rhtml @@ -38,3 +38,5 @@

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

    <% end %> <% end %> + +<% set_html_title l(:label_calendar) -%> diff --git a/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml index a66754842..395514b61 100644 --- a/app/views/projects/gantt.rhtml +++ b/app/views/projects/gantt.rhtml @@ -245,3 +245,5 @@ if Date.today >= @date_from and Date.today <= @date_to %>

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

    <% end %> <% end %> + +<% set_html_title l(:label_gantt) -%> diff --git a/app/views/projects/list.rhtml b/app/views/projects/list.rhtml index c6e5b4dec..c7630c0a2 100644 --- a/app/views/projects/list.rhtml +++ b/app/views/projects/list.rhtml @@ -16,3 +16,5 @@ <%= l(:label_my_projects) %>
    <% end %> + +<% set_html_title l(:label_project_plural) -%> diff --git a/app/views/projects/list_documents.rhtml b/app/views/projects/list_documents.rhtml index bb272ee86..6829b9bf5 100644 --- a/app/views/projects/list_documents.rhtml +++ b/app/views/projects/list_documents.rhtml @@ -35,3 +35,5 @@ <% end %> <% end %> + +<% set_html_title l(:label_document_plural) -%> diff --git a/app/views/projects/list_files.rhtml b/app/views/projects/list_files.rhtml index 0e4f19d81..b9f688509 100644 --- a/app/views/projects/list_files.rhtml +++ b/app/views/projects/list_files.rhtml @@ -41,4 +41,6 @@ <% end %> <% end %> - \ No newline at end of file + + +<% set_html_title l(:label_attachment_plural) -%> diff --git a/app/views/projects/settings.rhtml b/app/views/projects/settings.rhtml index 13359de61..37a469df1 100644 --- a/app/views/projects/settings.rhtml +++ b/app/views/projects/settings.rhtml @@ -14,3 +14,5 @@ <%= tab = params[:tab] ? h(params[:tab]) : project_settings_tabs.first[:name] javascript_tag "showTab('#{tab}');" %> + +<% set_html_title l(:label_settings) -%> diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index bb01df1f4..ecde32bde 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -81,3 +81,5 @@ <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %> <% end %> + +<% set_html_title l(:label_overview) -%> diff --git a/app/views/repositories/changes.rhtml b/app/views/repositories/changes.rhtml index f843983db..218459128 100644 --- a/app/views/repositories/changes.rhtml +++ b/app/views/repositories/changes.rhtml @@ -16,3 +16,5 @@

    <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%> + +<% set_html_title(h(@entry.name)) -%> diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 64d1668bc..d87dfa955 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -62,3 +62,5 @@ <% content_for :header_tags do %> <%= stylesheet_link_tag "scm" %> <% end %> + +<% set_html_title("#{l(:label_revision)} #{@changeset.revision}") -%> diff --git a/app/views/repositories/revisions.rhtml b/app/views/repositories/revisions.rhtml index 2a45fc2ef..4483a482c 100644 --- a/app/views/repositories/revisions.rhtml +++ b/app/views/repositories/revisions.rhtml @@ -16,3 +16,5 @@ <%= stylesheet_link_tag "scm" %> <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %> <% end %> + +<% set_html_title l(:label_revision_plural) -%> diff --git a/app/views/repositories/show.rhtml b/app/views/repositories/show.rhtml index c9f44d575..9fb45514d 100644 --- a/app/views/repositories/show.rhtml +++ b/app/views/repositories/show.rhtml @@ -21,3 +21,5 @@ <% content_for :header_tags do %> <%= stylesheet_link_tag "scm" %> <% end %> + +<% set_html_title l(:label_repository) -%> diff --git a/app/views/roles/list.rhtml b/app/views/roles/list.rhtml index bdea2475f..02bb20f5e 100644 --- a/app/views/roles/list.rhtml +++ b/app/views/roles/list.rhtml @@ -32,3 +32,5 @@

    <%= pagination_links_full @role_pages %>

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

    + +<% set_html_title(l(:label_role_plural)) -%> diff --git a/app/views/roles/workflow.rhtml b/app/views/roles/workflow.rhtml index 0a435744f..02f1b48a2 100644 --- a/app/views/roles/workflow.rhtml +++ b/app/views/roles/workflow.rhtml @@ -54,3 +54,5 @@ <% end %> <% end %> + +<% set_html_title(l(:label_workflow)) -%> diff --git a/app/views/search/index.rhtml b/app/views/search/index.rhtml index 695107fe8..9f156cbe1 100644 --- a/app/views/search/index.rhtml +++ b/app/views/search/index.rhtml @@ -41,3 +41,5 @@ }, :href => url_for(params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %> <% end %>

    + +<% set_html_title(l(:label_search)) -%> diff --git a/app/views/settings/edit.rhtml b/app/views/settings/edit.rhtml index 4a0a400a3..758045f05 100644 --- a/app/views/settings/edit.rhtml +++ b/app/views/settings/edit.rhtml @@ -98,3 +98,5 @@ <%= submit_tag l(:button_save) %> <% end %> + +<% set_html_title(l(:label_settings)) -%> diff --git a/app/views/trackers/list.rhtml b/app/views/trackers/list.rhtml index 8517c44d6..d339bdba0 100644 --- a/app/views/trackers/list.rhtml +++ b/app/views/trackers/list.rhtml @@ -30,4 +30,6 @@ -<%= pagination_links_full @tracker_pages %> \ No newline at end of file +<%= pagination_links_full @tracker_pages %> + +<% set_html_title(l(:label_tracker_plural)) -%> diff --git a/app/views/users/list.rhtml b/app/views/users/list.rhtml index 2ffbcd6f6..3f879d7a6 100644 --- a/app/views/users/list.rhtml +++ b/app/views/users/list.rhtml @@ -57,4 +57,6 @@

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

    \ No newline at end of file +

    + +<% set_html_title(l(:label_user_plural)) -%> From 6d9490ddcc9c501d31a8b403146cd4ba6d8cc5b5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 10 Dec 2007 17:58:07 +0000 Subject: [PATCH 053/710] Merged Rails 2.0 compatibility changes. Compatibility with Rails 1.2 is preserved. git-svn-id: http://redmine.rubyforge.org/svn/trunk@975 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/search_controller.rb | 7 +- app/models/mailer.rb | 7 +- app/models/project.rb | 9 +- app/views/mailer/issue_add.text.html.rhtml | 2 +- app/views/mailer/issue_add.text.plain.rhtml | 2 +- app/views/mailer/issue_edit.text.html.rhtml | 2 +- app/views/mailer/issue_edit.text.plain.rhtml | 2 +- config/environment.rb | 1 + config/environments/development.rb | 3 - test/fixtures/members.yml | 7 + test/fixtures/users.yml | 16 + test/functional/sys_api_test.rb | 31 + test/unit/project_test.rb | 10 +- vendor/plugins/actionwebservice/CHANGELOG | 265 ++++ vendor/plugins/actionwebservice/MIT-LICENSE | 21 + vendor/plugins/actionwebservice/README | 364 +++++ vendor/plugins/actionwebservice/Rakefile | 172 ++ vendor/plugins/actionwebservice/TODO | 32 + vendor/plugins/actionwebservice/init.rb | 7 + vendor/plugins/actionwebservice/install.rb | 30 + .../lib/action_web_service.rb | 66 + .../lib/action_web_service/api.rb | 297 ++++ .../lib/action_web_service/base.rb | 38 + .../lib/action_web_service/casting.rb | 138 ++ .../lib/action_web_service/client.rb | 3 + .../lib/action_web_service/client/base.rb | 28 + .../action_web_service/client/soap_client.rb | 113 ++ .../client/xmlrpc_client.rb | 58 + .../lib/action_web_service/container.rb | 3 + .../container/action_controller_container.rb | 93 ++ .../container/delegated_container.rb | 86 + .../container/direct_container.rb | 69 + .../lib/action_web_service/dispatcher.rb | 2 + .../action_web_service/dispatcher/abstract.rb | 207 +++ .../action_controller_dispatcher.rb | 379 +++++ .../lib/action_web_service/invocation.rb | 202 +++ .../lib/action_web_service/protocol.rb | 4 + .../action_web_service/protocol/abstract.rb | 112 ++ .../action_web_service/protocol/discovery.rb | 37 + .../protocol/soap_protocol.rb | 176 +++ .../protocol/soap_protocol/marshaler.rb | 235 +++ .../protocol/xmlrpc_protocol.rb | 122 ++ .../lib/action_web_service/scaffolding.rb | 283 ++++ .../lib/action_web_service/struct.rb | 64 + .../support/class_inheritable_options.rb | 26 + .../support/signature_types.rb | 226 +++ .../templates/scaffolds/layout.erb | 65 + .../templates/scaffolds/layout.rhtml | 0 .../templates/scaffolds/methods.erb | 6 + .../templates/scaffolds/methods.rhtml | 0 .../templates/scaffolds/parameters.erb | 29 + .../templates/scaffolds/parameters.rhtml | 0 .../templates/scaffolds/result.erb | 30 + .../templates/scaffolds/result.rhtml | 0 .../lib/action_web_service/test_invoke.rb | 110 ++ .../lib/action_web_service/version.rb | 9 + .../actionwebservice/lib/actionwebservice.rb | 1 + vendor/plugins/actionwebservice/setup.rb | 1379 +++++++++++++++++ vendor/plugins/acts_as_list/README | 23 + vendor/plugins/acts_as_list/init.rb | 3 + .../lib/active_record/acts/list.rb | 256 +++ vendor/plugins/acts_as_list/test/list_test.rb | 332 ++++ vendor/plugins/acts_as_tree/README | 26 + vendor/plugins/acts_as_tree/Rakefile | 22 + vendor/plugins/acts_as_tree/init.rb | 1 + .../lib/active_record/acts/tree.rb | 96 ++ .../acts_as_tree/test/abstract_unit.rb | 0 .../acts_as_tree/test/acts_as_tree_test.rb | 219 +++ vendor/plugins/acts_as_tree/test/database.yml | 0 .../acts_as_tree/test/fixtures/mixin.rb | 0 .../acts_as_tree/test/fixtures/mixins.yml | 0 vendor/plugins/acts_as_tree/test/schema.rb | 0 vendor/plugins/classic_pagination/CHANGELOG | 152 ++ vendor/plugins/classic_pagination/README | 18 + vendor/plugins/classic_pagination/Rakefile | 22 + vendor/plugins/classic_pagination/init.rb | 33 + vendor/plugins/classic_pagination/install.rb | 1 + .../classic_pagination/lib/pagination.rb | 405 +++++ .../lib/pagination_helper.rb | 135 ++ .../test/fixtures/companies.yml | 24 + .../test/fixtures/company.rb | 9 + .../test/fixtures/developer.rb | 7 + .../test/fixtures/developers.yml | 21 + .../test/fixtures/developers_projects.yml | 13 + .../test/fixtures/project.rb | 3 + .../test/fixtures/projects.yml | 7 + .../test/fixtures/replies.yml | 13 + .../classic_pagination/test/fixtures/reply.rb | 5 + .../test/fixtures/schema.sql | 42 + .../classic_pagination/test/fixtures/topic.rb | 3 + .../test/fixtures/topics.yml | 22 + .../plugins/classic_pagination/test/helper.rb | 117 ++ .../test/pagination_helper_test.rb | 38 + .../test/pagination_test.rb | 177 +++ 94 files changed, 7918 insertions(+), 13 deletions(-) create mode 100644 test/functional/sys_api_test.rb create mode 100644 vendor/plugins/actionwebservice/CHANGELOG create mode 100644 vendor/plugins/actionwebservice/MIT-LICENSE create mode 100644 vendor/plugins/actionwebservice/README create mode 100644 vendor/plugins/actionwebservice/Rakefile create mode 100644 vendor/plugins/actionwebservice/TODO create mode 100644 vendor/plugins/actionwebservice/init.rb create mode 100644 vendor/plugins/actionwebservice/install.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/api.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/base.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/casting.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/client.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/container.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/struct.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.rhtml create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.erb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/parameters.rhtml create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.erb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb create mode 100644 vendor/plugins/actionwebservice/lib/action_web_service/version.rb create mode 100644 vendor/plugins/actionwebservice/lib/actionwebservice.rb create mode 100644 vendor/plugins/actionwebservice/setup.rb create mode 100644 vendor/plugins/acts_as_list/README create mode 100644 vendor/plugins/acts_as_list/init.rb create mode 100644 vendor/plugins/acts_as_list/lib/active_record/acts/list.rb create mode 100644 vendor/plugins/acts_as_list/test/list_test.rb create mode 100644 vendor/plugins/acts_as_tree/README create mode 100644 vendor/plugins/acts_as_tree/Rakefile create mode 100644 vendor/plugins/acts_as_tree/init.rb create mode 100644 vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb create mode 100644 vendor/plugins/acts_as_tree/test/abstract_unit.rb create mode 100644 vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb create mode 100644 vendor/plugins/acts_as_tree/test/database.yml create mode 100644 vendor/plugins/acts_as_tree/test/fixtures/mixin.rb create mode 100644 vendor/plugins/acts_as_tree/test/fixtures/mixins.yml create mode 100644 vendor/plugins/acts_as_tree/test/schema.rb create mode 100644 vendor/plugins/classic_pagination/CHANGELOG create mode 100644 vendor/plugins/classic_pagination/README create mode 100644 vendor/plugins/classic_pagination/Rakefile create mode 100644 vendor/plugins/classic_pagination/init.rb create mode 100644 vendor/plugins/classic_pagination/install.rb create mode 100644 vendor/plugins/classic_pagination/lib/pagination.rb create mode 100644 vendor/plugins/classic_pagination/lib/pagination_helper.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/companies.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/company.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/developer.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/developers.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/project.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/projects.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/replies.yml create mode 100644 vendor/plugins/classic_pagination/test/fixtures/reply.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/schema.sql create mode 100644 vendor/plugins/classic_pagination/test/fixtures/topic.rb create mode 100644 vendor/plugins/classic_pagination/test/fixtures/topics.yml create mode 100644 vendor/plugins/classic_pagination/test/helper.rb create mode 100644 vendor/plugins/classic_pagination/test/pagination_helper_test.rb create mode 100644 vendor/plugins/classic_pagination/test/pagination_test.rb diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index ee4f863aa..69e1ee503 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -90,9 +90,10 @@ class SearchController < ApplicationController end else operator = @all_words ? ' AND ' : ' OR ' - Project.with_scope(:find => {:conditions => Project.visible_by(User.current)}) do - @results += Project.find(:all, :limit => limit, :conditions => [ (["(LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'projects' - end + @results += Project.find(:all, + :limit => limit, + :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] + ) if @scope.include? 'projects' # if only one project is found, user is redirected to its overview redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1 end diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 9639e1a9c..257f57726 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -150,6 +150,11 @@ class Mailer < ActionMailer::Base def render_message(method_name, body) layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' body[:content_for_layout] = render(:file => method_name, :body => body) - ActionView::Base.new(File.join(template_root, 'mailer'), body, self).render(:file => layout) + ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}") end + + # Makes partial rendering work with Rails 1.2 (retro-compatibility) + def self.controller_path + '' + end unless respond_to?('controller_path') end diff --git a/app/models/project.rb b/app/models/project.rb index 5788732d7..70b1eccd1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -20,7 +20,7 @@ class Project < ActiveRecord::Base STATUS_ACTIVE = 1 STATUS_ARCHIVED = 9 - has_many :members, :dependent => :delete_all, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" has_many :users, :through => :members has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :enabled_modules, :dependent => :delete_all @@ -62,6 +62,8 @@ class Project < ActiveRecord::Base validates_length_of :identifier, :in => 3..20 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ + before_destroy :delete_all_members + def identifier=(identifier) super unless identifier_frozen? end @@ -129,6 +131,11 @@ class Project < ActiveRecord::Base children.select {|child| child.active?} end + # Deletes all project's members + def delete_all_members + Member.delete_all(['project_id = ?', id]) + end + # Users issues can be assigned to def assignable_users members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort diff --git a/app/views/mailer/issue_add.text.html.rhtml b/app/views/mailer/issue_add.text.html.rhtml index adcb4c9fe..6d20c333f 100644 --- a/app/views/mailer/issue_add.text.html.rhtml +++ b/app/views/mailer/issue_add.text.html.rhtml @@ -1,3 +1,3 @@ <%= l(:text_issue_added, "##{@issue.id}") %>
    -<%= render :file => "_issue_text_html", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_add.text.plain.rhtml b/app/views/mailer/issue_add.text.plain.rhtml index 797fb11b6..38c17e777 100644 --- a/app/views/mailer/issue_add.text.plain.rhtml +++ b/app/views/mailer/issue_add.text.plain.rhtml @@ -1,3 +1,3 @@ <%= l(:text_issue_added, "##{@issue.id}") %> ---------------------------------------- -<%= render :file => "_issue_text_plain", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.html.rhtml b/app/views/mailer/issue_edit.text.html.rhtml index 40d34968e..9af8592b4 100644 --- a/app/views/mailer/issue_edit.text.html.rhtml +++ b/app/views/mailer/issue_edit.text.html.rhtml @@ -7,4 +7,4 @@ <%= textilizable(@journal.notes) %>
    -<%= render :file => "_issue_text_html", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.plain.rhtml b/app/views/mailer/issue_edit.text.plain.rhtml index 32019eaea..04b1bc54a 100644 --- a/app/views/mailer/issue_edit.text.plain.rhtml +++ b/app/views/mailer/issue_edit.text.plain.rhtml @@ -5,4 +5,4 @@ <% end %> <%= @journal.notes if @journal.notes? %> ---------------------------------------- -<%= render :file => "_issue_text_plain", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/config/environment.rb b/config/environment.rb index 447e47e21..dced5993e 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -23,6 +23,7 @@ Rails::Initializer.run do |config| # Use the database for sessions instead of the file system # (create the session table with 'rake create_sessions_table') # config.action_controller.session_store = :active_record_store + config.action_controller.session_store = :PStore # Enable page/fragment caching by setting a file-based store # (remember to create the caching directory and make it readable to the application) diff --git a/config/environments/development.rb b/config/environments/development.rb index 04b779200..c816f03e3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -8,9 +8,6 @@ config.cache_classes = false # Log error messages when you accidentally call methods on nil. config.whiny_nils = true -# Enable the breakpoint server that script/breakpointer connects to -config.breakpoint_server = true - # Show full error reports and disable caching config.action_controller.consider_all_requests_local = true config.action_controller.perform_caching = false diff --git a/test/fixtures/members.yml b/test/fixtures/members.yml index 392225e52..2c9209131 100644 --- a/test/fixtures/members.yml +++ b/test/fixtures/members.yml @@ -17,4 +17,11 @@ members_003: role_id: 2 id: 3 user_id: 2 +members_004: + id: 4 + created_on: 2006-07-19 19:35:36 +02:00 + project_id: 1 + role_id: 2 + # Locked user + user_id: 5 \ No newline at end of file diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index ffa2fe42e..df7123879 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -59,3 +59,19 @@ users_003: auth_source_id: mail_notification: true login: dlopper +users_005: + id: 5 + created_on: 2006-07-19 19:33:19 +02:00 + # Locked + status: 3 + last_login_on: + language: en + hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: dlopper2@somenet.foo + lastname: Lopper2 + firstname: Dave2 + auth_source_id: + mail_notification: true + login: dlopper2 diff --git a/test/functional/sys_api_test.rb b/test/functional/sys_api_test.rb new file mode 100644 index 000000000..ec8d0964e --- /dev/null +++ b/test/functional/sys_api_test.rb @@ -0,0 +1,31 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'sys_controller' + +# Re-raise errors caught by the controller. +class SysController; def rescue_action(e) raise e end; end + +class SysControllerTest < Test::Unit::TestCase + fixtures :projects, :repositories + + def setup + @controller = SysController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + # Enable WS + Setting.sys_api_enabled = 1 + end + + def test_projects + result = invoke :projects + assert_equal Project.count, result.size + assert result.first.is_a?(Project) + end + + def test_repository_created + project = Project.find(3) + assert_nil project.repository + assert invoke(:repository_created, project.identifier, 'http://localhost/svn') + project.reload + assert_not_nil project.repository + end +end diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index a8cf46e4f..62ba2b02d 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -18,7 +18,7 @@ require File.dirname(__FILE__) + '/../test_helper' class ProjectTest < Test::Unit::TestCase - fixtures :projects, :issues, :issue_statuses, :journals, :journal_details + fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles def setup @ecookbook = Project.find(1) @@ -80,8 +80,16 @@ class ProjectTest < Test::Unit::TestCase end def test_destroy + # 2 active members + assert_equal 2, @ecookbook.members.size + # and 1 is locked + assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size + @ecookbook.destroy + # make sure that the project non longer exists assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) } + # make sure all members have been removed + assert_equal 0, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size end def test_subproject_ok diff --git a/vendor/plugins/actionwebservice/CHANGELOG b/vendor/plugins/actionwebservice/CHANGELOG new file mode 100644 index 000000000..bb5280356 --- /dev/null +++ b/vendor/plugins/actionwebservice/CHANGELOG @@ -0,0 +1,265 @@ +*SVN* + +* Documentation for ActionWebService::API::Base. Closes #7275. [zackchandler] + +* Allow action_web_service to handle various HTTP methods including GET. Closes #7011. [zackchandler] + +* Ensure that DispatcherError is being thrown when a malformed request is received. [Kent Sibilev] + +* Added support for decimal types. Closes #6676. [Kent Sibilev] + +* Removed deprecated end_form_tag helper. [Kent Sibilev] + +* Removed deprecated @request and @response usages. [Kent Sibilev] + +* Removed invocation of deprecated before_action and around_action filter methods. Corresponding before_invocation and after_invocation methods should be used instead. #6275 [Kent Sibilev] + +* Provide access to the underlying SOAP driver. #6212 [bmilekic, Kent Sibilev] + +* Deprecation: update docs. #5998 [jakob@mentalized.net, Kevin Clark] + +* ActionWebService WSDL generation ignores HTTP_X_FORWARDED_HOST [Paul Butcher ] + +* Tighten rescue clauses. #5985 [james@grayproductions.net] + +* Fixed XMLRPC multicall when one of the called methods returns a struct object. [Kent Sibilev] + +* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar] + +* Fix invoke_layered since api_method didn't declare :expects. Closes #4720. [Kevin Ballard , Kent Sibilev] + +* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.] + +* Replace Ruby's deprecated append_features in favor of included. [Marcel Molina Jr.] + +* Fix test database name typo. [Marcel Molina Jr.] + +*1.1.2* (April 9th, 2006) + +* Rely on Active Record 1.14.2 + + +*1.1.1* (April 6th, 2006) + +* Do not convert driver options to strings (#4499) + + +*1.1.0* (March 27th, 2006) + +* Make ActiveWebService::Struct type reloadable + +* Fix scaffolding action when one of the members of a structural type has date or time type + +* Remove extra index hash when generating scaffold html for parameters of structural type #4374 [joe@mjg2.com] + +* Fix Scaffold Fails with Struct as a Parameter #4363 [joe@mjg2.com] + +* Fix soap type registration of multidimensional arrays (#4232) + +* Fix that marshaler couldn't handle ActiveRecord models defined in a different namespace (#2392). + +* Fix that marshaler couldn't handle structs with members of ActiveRecord type (#1889). + +* Fix that marshaler couldn't handle nil values for inner structs (#3576). + +* Fix that changes to ActiveWebService::API::Base required restarting of the server (#2390). + +* Fix scaffolding for signatures with :date, :time and :base64 types (#3321, #2769, #2078). + +* Fix for incorrect casting of TrueClass/FalseClass instances (#2633, #3421). + +* Fix for incompatibility problems with SOAP4R 1.5.5 (#2553) [Kent Sibilev] + + +*1.0.0* (December 13th, 2005) + +* Become part of Rails 1.0 + +*0.9.4* (December 7th, 2005) + +* Update from LGPL to MIT license as per Minero Aoki's permission. [Marcel Molina Jr.] + +* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.] + +* Fix that XML-RPC date/time values did not have well-defined behaviour (#2516, #2534). This fix has one caveat, in that we can't support pre-1970 dates from XML-RPC clients. + +*0.9.3* (November 7th, 2005) + +* Upgraded to Action Pack 1.11.0 and Active Record 1.13.0 + + +*0.9.2* (October 26th, 2005) + +* Upgraded to Action Pack 1.10.2 and Active Record 1.12.2 + + +*0.9.1* (October 19th, 2005) + +* Upgraded to Action Pack 1.10.1 and Active Record 1.12.1 + + +*0.9.0* (October 16th, 2005) + +* Fix invalid XML request generation bug in test_invoke [Ken Barker] + +* Add XML-RPC 'system.multicall' support #1941 [jbonnar] + +* Fix duplicate XSD entries for custom types shared across delegated/layered services #1729 [Tyler Kovacs] + +* Allow multiple invocations in the same test method #1720 [dkhawk] + +* Added ActionWebService::API::Base.soap_client and ActionWebService::API::Base.xmlrpc_client helper methods to create the internal clients for an API, useful for testing from ./script/console + +* ActionWebService now always returns UTF-8 responses. + + +*0.8.1* (11 July, 2005) + +* Fix scaffolding for Action Pack controller changes + + +*0.8.0* (6 July, 2005) + +* Fix WSDL generation by aliasing #inherited instead of trying to overwrite it, or the WSDL action may end up not being defined in the controller + +* Add ActionController::Base.wsdl_namespace option, to allow overriding of the namespace used in generated WSDL and SOAP messages. This is equivalent to the [WebService(Namespace = "Value")] attribute in .NET. + +* Add workaround for Ruby 1.8.3's SOAP4R changing the return value of SOAP::Mapping::Registry#find_mapped_soap_class #1414 [Shugo Maeda] + +* Fix moduled controller URLs in WSDL, and add unit test to verify the generated URL #1428 + +* Fix scaffolding template paths, it was broken on Win32 + +* Fix that functional testing of :layered controllers failed when using the SOAP protocol + +* Allow invocation filters in :direct controllers as well, as they have access to more information regarding the web service request than ActionPack filters + +* Add support for a :base64 signature type #1272 [Shugo Maeda] + +* Fix that boolean fields were not rendered correctly in scaffolding + +* Fix that scaffolding was not working for :delegated dispatching + +* Add support for structured types as input parameters to scaffolding, this should let one test the blogging APIs using scaffolding as well + +* Fix that generated WSDL was not using relative_url_root for base URI #1210 [Shugo Maeda] + +* Use UTF-8 encoding by default for SOAP responses, but if an encoding is supplied by caller, use that for the response #1211 [Shugo Maeda, NAKAMURA Hiroshi] + +* If the WSDL was retrieved over HTTPS, use HTTPS URLs in the WSDL too + +* Fix that casting change in 0.7.0 would convert nil values to the default value for the type instead of leaving it as nil + + +*0.7.1* (20th April, 2005) + +* Depend on Active Record 1.10.1 and Action Pack 1.8.1 + + +*0.7.0* (19th April, 2005) + +* When casting structured types, don't try to send obj.name= unless obj responds to it, causes casting to be less likely to fail for XML-RPC + +* Add scaffolding via ActionController::Base.web_service_scaffold for quick testing using a web browser + +* ActionWebService::API::Base#api_methods now returns a hash containing ActionWebService::API::Method objects instead of hashes. However, ActionWebService::API::Method defines a #[]() backwards compatibility method so any existing code utilizing this will still work. + +* The :layered dispatching mode can now be used with SOAP as well, allowing you to support SOAP and XML-RPC clients for APIs like the metaWeblog API + +* Remove ActiveRecordSoapMarshallable workaround, see #912 for details + +* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC) + +* Ensure return value is properly cast as well, fixes XML-RPC interoperability with Ecto and possibly other clients + +* Include backtraces in 500 error responses for failed request parsing, and remove "rescue nil" statements obscuring real errors for XML-RPC + +* Perform casting of struct members even if the structure is already of the correct type, so that the type we specify for the struct member is always the type of the value seen by the API implementation + + +*0.6.2* (27th March, 2005) + +* Allow method declarations for direct dispatching to declare parameters as well. We treat an arity of < 0 or > 0 as an indication that we should send through parameters. Closes #939. + + +*0.6.1* (22th March, 2005) + +* Fix that method response QNames mismatched with that declared in the WSDL, makes SOAP::WSDLDriverFactory work against AWS again + +* Fix that @request.env was being modified, instead, dup the value gotten from env + +* Fix XML-RPC example to use :layered mode, so it works again + +* Support casting '0' or 0 into false, and '1' or 1 into true, when expecting a boolean value + +* Fix that SOAP fault response fault code values were not QName's #804 + + +*0.6.0* (7th March, 2005) + +* Add action_controller/test_invoke, used for integrating AWS with the Rails testing infrastructure + +* Allow passing through options to the SOAP RPC driver for the SOAP client + +* Make the SOAP WS marshaler use #columns to decide which fields to marshal as well, avoids providing attributes brought in by associations + +* Add ActionWebService::API::Base.allow_active_record_expects option, with a default of false. Setting this to true will allow specifying ActiveRecord::Base model classes in :expects. API writers should take care to validate the received ActiveRecord model objects when turning it on, and/or have an authentication mechanism in place to reduce the security risk. + +* Improve error message reporting. Bugs in either AWS or the web service itself will send back a protocol-specific error report message if possible, otherwise, provide as much detail as possible. + +* Removed type checking of received parameters, and perform casting for XML-RPC if possible, but fallback to the received parameters if casting fails, closes #677 + +* Refactored SOAP and XML-RPC marshaling and encoding into a small library devoted exclusively to protocol specifics, also cleaned up the SOAP marshaling approach, so that array and custom type marshaling should be a bit faster. + +* Add namespaced XML-RPC method name support, closes #678 + +* Replace '::' with '..' in fully qualified type names for marshaling and WSDL. This improves interoperability with .NET, and closes #676. + + +*0.5.0* (24th February, 2005) + + * lib/action_service/dispatcher*: replace "router" fragments with + one file for Action Controllers, moves dispatching work out of + the container + * lib/*,test/*,examples/*: rename project to + ActionWebService. prefix all generic "service" type names with web_. + update all using code as well as the RDoc. + * lib/action_service/router/wsdl.rb: ensure that #wsdl is + defined in the final container class, or the new ActionPack + filtering will exclude it + * lib/action_service/struct.rb,test/struct_test.rb: create a + default #initialize on inherit that accepts a Hash containing + the default member values + * lib/action_service/api/action_controller.rb: add support and + tests for #client_api in controller + * test/router_wsdl_test.rb: add tests to ensure declared + service names don't contain ':', as ':' causes interoperability + issues + * lib/*, test/*: rename "interface" concept to "api", and change all + related uses to reflect this change. update all uses of Inflector + to call the method on String instead. + * test/api_test.rb: add test to ensure API definition not + instantiatable + * lib/action_service/invocation.rb: change @invocation_params to + @method_params + * lib/*: update RDoc + * lib/action_service/struct.rb: update to support base types + * lib/action_service/support/signature.rb: support the notion of + "base types" in signatures, with well-known unambiguous names such as :int, + :bool, etc, which map to the correct Ruby class. accept the same names + used by ActiveRecord as well as longer versions of each, as aliases. + * examples/*: update for seperate API definition updates + * lib/action_service/*, test/*: extensive refactoring: define API methods in + a seperate class, and specify it wherever used with 'service_api'. + this makes writing a client API for accessing defined API methods + with ActionWebService really easy. + * lib/action_service/container.rb: fix a bug in default call + handling for direct dispatching, and add ActionController filter + support for direct dispatching. + * test/router_action_controller_test.rb: add tests to ensure + ActionController filters are actually called. + * test/protocol_soap_test.rb: add more tests for direct dispatching. + +0.3.0 + + * First public release diff --git a/vendor/plugins/actionwebservice/MIT-LICENSE b/vendor/plugins/actionwebservice/MIT-LICENSE new file mode 100644 index 000000000..528941e84 --- /dev/null +++ b/vendor/plugins/actionwebservice/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2005 Leon Breedt + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/plugins/actionwebservice/README b/vendor/plugins/actionwebservice/README new file mode 100644 index 000000000..78b91f081 --- /dev/null +++ b/vendor/plugins/actionwebservice/README @@ -0,0 +1,364 @@ += Action Web Service -- Serving APIs on rails + +Action Web Service provides a way to publish interoperable web service APIs with +Rails without spending a lot of time delving into protocol details. + + +== Features + +* SOAP RPC protocol support +* Dynamic WSDL generation for APIs +* XML-RPC protocol support +* Clients that use the same API definitions as the server for + easy interoperability with other Action Web Service based applications +* Type signature hints to improve interoperability with static languages +* Active Record model class support in signatures + + +== Defining your APIs + +You specify the methods you want to make available as API methods in an +ActionWebService::API::Base derivative, and then specify this API +definition class wherever you want to use that API. + +The implementation of the methods is done separately from the API +specification. + + +==== Method name inflection + +Action Web Service will camelcase the method names according to Rails Inflector +rules for the API visible to public callers. What this means, for example, +is that the method names in generated WSDL will be camelcased, and callers will +have to supply the camelcased name in their requests for the request to +succeed. + +If you do not desire this behaviour, you can turn it off with the +ActionWebService::API::Base +inflect_names+ option. + + +==== Inflection examples + + :add => Add + :find_all => FindAll + + +==== Disabling inflection + + class PersonAPI < ActionWebService::API::Base + inflect_names false + end + + +==== API definition example + + class PersonAPI < ActionWebService::API::Base + api_method :add, :expects => [:string, :string, :bool], :returns => [:int] + api_method :remove, :expects => [:int], :returns => [:bool] + end + +==== API usage example + + class PersonController < ActionController::Base + web_service_api PersonAPI + + def add + end + + def remove + end + end + + +== Publishing your APIs + +Action Web Service uses Action Pack to process protocol requests. There are two +modes of dispatching protocol requests, _Direct_, and _Delegated_. + + +=== Direct dispatching + +This is the default mode. In this mode, public controller instance methods +implement the API methods, and parameters are passed through to the methods in +accordance with the API specification. + +The return value of the method is sent back as the return value to the +caller. + +In this mode, a special api action is generated in the target +controller to unwrap the protocol request, forward it on to the relevant method +and send back the wrapped return value. This action must not be +overridden. + +==== Direct dispatching example + + class PersonController < ApplicationController + web_service_api PersonAPI + + def add + end + + def remove + end + end + + class PersonAPI < ActionWebService::API::Base + ... + end + + +For this example, protocol requests for +Add+ and +Remove+ methods sent to +/person/api will be routed to the controller methods +add+ and +remove+. + + +=== Delegated dispatching + +This mode can be turned on by setting the +web_service_dispatching_mode+ option +in a controller to :delegated. + +In this mode, the controller contains one or more web service objects (objects +that implement an ActionWebService::API::Base definition). These web service +objects are each mapped onto one controller action only. + +==== Delegated dispatching example + + class ApiController < ApplicationController + web_service_dispatching_mode :delegated + + web_service :person, PersonService.new + end + + class PersonService < ActionWebService::Base + web_service_api PersonAPI + + def add + end + + def remove + end + end + + class PersonAPI < ActionWebService::API::Base + ... + end + + +For this example, all protocol requests for +PersonService+ are +sent to the /api/person action. + +The /api/person action is generated when the +web_service+ +method is called. This action must not be overridden. + +Other controller actions (actions that aren't the target of a +web_service+ call) +are ignored for ActionWebService purposes, and can do normal action tasks. + + +=== Layered dispatching + +This mode can be turned on by setting the +web_service_dispatching_mode+ option +in a controller to :layered. + +This mode is similar to _delegated_ mode, in that multiple web service objects +can be attached to one controller, however, all protocol requests are sent to a +single endpoint. + +Use this mode when you want to share code between XML-RPC and SOAP clients, +for APIs where the XML-RPC method names have prefixes. An example of such +a method name would be blogger.newPost. + + +==== Layered dispatching example + + + class ApiController < ApplicationController + web_service_dispatching_mode :layered + + web_service :mt, MovableTypeService.new + web_service :blogger, BloggerService.new + web_service :metaWeblog, MetaWeblogService.new + end + + class MovableTypeService < ActionWebService::Base + ... + end + + class BloggerService < ActionWebService::Base + ... + end + + class MetaWeblogService < ActionWebService::API::Base + ... + end + + +For this example, an XML-RPC call for a method with a name like +mt.getCategories will be sent to the getCategories +method on the :mt service. + + +== Customizing WSDL generation + +You can customize the names used for the SOAP bindings in the generated +WSDL by using the wsdl_service_name option in a controller: + + class WsController < ApplicationController + wsdl_service_name 'MyApp' + end + +You can also customize the namespace used in the generated WSDL for +custom types and message definition types: + + class WsController < ApplicationController + wsdl_namespace 'http://my.company.com/app/wsapi' + end + +The default namespace used is 'urn:ActionWebService', if you don't supply +one. + + +== ActionWebService and UTF-8 + +If you're going to be sending back strings containing non-ASCII UTF-8 +characters using the :string data type, you need to make sure that +Ruby is using UTF-8 as the default encoding for its strings. + +The default in Ruby is to use US-ASCII encoding for strings, which causes a string +validation check in the Ruby SOAP library to fail and your string to be sent +back as a Base-64 value, which may confuse clients that expected strings +because of the WSDL. + +Two ways of setting the default string encoding are: + +* Start Ruby using the -Ku command-line option to the Ruby executable +* Set the $KCODE flag in config/environment.rb to the + string 'UTF8' + + +== Testing your APIs + + +=== Functional testing + +You can perform testing of your APIs by creating a functional test for the +controller dispatching the API, and calling #invoke in the test case to +perform the invocation. + +Example: + + class PersonApiControllerTest < Test::Unit::TestCase + def setup + @controller = PersonController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_add + result = invoke :remove, 1 + assert_equal true, result + end + end + +This example invokes the API method test, defined on +the PersonController, and returns the result. + + +=== Scaffolding + +You can also test your APIs with a web browser by attaching scaffolding +to the controller. + +Example: + + class PersonController + web_service_scaffold :invocation + end + +This creates an action named invocation on the PersonController. + +Navigating to this action lets you select the method to invoke, supply the parameters, +and view the result of the invocation. + + +== Using the client support + +Action Web Service includes client classes that can use the same API +definition as the server. The advantage of this approach is that your client +will have the same support for Active Record and structured types as the +server, and can just use them directly, and rely on the marshaling to Do The +Right Thing. + +*Note*: The client support is intended for communication between Ruby on Rails +applications that both use Action Web Service. It may work with other servers, but +that is not its intended use, and interoperability can't be guaranteed, especially +not for .NET web services. + +Web services protocol specifications are complex, and Action Web Service client +support can only be guaranteed to work with a subset. + + +==== Factory created client example + + class BlogManagerController < ApplicationController + web_client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger' + end + + class SearchingController < ApplicationController + web_client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch' + end + +See ActionWebService::API::ActionController::ClassMethods for more details. + +==== Manually created client example + + class PersonAPI < ActionWebService::API::Base + api_method :find_all, :returns => [[Person]] + end + + soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...") + persons = soap_client.find_all + + class BloggerAPI < ActionWebService::API::Base + inflect_names false + api_method :getRecentPosts, :returns => [[Blog::Post]] + end + + blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger") + posts = blog.getRecentPosts + + +See ActionWebService::Client::Soap and ActionWebService::Client::XmlRpc for more details. + +== Dependencies + +Action Web Service requires that the Action Pack and Active Record are either +available to be required immediately or are accessible as GEMs. + +It also requires a version of Ruby that includes SOAP support in the standard +library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended; this +is the version tested against. + + +== Download + +The latest Action Web Service version can be downloaded from +http://rubyforge.org/projects/actionservice + + +== Installation + +You can install Action Web Service with the following command. + + % [sudo] ruby setup.rb + + +== License + +Action Web Service is released under the MIT license. + + +== Support + +The Ruby on Rails mailing list + +Or, to contact the author, send mail to bitserf@gmail.com + diff --git a/vendor/plugins/actionwebservice/Rakefile b/vendor/plugins/actionwebservice/Rakefile new file mode 100644 index 000000000..ad2ad223e --- /dev/null +++ b/vendor/plugins/actionwebservice/Rakefile @@ -0,0 +1,172 @@ +require 'rubygems' +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' +require 'rake/packagetask' +require 'rake/gempackagetask' +require 'rake/contrib/rubyforgepublisher' +require 'fileutils' +require File.join(File.dirname(__FILE__), 'lib', 'action_web_service', 'version') + +PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' +PKG_NAME = 'actionwebservice' +PKG_VERSION = ActionWebService::VERSION::STRING + PKG_BUILD +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" +PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}" + +RELEASE_NAME = "REL #{PKG_VERSION}" + +RUBY_FORGE_PROJECT = "aws" +RUBY_FORGE_USER = "webster132" + +desc "Default Task" +task :default => [ :test ] + + +# Run the unit tests +Rake::TestTask.new { |t| + t.libs << "test" + t.test_files = Dir['test/*_test.rb'] + t.verbose = true +} + +SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions)) + +desc 'Build the MySQL test database' +task :build_database do + %x( mysqladmin create actionwebservice_unittest ) + %x( mysql actionwebservice_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} ) +end + + +# Generate the RDoc documentation +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "Action Web Service -- Web services for Action Pack" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.options << '--charset' << 'utf-8' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('CHANGELOG') + rdoc.rdoc_files.include('lib/action_web_service.rb') + rdoc.rdoc_files.include('lib/action_web_service/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/api/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/client/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/container/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/dispatcher/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/protocol/*.rb') + rdoc.rdoc_files.include('lib/action_web_service/support/*.rb') +} + + +# Create compressed packages +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = PKG_NAME + s.summary = "Web service support for Action Pack." + s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack} + s.version = PKG_VERSION + + s.author = "Leon Breedt" + s.email = "bitserf@gmail.com" + s.rubyforge_project = "aws" + s.homepage = "http://www.rubyonrails.org" + + s.add_dependency('actionpack', '= 1.13.5' + PKG_BUILD) + s.add_dependency('activerecord', '= 1.15.5' + PKG_BUILD) + + s.has_rdoc = true + s.requirements << 'none' + s.require_path = 'lib' + s.autorequire = 'action_web_service' + + s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ] + s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) } + s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) } +end +Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = true + p.need_zip = true +end + + +# Publish beta gem +desc "Publish the API documentation" +task :pgem => [:package] do + Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload + `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'` +end + +# Publish documentation +desc "Publish the API documentation" +task :pdoc => [:rdoc] do + Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/aws", "doc").upload +end + + +def each_source_file(*args) + prefix, includes, excludes, open_file = args + prefix ||= File.dirname(__FILE__) + open_file = true if open_file.nil? + includes ||= %w[lib\/action_web_service\.rb$ lib\/action_web_service\/.*\.rb$] + excludes ||= %w[lib\/action_web_service\/vendor] + Find.find(prefix) do |file_name| + next if file_name =~ /\.svn/ + file_name.gsub!(/^\.\//, '') + continue = false + includes.each do |inc| + if file_name.match(/#{inc}/) + continue = true + break + end + end + next unless continue + excludes.each do |exc| + if file_name.match(/#{exc}/) + continue = false + break + end + end + next unless continue + if open_file + File.open(file_name) do |f| + yield file_name, f + end + else + yield file_name + end + end +end + +desc "Count lines of the AWS source code" +task :lines do + total_lines = total_loc = 0 + puts "Per File:" + each_source_file do |file_name, f| + file_lines = file_loc = 0 + while line = f.gets + file_lines += 1 + next if line =~ /^\s*$/ + next if line =~ /^\s*#/ + file_loc += 1 + end + puts " #{file_name}: Lines #{file_lines}, LOC #{file_loc}" + total_lines += file_lines + total_loc += file_loc + end + puts "Total:" + puts " Lines #{total_lines}, LOC #{total_loc}" +end + +desc "Publish the release files to RubyForge." +task :release => [ :package ] do + require 'rubyforge' + + packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" } + + rubyforge = RubyForge.new + rubyforge.login + rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages) +end diff --git a/vendor/plugins/actionwebservice/TODO b/vendor/plugins/actionwebservice/TODO new file mode 100644 index 000000000..7c022c14c --- /dev/null +++ b/vendor/plugins/actionwebservice/TODO @@ -0,0 +1,32 @@ += Post-1.0 + - Document/Literal SOAP support + - URL-based dispatching, URL identifies method + + - Add :rest dispatching mode, a.l.a. Backpack API. Clean up dispatching + in general. Support vanilla XML-format as a "Rails" protocol? + XML::Simple deserialization into params? + + web_service_dispatching_mode :rest + + def method1(params) + end + + def method2(params) + end + + + /ws/method1 + + /ws/method2 + + + - Allow locking down a controller to only accept messages for a particular + protocol. This will allow us to generate fully conformant error messages + in cases where we currently fudge it if we don't know the protocol. + + - Allow AWS user to participate in typecasting, so they can centralize + workarounds for buggy input in one place + += Refactoring + - Don't have clean way to go from SOAP Class object to the xsd:NAME type + string -- NaHi possibly looking at remedying this situation diff --git a/vendor/plugins/actionwebservice/init.rb b/vendor/plugins/actionwebservice/init.rb new file mode 100644 index 000000000..582f73717 --- /dev/null +++ b/vendor/plugins/actionwebservice/init.rb @@ -0,0 +1,7 @@ +require 'action_web_service' + +# These need to be in the load path for action_web_service to work +Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"] + +# AWS Test helpers +require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] == 'test' diff --git a/vendor/plugins/actionwebservice/install.rb b/vendor/plugins/actionwebservice/install.rb new file mode 100644 index 000000000..da08bf5f9 --- /dev/null +++ b/vendor/plugins/actionwebservice/install.rb @@ -0,0 +1,30 @@ +require 'rbconfig' +require 'find' +require 'ftools' + +include Config + +# this was adapted from rdoc's install.rb by way of Log4r + +$sitedir = CONFIG["sitelibdir"] +unless $sitedir + version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"] + $libdir = File.join(CONFIG["libdir"], "ruby", version) + $sitedir = $:.find {|x| x =~ /site_ruby/ } + if !$sitedir + $sitedir = File.join($libdir, "site_ruby") + elsif $sitedir !~ Regexp.quote(version) + $sitedir = File.join($sitedir, version) + end +end + +# the actual gruntwork +Dir.chdir("lib") + +Find.find("action_web_service", "action_web_service.rb") { |f| + if f[-3..-1] == ".rb" + File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true) + else + File::makedirs(File.join($sitedir, *f.split(/\//))) + end +} diff --git a/vendor/plugins/actionwebservice/lib/action_web_service.rb b/vendor/plugins/actionwebservice/lib/action_web_service.rb new file mode 100644 index 000000000..0632dd1ec --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service.rb @@ -0,0 +1,66 @@ +#-- +# Copyright (C) 2005 Leon Breedt +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +begin + require 'active_support' + require 'action_controller' + require 'active_record' +rescue LoadError + require 'rubygems' + gem 'activesupport', '>= 1.0.2' + gem 'actionpack', '>= 1.6.0' + gem 'activerecord', '>= 1.9.0' +end + +$:.unshift(File.dirname(__FILE__) + "/action_web_service/vendor/") + +require 'action_web_service/support/class_inheritable_options' +require 'action_web_service/support/signature_types' +require 'action_web_service/base' +require 'action_web_service/client' +require 'action_web_service/invocation' +require 'action_web_service/api' +require 'action_web_service/casting' +require 'action_web_service/struct' +require 'action_web_service/container' +require 'action_web_service/protocol' +require 'action_web_service/dispatcher' +require 'action_web_service/scaffolding' + +ActionWebService::Base.class_eval do + include ActionWebService::Container::Direct + include ActionWebService::Invocation +end + +ActionController::Base.class_eval do + include ActionWebService::Protocol::Discovery + include ActionWebService::Protocol::Soap + include ActionWebService::Protocol::XmlRpc + include ActionWebService::Container::Direct + include ActionWebService::Container::Delegated + include ActionWebService::Container::ActionController + include ActionWebService::Invocation + include ActionWebService::Dispatcher + include ActionWebService::Dispatcher::ActionController + include ActionWebService::Scaffolding +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/api.rb b/vendor/plugins/actionwebservice/lib/action_web_service/api.rb new file mode 100644 index 000000000..d16dc420d --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/api.rb @@ -0,0 +1,297 @@ +module ActionWebService # :nodoc: + module API # :nodoc: + # A web service API class specifies the methods that will be available for + # invocation for an API. It also contains metadata such as the method type + # signature hints. + # + # It is not intended to be instantiated. + # + # It is attached to web service implementation classes like + # ActionWebService::Base and ActionController::Base derivatives by using + # container.web_service_api, where container is an + # ActionController::Base or a ActionWebService::Base. + # + # See ActionWebService::Container::Direct::ClassMethods for an example + # of use. + class Base + # Whether to transform the public API method names into camel-cased names + class_inheritable_option :inflect_names, true + + # By default only HTTP POST requests are processed + class_inheritable_option :allowed_http_methods, [ :post ] + + # Whether to allow ActiveRecord::Base models in :expects. + # The default is +false+; you should be aware of the security implications + # of allowing this, and ensure that you don't allow remote callers to + # easily overwrite data they should not have access to. + class_inheritable_option :allow_active_record_expects, false + + # If present, the name of a method to call when the remote caller + # tried to call a nonexistent method. Semantically equivalent to + # +method_missing+. + class_inheritable_option :default_api_method + + # Disallow instantiation + private_class_method :new, :allocate + + class << self + include ActionWebService::SignatureTypes + + # API methods have a +name+, which must be the Ruby method name to use when + # performing the invocation on the web service object. + # + # The signatures for the method input parameters and return value can + # by specified in +options+. + # + # A signature is an array of one or more parameter specifiers. + # A parameter specifier can be one of the following: + # + # * A symbol or string representing one of the Action Web Service base types. + # See ActionWebService::SignatureTypes for a canonical list of the base types. + # * The Class object of the parameter type + # * A single-element Array containing one of the two preceding items. This + # will cause Action Web Service to treat the parameter at that position + # as an array containing only values of the given type. + # * A Hash containing as key the name of the parameter, and as value + # one of the three preceding items + # + # If no method input parameter or method return value signatures are given, + # the method is assumed to take no parameters and/or return no values of + # interest, and any values that are received by the server will be + # discarded and ignored. + # + # Valid options: + # [:expects] Signature for the method input parameters + # [:returns] Signature for the method return value + # [:expects_and_returns] Signature for both input parameters and return value + def api_method(name, options={}) + unless options.is_a?(Hash) + raise(ActionWebServiceError, "Expected a Hash for options") + end + validate_options([:expects, :returns, :expects_and_returns], options.keys) + if options[:expects_and_returns] + expects = options[:expects_and_returns] + returns = options[:expects_and_returns] + else + expects = options[:expects] + returns = options[:returns] + end + expects = canonical_signature(expects) + returns = canonical_signature(returns) + if expects + expects.each do |type| + type = type.element_type if type.is_a?(ArrayType) + if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects + raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects") + end + end + end + name = name.to_sym + public_name = public_api_method_name(name) + method = Method.new(name, public_name, expects, returns) + write_inheritable_hash("api_methods", name => method) + write_inheritable_hash("api_public_method_names", public_name => name) + end + + # Whether the given method name is a service method on this API + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # end + # + # ProjectsApi.has_api_method?('GetCount') #=> false + # ProjectsApi.has_api_method?(:getCount) #=> true + def has_api_method?(name) + api_methods.has_key?(name) + end + + # Whether the given public method name has a corresponding service method + # on this API + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # end + # + # ProjectsApi.has_api_method?(:getCount) #=> false + # ProjectsApi.has_api_method?('GetCount') #=> true + def has_public_api_method?(public_name) + api_public_method_names.has_key?(public_name) + end + + # The corresponding public method name for the given service method name + # + # ProjectsApi.public_api_method_name('GetCount') #=> "GetCount" + # ProjectsApi.public_api_method_name(:getCount) #=> "GetCount" + def public_api_method_name(name) + if inflect_names + name.to_s.camelize + else + name.to_s + end + end + + # The corresponding service method name for the given public method name + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # end + # + # ProjectsApi.api_method_name('GetCount') #=> :getCount + def api_method_name(public_name) + api_public_method_names[public_name] + end + + # A Hash containing all service methods on this API, and their + # associated metadata. + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # api_method :getCompletedCount, :returns => [:int] + # end + # + # ProjectsApi.api_methods #=> + # {:getCount=>#, + # :getCompletedCount=>#} + # ProjectsApi.api_methods[:getCount].public_name #=> "GetCount" + def api_methods + read_inheritable_attribute("api_methods") || {} + end + + # The Method instance for the given public API method name, if any + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # api_method :getCompletedCount, :returns => [:int] + # end + # + # ProjectsApi.public_api_method_instance('GetCount') #=> <# + # ProjectsApi.public_api_method_instance(:getCount) #=> nil + def public_api_method_instance(public_method_name) + api_method_instance(api_method_name(public_method_name)) + end + + # The Method instance for the given API method name, if any + # + # class ProjectsApi < ActionWebService::API::Base + # api_method :getCount, :returns => [:int] + # api_method :getCompletedCount, :returns => [:int] + # end + # + # ProjectsApi.api_method_instance(:getCount) #=> + # ProjectsApi.api_method_instance('GetCount') #=> + def api_method_instance(method_name) + api_methods[method_name] + end + + # The Method instance for the default API method, if any + def default_api_method_instance + return nil unless name = default_api_method + instance = read_inheritable_attribute("default_api_method_instance") + if instance && instance.name == name + return instance + end + instance = Method.new(name, public_api_method_name(name), nil, nil) + write_inheritable_attribute("default_api_method_instance", instance) + instance + end + + private + def api_public_method_names + read_inheritable_attribute("api_public_method_names") || {} + end + + def validate_options(valid_option_keys, supplied_option_keys) + unknown_option_keys = supplied_option_keys - valid_option_keys + unless unknown_option_keys.empty? + raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}") + end + end + end + end + + # Represents an API method and its associated metadata, and provides functionality + # to assist in commonly performed API method tasks. + class Method + attr :name + attr :public_name + attr :expects + attr :returns + + def initialize(name, public_name, expects, returns) + @name = name + @public_name = public_name + @expects = expects + @returns = returns + @caster = ActionWebService::Casting::BaseCaster.new(self) + end + + # The list of parameter names for this method + def param_names + return [] unless @expects + @expects.map{ |type| type.name } + end + + # Casts a set of Ruby values into the expected Ruby values + def cast_expects(params) + @caster.cast_expects(params) + end + + # Cast a Ruby return value into the expected Ruby value + def cast_returns(return_value) + @caster.cast_returns(return_value) + end + + # Returns the index of the first expected parameter + # with the given name + def expects_index_of(param_name) + return -1 if @expects.nil? + (0..(@expects.length-1)).each do |i| + return i if @expects[i].name.to_s == param_name.to_s + end + -1 + end + + # Returns a hash keyed by parameter name for the given + # parameter list + def expects_to_hash(params) + return {} if @expects.nil? + h = {} + @expects.zip(params){ |type, param| h[type.name] = param } + h + end + + # Backwards compatibility with previous API + def [](sig_type) + case sig_type + when :expects + @expects.map{|x| compat_signature_entry(x)} + when :returns + @returns.map{|x| compat_signature_entry(x)} + end + end + + # String representation of this method + def to_s + fqn = "" + fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ") + fqn << "#{@public_name}(" + fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects + fqn << ")" + fqn + end + + private + def compat_signature_entry(entry) + if entry.array? + [compat_signature_entry(entry.element_type)] + else + if entry.spec.is_a?(Hash) + {entry.spec.keys.first => entry.type_class} + else + entry.type_class + end + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/base.rb b/vendor/plugins/actionwebservice/lib/action_web_service/base.rb new file mode 100644 index 000000000..6282061d8 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/base.rb @@ -0,0 +1,38 @@ +module ActionWebService # :nodoc: + class ActionWebServiceError < StandardError # :nodoc: + end + + # An Action Web Service object implements a specified API. + # + # Used by controllers operating in _Delegated_ dispatching mode. + # + # ==== Example + # + # class PersonService < ActionWebService::Base + # web_service_api PersonAPI + # + # def find_person(criteria) + # Person.find(:all) [...] + # end + # + # def delete_person(id) + # Person.find_by_id(id).destroy + # end + # end + # + # class PersonAPI < ActionWebService::API::Base + # api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]] + # api_method :delete_person, :expects => [:int] + # end + # + # class SearchCriteria < ActionWebService::Struct + # member :firstname, :string + # member :lastname, :string + # member :email, :string + # end + class Base + # Whether to report exceptions back to the caller in the protocol's exception + # format + class_inheritable_option :web_service_exception_reporting, true + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/casting.rb b/vendor/plugins/actionwebservice/lib/action_web_service/casting.rb new file mode 100644 index 000000000..71f422eae --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/casting.rb @@ -0,0 +1,138 @@ +require 'time' +require 'date' +require 'xmlrpc/datetime' + +module ActionWebService # :nodoc: + module Casting # :nodoc: + class CastingError < ActionWebServiceError # :nodoc: + end + + # Performs casting of arbitrary values into the correct types for the signature + class BaseCaster # :nodoc: + def initialize(api_method) + @api_method = api_method + end + + # Coerces the parameters in +params+ (an Enumerable) into the types + # this method expects + def cast_expects(params) + self.class.cast_expects(@api_method, params) + end + + # Coerces the given +return_value+ into the type returned by this + # method + def cast_returns(return_value) + self.class.cast_returns(@api_method, return_value) + end + + class << self + include ActionWebService::SignatureTypes + + def cast_expects(api_method, params) # :nodoc: + return [] if api_method.expects.nil? + api_method.expects.zip(params).map{ |type, param| cast(param, type) } + end + + def cast_returns(api_method, return_value) # :nodoc: + return nil if api_method.returns.nil? + cast(return_value, api_method.returns[0]) + end + + def cast(value, signature_type) # :nodoc: + return value if signature_type.nil? # signature.length != params.length + return nil if value.nil? + # XMLRPC protocol doesn't support nil values. It uses false instead. + # It should never happen for SOAP. + if signature_type.structured? && value.equal?(false) + return nil + end + unless signature_type.array? || signature_type.structured? + return value if canonical_type(value.class) == signature_type.type + end + if signature_type.array? + unless value.respond_to?(:entries) && !value.is_a?(String) + raise CastingError, "Don't know how to cast #{value.class} into #{signature_type.type.inspect}" + end + value.entries.map do |entry| + cast(entry, signature_type.element_type) + end + elsif signature_type.structured? + cast_to_structured_type(value, signature_type) + elsif !signature_type.custom? + cast_base_type(value, signature_type) + end + end + + def cast_base_type(value, signature_type) # :nodoc: + # This is a work-around for the fact that XML-RPC special-cases DateTime values into its own DateTime type + # in order to support iso8601 dates. This doesn't work too well for us, so we'll convert it into a Time, + # with the caveat that we won't be able to handle pre-1970 dates that are sent to us. + # + # See http://dev.rubyonrails.com/ticket/2516 + value = value.to_time if value.is_a?(XMLRPC::DateTime) + + case signature_type.type + when :int + Integer(value) + when :string + value.to_s + when :base64 + if value.is_a?(ActionWebService::Base64) + value + else + ActionWebService::Base64.new(value.to_s) + end + when :bool + return false if value.nil? + return value if value == true || value == false + case value.to_s.downcase + when '1', 'true', 'y', 'yes' + true + when '0', 'false', 'n', 'no' + false + else + raise CastingError, "Don't know how to cast #{value.class} into Boolean" + end + when :float + Float(value) + when :decimal + BigDecimal(value.to_s) + when :time + value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash) + value.kind_of?(Time) ? value : Time.parse(value.to_s) + when :date + value = "%s/%s/%s" % value.values_at(*%w[2 3 1]) if value.kind_of?(Hash) + value.kind_of?(Date) ? value : Date.parse(value.to_s) + when :datetime + value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash) + value.kind_of?(DateTime) ? value : DateTime.parse(value.to_s) + end + end + + def cast_to_structured_type(value, signature_type) # :nodoc: + obj = nil + obj = value if canonical_type(value.class) == canonical_type(signature_type.type) + obj ||= signature_type.type_class.new + if value.respond_to?(:each_pair) + klass = signature_type.type_class + value.each_pair do |name, val| + type = klass.respond_to?(:member_type) ? klass.member_type(name) : nil + val = cast(val, type) if type + # See http://dev.rubyonrails.com/ticket/3567 + val = val.to_time if val.is_a?(XMLRPC::DateTime) + obj.__send__("#{name}=", val) if obj.respond_to?(name) + end + elsif value.respond_to?(:attributes) + signature_type.each_member do |name, type| + val = value.__send__(name) + obj.__send__("#{name}=", cast(val, type)) if obj.respond_to?(name) + end + else + raise CastingError, "Don't know how to cast #{value.class} to #{signature_type.type_class}" + end + obj + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/client.rb b/vendor/plugins/actionwebservice/lib/action_web_service/client.rb new file mode 100644 index 000000000..2a1e33054 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/client.rb @@ -0,0 +1,3 @@ +require 'action_web_service/client/base' +require 'action_web_service/client/soap_client' +require 'action_web_service/client/xmlrpc_client' diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb b/vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb new file mode 100644 index 000000000..9dada7bf9 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/client/base.rb @@ -0,0 +1,28 @@ +module ActionWebService # :nodoc: + module Client # :nodoc: + class ClientError < StandardError # :nodoc: + end + + class Base # :nodoc: + def initialize(api, endpoint_uri) + @api = api + @endpoint_uri = endpoint_uri + end + + def method_missing(name, *args) # :nodoc: + call_name = method_name(name) + return super(name, *args) if call_name.nil? + self.perform_invocation(call_name, args) + end + + private + def method_name(name) + if @api.has_api_method?(name.to_sym) + name.to_s + elsif @api.has_public_api_method?(name.to_s) + @api.api_method_name(name.to_s).to_s + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb b/vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb new file mode 100644 index 000000000..ebabd8ea8 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/client/soap_client.rb @@ -0,0 +1,113 @@ +require 'soap/rpc/driver' +require 'uri' + +module ActionWebService # :nodoc: + module Client # :nodoc: + + # Implements SOAP client support (using RPC encoding for the messages). + # + # ==== Example Usage + # + # class PersonAPI < ActionWebService::API::Base + # api_method :find_all, :returns => [[Person]] + # end + # + # soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...") + # persons = soap_client.find_all + # + class Soap < Base + # provides access to the underlying soap driver + attr_reader :driver + + # Creates a new web service client using the SOAP RPC protocol. + # + # +api+ must be an ActionWebService::API::Base derivative, and + # +endpoint_uri+ must point at the relevant URL to which protocol requests + # will be sent with HTTP POST. + # + # Valid options: + # [:namespace] If the remote server has used a custom namespace to + # declare its custom types, you can specify it here. This would + # be the namespace declared with a [WebService(Namespace = "http://namespace")] attribute + # in .NET, for example. + # [:driver_options] If you want to supply any custom SOAP RPC driver + # options, you can provide them as a Hash here + # + # The :driver_options option can be used to configure the backend SOAP + # RPC driver. An example of configuring the SOAP backend to do + # client-certificate authenticated SSL connections to the server: + # + # opts = {} + # opts['protocol.http.ssl_config.verify_mode'] = 'OpenSSL::SSL::VERIFY_PEER' + # opts['protocol.http.ssl_config.client_cert'] = client_cert_file_path + # opts['protocol.http.ssl_config.client_key'] = client_key_file_path + # opts['protocol.http.ssl_config.ca_file'] = ca_cert_file_path + # client = ActionWebService::Client::Soap.new(api, 'https://some/service', :driver_options => opts) + def initialize(api, endpoint_uri, options={}) + super(api, endpoint_uri) + @namespace = options[:namespace] || 'urn:ActionWebService' + @driver_options = options[:driver_options] || {} + @protocol = ActionWebService::Protocol::Soap::SoapProtocol.new @namespace + @soap_action_base = options[:soap_action_base] + @soap_action_base ||= URI.parse(endpoint_uri).path + @driver = create_soap_rpc_driver(api, endpoint_uri) + @driver_options.each do |name, value| + @driver.options[name.to_s] = value + end + end + + protected + def perform_invocation(method_name, args) + method = @api.api_methods[method_name.to_sym] + args = method.cast_expects(args.dup) rescue args + return_value = @driver.send(method_name, *args) + method.cast_returns(return_value.dup) rescue return_value + end + + def soap_action(method_name) + "#{@soap_action_base}/#{method_name}" + end + + private + def create_soap_rpc_driver(api, endpoint_uri) + @protocol.register_api(api) + driver = SoapDriver.new(endpoint_uri, nil) + driver.mapping_registry = @protocol.marshaler.registry + api.api_methods.each do |name, method| + qname = XSD::QName.new(@namespace, method.public_name) + action = soap_action(method.public_name) + expects = method.expects + returns = method.returns + param_def = [] + if expects + expects.each do |type| + type_binding = @protocol.marshaler.lookup_type(type) + if SOAP::Version >= "1.5.5" + param_def << ['in', type.name.to_s, [type_binding.type.type_class.to_s]] + else + param_def << ['in', type.name, type_binding.mapping] + end + end + end + if returns + type_binding = @protocol.marshaler.lookup_type(returns[0]) + if SOAP::Version >= "1.5.5" + param_def << ['retval', 'return', [type_binding.type.type_class.to_s]] + else + param_def << ['retval', 'return', type_binding.mapping] + end + end + driver.add_method(qname, action, method.name.to_s, param_def) + end + driver + end + + class SoapDriver < SOAP::RPC::Driver # :nodoc: + def add_method(qname, soapaction, name, param_def) + @proxy.add_rpc_method(qname, soapaction, name, param_def) + add_rpc_method_interface(name, param_def) + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb b/vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb new file mode 100644 index 000000000..42b5c5d4f --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/client/xmlrpc_client.rb @@ -0,0 +1,58 @@ +require 'uri' +require 'xmlrpc/client' + +module ActionWebService # :nodoc: + module Client # :nodoc: + + # Implements XML-RPC client support + # + # ==== Example Usage + # + # class BloggerAPI < ActionWebService::API::Base + # inflect_names false + # api_method :getRecentPosts, :returns => [[Blog::Post]] + # end + # + # blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger") + # posts = blog.getRecentPosts + class XmlRpc < Base + + # Creates a new web service client using the XML-RPC protocol. + # + # +api+ must be an ActionWebService::API::Base derivative, and + # +endpoint_uri+ must point at the relevant URL to which protocol requests + # will be sent with HTTP POST. + # + # Valid options: + # [:handler_name] If the remote server defines its services inside special + # handler (the Blogger API uses a "blogger" handler name for example), + # provide it here, or your method calls will fail + def initialize(api, endpoint_uri, options={}) + @api = api + @handler_name = options[:handler_name] + @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new + @client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout]) + end + + protected + def perform_invocation(method_name, args) + method = @api.api_methods[method_name.to_sym] + if method.expects && method.expects.length != args.length + raise(ArgumentError, "#{method.public_name}: wrong number of arguments (#{args.length} for #{method.expects.length})") + end + args = method.cast_expects(args.dup) rescue args + if method.expects + method.expects.each_with_index{ |type, i| args[i] = @protocol.value_to_xmlrpc_wire_format(args[i], type) } + end + ok, return_value = @client.call2(public_name(method_name), *args) + return (method.cast_returns(return_value.dup) rescue return_value) if ok + raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}") + end + + def public_name(method_name) + public_name = @api.public_api_method_name(method_name) + @handler_name ? "#{@handler_name}.#{public_name}" : public_name + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/container.rb b/vendor/plugins/actionwebservice/lib/action_web_service/container.rb new file mode 100644 index 000000000..13d9d8ab5 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/container.rb @@ -0,0 +1,3 @@ +require 'action_web_service/container/direct_container' +require 'action_web_service/container/delegated_container' +require 'action_web_service/container/action_controller_container' diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb b/vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb new file mode 100644 index 000000000..bbc28083c --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/container/action_controller_container.rb @@ -0,0 +1,93 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module ActionController # :nodoc: + def self.included(base) # :nodoc: + class << base + include ClassMethods + alias_method_chain :inherited, :api + alias_method_chain :web_service_api, :require + end + end + + module ClassMethods + # Creates a client for accessing remote web services, using the + # given +protocol+ to communicate with the +endpoint_uri+. + # + # ==== Example + # + # class MyController < ActionController::Base + # web_client_api :blogger, :xmlrpc, "http://blogger.com/myblog/api/RPC2", :handler_name => 'blogger' + # end + # + # In this example, a protected method named blogger will + # now exist on the controller, and calling it will return the + # XML-RPC client object for working with that remote service. + # + # +options+ is the set of protocol client specific options (see + # a protocol client class for details). + # + # If your API definition does not exist on the load path with the + # correct rules for it to be found using +name+, you can pass in + # the API definition class via +options+, using a key of :api + def web_client_api(name, protocol, endpoint_uri, options={}) + unless method_defined?(name) + api_klass = options.delete(:api) || require_web_service_api(name) + class_eval do + define_method(name) do + create_web_service_client(api_klass, protocol, endpoint_uri, options) + end + protected name + end + end + end + + def web_service_api_with_require(definition=nil) # :nodoc: + return web_service_api_without_require if definition.nil? + case definition + when String, Symbol + klass = require_web_service_api(definition) + else + klass = definition + end + web_service_api_without_require(klass) + end + + def require_web_service_api(name) # :nodoc: + case name + when String, Symbol + file_name = name.to_s.underscore + "_api" + class_name = file_name.camelize + class_names = [class_name, class_name.sub(/Api$/, 'API')] + begin + require_dependency(file_name) + rescue LoadError => load_error + requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1] + msg = requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}" + raise LoadError.new(msg).copy_blame!(load_error) + end + klass = nil + class_names.each do |name| + klass = name.constantize rescue nil + break unless klass.nil? + end + unless klass + raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found") + end + klass + else + raise(ArgumentError, "expected String or Symbol argument") + end + end + + private + def inherited_with_api(child) + inherited_without_api(child) + begin child.web_service_api(child.controller_path) + rescue MissingSourceFile => e + raise unless e.is_missing?("apis/#{child.controller_path}_api") + end + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb b/vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb new file mode 100644 index 000000000..5477f8d10 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/container/delegated_container.rb @@ -0,0 +1,86 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module Delegated # :nodoc: + class ContainerError < ActionWebServiceError # :nodoc: + end + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + base.send(:include, ActionWebService::Container::Delegated::InstanceMethods) + end + + module ClassMethods + # Declares a web service that will provide access to the API of the given + # +object+. +object+ must be an ActionWebService::Base derivative. + # + # Web service object creation can either be _immediate_, where the object + # instance is given at class definition time, or _deferred_, where + # object instantiation is delayed until request time. + # + # ==== Immediate web service object example + # + # class ApiController < ApplicationController + # web_service_dispatching_mode :delegated + # + # web_service :person, PersonService.new + # end + # + # For deferred instantiation, a block should be given instead of an + # object instance. This block will be executed in controller instance + # context, so it can rely on controller instance variables being present. + # + # ==== Deferred web service object example + # + # class ApiController < ApplicationController + # web_service_dispatching_mode :delegated + # + # web_service(:person) { PersonService.new(request.env) } + # end + def web_service(name, object=nil, &block) + if (object && block_given?) || (object.nil? && block.nil?) + raise(ContainerError, "either service, or a block must be given") + end + name = name.to_sym + if block_given? + info = { name => { :block => block } } + else + info = { name => { :object => object } } + end + write_inheritable_hash("web_services", info) + call_web_service_definition_callbacks(self, name, info) + end + + # Whether this service contains a service with the given +name+ + def has_web_service?(name) + web_services.has_key?(name.to_sym) + end + + def web_services # :nodoc: + read_inheritable_attribute("web_services") || {} + end + + def add_web_service_definition_callback(&block) # :nodoc: + write_inheritable_array("web_service_definition_callbacks", [block]) + end + + private + def call_web_service_definition_callbacks(container_class, web_service_name, service_info) + (read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block| + block.call(container_class, web_service_name, service_info) + end + end + end + + module InstanceMethods # :nodoc: + def web_service_object(web_service_name) + info = self.class.web_services[web_service_name.to_sym] + unless info + raise(ContainerError, "no such web service '#{web_service_name}'") + end + service = info[:block] + service ? self.instance_eval(&service) : info[:object] + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb b/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb new file mode 100644 index 000000000..8818d8f45 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb @@ -0,0 +1,69 @@ +module ActionWebService # :nodoc: + module Container # :nodoc: + module Direct # :nodoc: + class ContainerError < ActionWebServiceError # :nodoc: + end + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + end + + module ClassMethods + # Attaches ActionWebService API +definition+ to the calling class. + # + # Action Controllers can have a default associated API, removing the need + # to call this method if you follow the Action Web Service naming conventions. + # + # A controller with a class name of GoogleSearchController will + # implicitly load app/apis/google_search_api.rb, and expect the + # API definition class to be named GoogleSearchAPI or + # GoogleSearchApi. + # + # ==== Service class example + # + # class MyService < ActionWebService::Base + # web_service_api MyAPI + # end + # + # class MyAPI < ActionWebService::API::Base + # ... + # end + # + # ==== Controller class example + # + # class MyController < ActionController::Base + # web_service_api MyAPI + # end + # + # class MyAPI < ActionWebService::API::Base + # ... + # end + def web_service_api(definition=nil) + if definition.nil? + read_inheritable_attribute("web_service_api") + else + if definition.is_a?(Symbol) + raise(ContainerError, "symbols can only be used for #web_service_api inside of a controller") + end + unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base) + raise(ContainerError, "#{definition.to_s} is not a valid API definition") + end + write_inheritable_attribute("web_service_api", definition) + call_web_service_api_callbacks(self, definition) + end + end + + def add_web_service_api_callback(&block) # :nodoc: + write_inheritable_array("web_service_api_callbacks", [block]) + end + + private + def call_web_service_api_callbacks(container_class, definition) + (read_inheritable_attribute("web_service_api_callbacks") || []).each do |block| + block.call(container_class, definition) + end + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb b/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb new file mode 100644 index 000000000..601d83137 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher.rb @@ -0,0 +1,2 @@ +require 'action_web_service/dispatcher/abstract' +require 'action_web_service/dispatcher/action_controller_dispatcher' diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb b/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb new file mode 100644 index 000000000..cb94d649e --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/abstract.rb @@ -0,0 +1,207 @@ +require 'benchmark' + +module ActionWebService # :nodoc: + module Dispatcher # :nodoc: + class DispatcherError < ActionWebService::ActionWebServiceError # :nodoc: + def initialize(*args) + super + set_backtrace(caller) + end + end + + def self.included(base) # :nodoc: + base.class_inheritable_option(:web_service_dispatching_mode, :direct) + base.class_inheritable_option(:web_service_exception_reporting, true) + base.send(:include, ActionWebService::Dispatcher::InstanceMethods) + end + + module InstanceMethods # :nodoc: + private + def invoke_web_service_request(protocol_request) + invocation = web_service_invocation(protocol_request) + if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol) + xmlrpc_multicall_invoke(invocation) + else + web_service_invoke(invocation) + end + end + + def web_service_direct_invoke(invocation) + @method_params = invocation.method_ordered_params + arity = method(invocation.api_method.name).arity rescue 0 + if arity < 0 || arity > 0 + params = @method_params + else + params = [] + end + web_service_filtered_invoke(invocation, params) + end + + def web_service_delegated_invoke(invocation) + web_service_filtered_invoke(invocation, invocation.method_ordered_params) + end + + def web_service_filtered_invoke(invocation, params) + cancellation_reason = nil + return_value = invocation.service.perform_invocation(invocation.api_method.name, params) do |x| + cancellation_reason = x + end + if cancellation_reason + raise(DispatcherError, "request canceled: #{cancellation_reason}") + end + return_value + end + + def web_service_invoke(invocation) + case web_service_dispatching_mode + when :direct + return_value = web_service_direct_invoke(invocation) + when :delegated, :layered + return_value = web_service_delegated_invoke(invocation) + end + web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value) + end + + def xmlrpc_multicall_invoke(invocations) + responses = [] + invocations.each do |invocation| + if invocation.is_a?(Hash) + responses << [invocation, nil] + next + end + begin + case web_service_dispatching_mode + when :direct + return_value = web_service_direct_invoke(invocation) + when :delegated, :layered + return_value = web_service_delegated_invoke(invocation) + end + api_method = invocation.api_method + if invocation.api.has_api_method?(api_method.name) + response_type = (api_method.returns ? api_method.returns[0] : nil) + return_value = api_method.cast_returns(return_value) + else + response_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0) + end + responses << [return_value, response_type] + rescue Exception => e + responses << [{ 'faultCode' => 3, 'faultString' => e.message }, nil] + end + end + invocation = invocations[0] + invocation.protocol.encode_multicall_response(responses, invocation.protocol_options) + end + + def web_service_invocation(request, level = 0) + public_method_name = request.method_name + invocation = Invocation.new + invocation.protocol = request.protocol + invocation.protocol_options = request.protocol_options + invocation.service_name = request.service_name + if web_service_dispatching_mode == :layered + case invocation.protocol + when Protocol::Soap::SoapProtocol + soap_action = request.protocol_options[:soap_action] + if soap_action && soap_action =~ /^\/\w+\/(\w+)\// + invocation.service_name = $1 + end + when Protocol::XmlRpc::XmlRpcProtocol + if request.method_name =~ /^([^\.]+)\.(.*)$/ + public_method_name = $2 + invocation.service_name = $1 + end + end + end + if invocation.protocol.is_a? Protocol::XmlRpc::XmlRpcProtocol + if public_method_name == 'multicall' && invocation.service_name == 'system' + if level > 0 + raise(DispatcherError, "Recursive system.multicall invocations not allowed") + end + multicall = request.method_params.dup + unless multicall.is_a?(Array) && multicall[0].is_a?(Array) + raise(DispatcherError, "Malformed multicall (expected array of Hash elements)") + end + multicall = multicall[0] + return multicall.map do |item| + raise(DispatcherError, "Multicall elements must be Hash") unless item.is_a?(Hash) + raise(DispatcherError, "Multicall elements must contain a 'methodName' key") unless item.has_key?('methodName') + method_name = item['methodName'] + params = item.has_key?('params') ? item['params'] : [] + multicall_request = request.dup + multicall_request.method_name = method_name + multicall_request.method_params = params + begin + web_service_invocation(multicall_request, level + 1) + rescue Exception => e + {'faultCode' => 4, 'faultMessage' => e.message} + end + end + end + end + case web_service_dispatching_mode + when :direct + invocation.api = self.class.web_service_api + invocation.service = self + when :delegated, :layered + invocation.service = web_service_object(invocation.service_name) + invocation.api = invocation.service.class.web_service_api + end + if invocation.api.nil? + raise(DispatcherError, "no API attached to #{invocation.service.class}") + end + invocation.protocol.register_api(invocation.api) + request.api = invocation.api + if invocation.api.has_public_api_method?(public_method_name) + invocation.api_method = invocation.api.public_api_method_instance(public_method_name) + else + if invocation.api.default_api_method.nil? + raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}") + else + invocation.api_method = invocation.api.default_api_method_instance + end + end + if invocation.service.nil? + raise(DispatcherError, "no service available for service name #{invocation.service_name}") + end + unless invocation.service.respond_to?(invocation.api_method.name) + raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method.name})") + end + request.api_method = invocation.api_method + begin + invocation.method_ordered_params = invocation.api_method.cast_expects(request.method_params.dup) + rescue + logger.warn "Casting of method parameters failed" unless logger.nil? + invocation.method_ordered_params = request.method_params + end + request.method_params = invocation.method_ordered_params + invocation.method_named_params = {} + invocation.api_method.param_names.inject(0) do |m, n| + invocation.method_named_params[n] = invocation.method_ordered_params[m] + m + 1 + end + invocation + end + + def web_service_create_response(protocol, protocol_options, api, api_method, return_value) + if api.has_api_method?(api_method.name) + return_type = api_method.returns ? api_method.returns[0] : nil + return_value = api_method.cast_returns(return_value) + else + return_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0) + end + protocol.encode_response(api_method.public_name + 'Response', return_value, return_type, protocol_options) + end + + class Invocation # :nodoc: + attr_accessor :protocol + attr_accessor :protocol_options + attr_accessor :service_name + attr_accessor :api + attr_accessor :api_method + attr_accessor :method_ordered_params + attr_accessor :method_named_params + attr_accessor :service + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb b/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb new file mode 100644 index 000000000..f9995197a --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/dispatcher/action_controller_dispatcher.rb @@ -0,0 +1,379 @@ +require 'benchmark' +require 'builder/xmlmarkup' + +module ActionWebService # :nodoc: + module Dispatcher # :nodoc: + module ActionController # :nodoc: + def self.included(base) # :nodoc: + class << base + include ClassMethods + alias_method_chain :inherited, :action_controller + end + base.class_eval do + alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke + end + base.add_web_service_api_callback do |klass, api| + if klass.web_service_dispatching_mode == :direct + klass.class_eval 'def api; dispatch_web_service_request; end' + end + end + base.add_web_service_definition_callback do |klass, name, info| + if klass.web_service_dispatching_mode == :delegated + klass.class_eval "def #{name}; dispatch_web_service_request; end" + elsif klass.web_service_dispatching_mode == :layered + klass.class_eval 'def api; dispatch_web_service_request; end' + end + end + base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods) + end + + module ClassMethods # :nodoc: + def inherited_with_action_controller(child) + inherited_without_action_controller(child) + child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlAction) + end + end + + module InstanceMethods # :nodoc: + private + def dispatch_web_service_request + method = request.method.to_s.upcase + allowed_methods = self.class.web_service_api ? (self.class.web_service_api.allowed_http_methods || []) : [ :post ] + allowed_methods = allowed_methods.map{|m| m.to_s.upcase } + if !allowed_methods.include?(method) + render :text => "#{method} not supported", :status=>500 + return + end + exception = nil + begin + ws_request = discover_web_service_request(request) + rescue Exception => e + exception = e + end + if ws_request + ws_response = nil + exception = nil + bm = Benchmark.measure do + begin + ws_response = invoke_web_service_request(ws_request) + rescue Exception => e + exception = e + end + end + log_request(ws_request, request.raw_post) + if exception + log_error(exception) unless logger.nil? + send_web_service_error_response(ws_request, exception) + else + send_web_service_response(ws_response, bm.real) + end + else + exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message") + log_error(exception) unless logger.nil? + send_web_service_error_response(ws_request, exception) + end + rescue Exception => e + log_error(e) unless logger.nil? + send_web_service_error_response(ws_request, e) + end + + def send_web_service_response(ws_response, elapsed=nil) + log_response(ws_response, elapsed) + options = { :type => ws_response.content_type, :disposition => 'inline' } + send_data(ws_response.body, options) + end + + def send_web_service_error_response(ws_request, exception) + if ws_request + unless self.class.web_service_exception_reporting + exception = DispatcherError.new("Internal server error (exception raised)") + end + api_method = ws_request.api_method + public_method_name = api_method ? api_method.public_name : ws_request.method_name + return_type = ActionWebService::SignatureTypes.canonical_signature_entry(Exception, 0) + ws_response = ws_request.protocol.encode_response(public_method_name + 'Response', exception, return_type, ws_request.protocol_options) + send_web_service_response(ws_response) + else + if self.class.web_service_exception_reporting + message = exception.message + backtrace = "\nBacktrace:\n#{exception.backtrace.join("\n")}" + else + message = "Exception raised" + backtrace = "" + end + render :text => "Internal protocol error: #{message}#{backtrace}", :status => 500 + end + end + + def web_service_direct_invoke(invocation) + invocation.method_named_params.each do |name, value| + params[name] = value + end + web_service_direct_invoke_without_controller(invocation) + end + + def log_request(ws_request, body) + unless logger.nil? + name = ws_request.method_name + api_method = ws_request.api_method + params = ws_request.method_params + if api_method && api_method.expects + params = api_method.expects.zip(params).map{ |type, param| "#{type.name}=>#{param.inspect}" } + else + params = params.map{ |param| param.inspect } + end + service = ws_request.service_name + logger.debug("\nWeb Service Request: #{name}(#{params.join(", ")}) Entrypoint: #{service}") + logger.debug(indent(body)) + end + end + + def log_response(ws_response, elapsed=nil) + unless logger.nil? + elapsed = (elapsed ? " (%f):" % elapsed : ":") + logger.debug("\nWeb Service Response" + elapsed + " => #{ws_response.return_value.inspect}") + logger.debug(indent(ws_response.body)) + end + end + + def indent(body) + body.split(/\n/).map{|x| " #{x}"}.join("\n") + end + end + + module WsdlAction # :nodoc: + XsdNs = 'http://www.w3.org/2001/XMLSchema' + WsdlNs = 'http://schemas.xmlsoap.org/wsdl/' + SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/' + SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/' + SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http' + + def wsdl + case request.method + when :get + begin + options = { :type => 'text/xml', :disposition => 'inline' } + send_data(to_wsdl, options) + rescue Exception => e + log_error(e) unless logger.nil? + end + when :post + render :text => 'POST not supported', :status => 500 + end + end + + private + def base_uri + host = request.host_with_port + relative_url_root = request.relative_url_root + scheme = request.ssl? ? 'https' : 'http' + '%s://%s%s/%s/' % [scheme, host, relative_url_root, self.class.controller_path] + end + + def to_wsdl + xml = '' + dispatching_mode = web_service_dispatching_mode + global_service_name = wsdl_service_name + namespace = wsdl_namespace || 'urn:ActionWebService' + soap_action_base = "/#{controller_name}" + + marshaler = ActionWebService::Protocol::Soap::SoapMarshaler.new(namespace) + apis = {} + case dispatching_mode + when :direct + api = self.class.web_service_api + web_service_name = controller_class_name.sub(/Controller$/, '').underscore + apis[web_service_name] = [api, register_api(api, marshaler)] + when :delegated, :layered + self.class.web_services.each do |web_service_name, info| + service = web_service_object(web_service_name) + api = service.class.web_service_api + apis[web_service_name] = [api, register_api(api, marshaler)] + end + end + custom_types = [] + apis.values.each do |api, bindings| + bindings.each do |b| + custom_types << b unless custom_types.include?(b) + end + end + + xm = Builder::XmlMarkup.new(:target => xml, :indent => 2) + xm.instruct! + xm.definitions('name' => wsdl_service_name, + 'targetNamespace' => namespace, + 'xmlns:typens' => namespace, + 'xmlns:xsd' => XsdNs, + 'xmlns:soap' => SoapNs, + 'xmlns:soapenc' => SoapEncodingNs, + 'xmlns:wsdl' => WsdlNs, + 'xmlns' => WsdlNs) do + # Generate XSD + if custom_types.size > 0 + xm.types do + xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do + custom_types.each do |binding| + case + when binding.type.array? + xm.xsd(:complexType, 'name' => binding.type_name) do + xm.xsd(:complexContent) do + xm.xsd(:restriction, 'base' => 'soapenc:Array') do + xm.xsd(:attribute, 'ref' => 'soapenc:arrayType', + 'wsdl:arrayType' => binding.element_binding.qualified_type_name('typens') + '[]') + end + end + end + when binding.type.structured? + xm.xsd(:complexType, 'name' => binding.type_name) do + xm.xsd(:all) do + binding.type.each_member do |name, type| + b = marshaler.register_type(type) + xm.xsd(:element, 'name' => name, 'type' => b.qualified_type_name('typens')) + end + end + end + end + end + end + end + end + + # APIs + apis.each do |api_name, values| + api = values[0] + api.api_methods.each do |name, method| + gen = lambda do |msg_name, direction| + xm.message('name' => message_name_for(api_name, msg_name)) do + sym = nil + if direction == :out + returns = method.returns + if returns + binding = marshaler.register_type(returns[0]) + xm.part('name' => 'return', 'type' => binding.qualified_type_name('typens')) + end + else + expects = method.expects + expects.each do |type| + binding = marshaler.register_type(type) + xm.part('name' => type.name, 'type' => binding.qualified_type_name('typens')) + end if expects + end + end + end + public_name = method.public_name + gen.call(public_name, :in) + gen.call("#{public_name}Response", :out) + end + + # Port + port_name = port_name_for(global_service_name, api_name) + xm.portType('name' => port_name) do + api.api_methods.each do |name, method| + xm.operation('name' => method.public_name) do + xm.input('message' => "typens:" + message_name_for(api_name, method.public_name)) + xm.output('message' => "typens:" + message_name_for(api_name, "#{method.public_name}Response")) + end + end + end + + # Bind it + binding_name = binding_name_for(global_service_name, api_name) + xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do + xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport) + api.api_methods.each do |name, method| + xm.operation('name' => method.public_name) do + case web_service_dispatching_mode + when :direct + soap_action = soap_action_base + "/api/" + method.public_name + when :delegated, :layered + soap_action = soap_action_base \ + + "/" + api_name.to_s \ + + "/" + method.public_name + end + xm.soap(:operation, 'soapAction' => soap_action) + xm.input do + xm.soap(:body, + 'use' => 'encoded', + 'namespace' => namespace, + 'encodingStyle' => SoapEncodingNs) + end + xm.output do + xm.soap(:body, + 'use' => 'encoded', + 'namespace' => namespace, + 'encodingStyle' => SoapEncodingNs) + end + end + end + end + end + + # Define it + xm.service('name' => "#{global_service_name}Service") do + apis.each do |api_name, values| + port_name = port_name_for(global_service_name, api_name) + binding_name = binding_name_for(global_service_name, api_name) + case web_service_dispatching_mode + when :direct, :layered + binding_target = 'api' + when :delegated + binding_target = api_name.to_s + end + xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do + xm.soap(:address, 'location' => "#{base_uri}#{binding_target}") + end + end + end + end + end + + def port_name_for(global_service, service) + "#{global_service}#{service.to_s.camelize}Port" + end + + def binding_name_for(global_service, service) + "#{global_service}#{service.to_s.camelize}Binding" + end + + def message_name_for(api_name, message_name) + mode = web_service_dispatching_mode + if mode == :layered || mode == :delegated + api_name.to_s + '-' + message_name + else + message_name + end + end + + def register_api(api, marshaler) + bindings = {} + traverse_custom_types(api, marshaler, bindings) do |binding| + bindings[binding] = nil unless bindings.has_key?(binding) + element_binding = binding.element_binding + bindings[element_binding] = nil if element_binding && !bindings.has_key?(element_binding) + end + bindings.keys + end + + def traverse_custom_types(api, marshaler, bindings, &block) + api.api_methods.each do |name, method| + expects, returns = method.expects, method.returns + expects.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if expects + returns.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if returns + end + end + + def traverse_type(marshaler, type, bindings, &block) + binding = marshaler.register_type(type) + return if bindings.has_key?(binding) + bindings[binding] = nil + yield binding + if type.array? + yield marshaler.register_type(type.element_type) + type = type.element_type + end + type.each_member{ |name, type| traverse_type(marshaler, type, bindings, &block) } if type.structured? + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb b/vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb new file mode 100644 index 000000000..2a9121ee2 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/invocation.rb @@ -0,0 +1,202 @@ +module ActionWebService # :nodoc: + module Invocation # :nodoc: + class InvocationError < ActionWebService::ActionWebServiceError # :nodoc: + end + + def self.included(base) # :nodoc: + base.extend(ClassMethods) + base.send(:include, ActionWebService::Invocation::InstanceMethods) + end + + # Invocation interceptors provide a means to execute custom code before + # and after method invocations on ActionWebService::Base objects. + # + # When running in _Direct_ dispatching mode, ActionController filters + # should be used for this functionality instead. + # + # The semantics of invocation interceptors are the same as ActionController + # filters, and accept the same parameters and options. + # + # A _before_ interceptor can also cancel execution by returning +false+, + # or returning a [false, "cancel reason"] array if it wishes to supply + # a reason for canceling the request. + # + # === Example + # + # class CustomService < ActionWebService::Base + # before_invocation :intercept_add, :only => [:add] + # + # def add(a, b) + # a + b + # end + # + # private + # def intercept_add + # return [false, "permission denied"] # cancel it + # end + # end + # + # Options: + # [:except] A list of methods for which the interceptor will NOT be called + # [:only] A list of methods for which the interceptor WILL be called + module ClassMethods + # Appends the given +interceptors+ to be called + # _before_ method invocation. + def append_before_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + append_interceptors_to_chain("before", interceptors) + end + + # Prepends the given +interceptors+ to be called + # _before_ method invocation. + def prepend_before_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + prepend_interceptors_to_chain("before", interceptors) + end + + alias :before_invocation :append_before_invocation + + # Appends the given +interceptors+ to be called + # _after_ method invocation. + def append_after_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + append_interceptors_to_chain("after", interceptors) + end + + # Prepends the given +interceptors+ to be called + # _after_ method invocation. + def prepend_after_invocation(*interceptors, &block) + conditions = extract_conditions!(interceptors) + interceptors << block if block_given? + add_interception_conditions(interceptors, conditions) + prepend_interceptors_to_chain("after", interceptors) + end + + alias :after_invocation :append_after_invocation + + def before_invocation_interceptors # :nodoc: + read_inheritable_attribute("before_invocation_interceptors") + end + + def after_invocation_interceptors # :nodoc: + read_inheritable_attribute("after_invocation_interceptors") + end + + def included_intercepted_methods # :nodoc: + read_inheritable_attribute("included_intercepted_methods") || {} + end + + def excluded_intercepted_methods # :nodoc: + read_inheritable_attribute("excluded_intercepted_methods") || {} + end + + private + def append_interceptors_to_chain(condition, interceptors) + write_inheritable_array("#{condition}_invocation_interceptors", interceptors) + end + + def prepend_interceptors_to_chain(condition, interceptors) + interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors") + write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors) + end + + def extract_conditions!(interceptors) + return nil unless interceptors.last.is_a? Hash + interceptors.pop + end + + def add_interception_conditions(interceptors, conditions) + return unless conditions + included, excluded = conditions[:only], conditions[:except] + write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included + write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded + end + + def condition_hash(interceptors, *methods) + interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})} + end + end + + module InstanceMethods # :nodoc: + def self.included(base) + base.class_eval do + alias_method_chain :perform_invocation, :interception + end + end + + def perform_invocation_with_interception(method_name, params, &block) + return if before_invocation(method_name, params, &block) == false + return_value = perform_invocation_without_interception(method_name, params) + after_invocation(method_name, params, return_value) + return_value + end + + def perform_invocation(method_name, params) + send(method_name, *params) + end + + def before_invocation(name, args, &block) + call_interceptors(self.class.before_invocation_interceptors, [name, args], &block) + end + + def after_invocation(name, args, result) + call_interceptors(self.class.after_invocation_interceptors, [name, args, result]) + end + + private + + def call_interceptors(interceptors, interceptor_args, &block) + if interceptors and not interceptors.empty? + interceptors.each do |interceptor| + next if method_exempted?(interceptor, interceptor_args[0].to_s) + result = case + when interceptor.is_a?(Symbol) + self.send(interceptor, *interceptor_args) + when interceptor_block?(interceptor) + interceptor.call(self, *interceptor_args) + when interceptor_class?(interceptor) + interceptor.intercept(self, *interceptor_args) + else + raise( + InvocationError, + "Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method" + ) + end + reason = nil + if result.is_a?(Array) + reason = result[1] if result[1] + result = result[0] + end + if result == false + block.call(reason) if block && reason + return false + end + end + end + end + + def interceptor_block?(interceptor) + interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1) + end + + def interceptor_class?(interceptor) + interceptor.respond_to?("intercept") + end + + def method_exempted?(interceptor, method_name) + case + when self.class.included_intercepted_methods[interceptor] + !self.class.included_intercepted_methods[interceptor].include?(method_name) + when self.class.excluded_intercepted_methods[interceptor] + self.class.excluded_intercepted_methods[interceptor].include?(method_name) + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb b/vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb new file mode 100644 index 000000000..053e9cb4b --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/protocol.rb @@ -0,0 +1,4 @@ +require 'action_web_service/protocol/abstract' +require 'action_web_service/protocol/discovery' +require 'action_web_service/protocol/soap_protocol' +require 'action_web_service/protocol/xmlrpc_protocol' diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb new file mode 100644 index 000000000..fff5f622c --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/abstract.rb @@ -0,0 +1,112 @@ +module ActionWebService # :nodoc: + module Protocol # :nodoc: + class ProtocolError < ActionWebServiceError # :nodoc: + end + + class AbstractProtocol # :nodoc: + def setup(controller) + end + + def decode_action_pack_request(action_pack_request) + end + + def encode_action_pack_request(service_name, public_method_name, raw_body, options={}) + klass = options[:request_class] || SimpleActionPackRequest + request = klass.new + request.request_parameters['action'] = service_name.to_s + request.env['RAW_POST_DATA'] = raw_body + request.env['REQUEST_METHOD'] = 'POST' + request.env['HTTP_CONTENT_TYPE'] = 'text/xml' + request + end + + def decode_request(raw_request, service_name, protocol_options={}) + end + + def encode_request(method_name, params, param_types) + end + + def decode_response(raw_response) + end + + def encode_response(method_name, return_value, return_type, protocol_options={}) + end + + def protocol_client(api, protocol_name, endpoint_uri, options) + end + + def register_api(api) + end + end + + class Request # :nodoc: + attr :protocol + attr_accessor :method_name + attr_accessor :method_params + attr :service_name + attr_accessor :api + attr_accessor :api_method + attr :protocol_options + + def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil, protocol_options=nil) + @protocol = protocol + @method_name = method_name + @method_params = method_params + @service_name = service_name + @api = api + @api_method = api_method + @protocol_options = protocol_options || {} + end + end + + class Response # :nodoc: + attr :body + attr :content_type + attr :return_value + + def initialize(body, content_type, return_value) + @body = body + @content_type = content_type + @return_value = return_value + end + end + + class SimpleActionPackRequest < ActionController::AbstractRequest # :nodoc: + def initialize + @env = {} + @qparams = {} + @rparams = {} + @cookies = {} + reset_session + end + + def query_parameters + @qparams + end + + def request_parameters + @rparams + end + + def env + @env + end + + def host + '' + end + + def cookies + @cookies + end + + def session + @session + end + + def reset_session + @session = {} + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb new file mode 100644 index 000000000..3d4e0818d --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/discovery.rb @@ -0,0 +1,37 @@ +module ActionWebService # :nodoc: + module Protocol # :nodoc: + module Discovery # :nodoc: + def self.included(base) + base.extend(ClassMethods) + base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods) + end + + module ClassMethods # :nodoc: + def register_protocol(klass) + write_inheritable_array("web_service_protocols", [klass]) + end + end + + module InstanceMethods # :nodoc: + private + def discover_web_service_request(action_pack_request) + (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| + protocol = protocol.create(self) + request = protocol.decode_action_pack_request(action_pack_request) + return request unless request.nil? + end + nil + end + + def create_web_service_client(api, protocol_name, endpoint_uri, options) + (self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol| + protocol = protocol.create(self) + client = protocol.protocol_client(api, protocol_name, endpoint_uri, options) + return client unless client.nil? + end + nil + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb new file mode 100644 index 000000000..1bce496a7 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol.rb @@ -0,0 +1,176 @@ +require 'action_web_service/protocol/soap_protocol/marshaler' +require 'soap/streamHandler' +require 'action_web_service/client/soap_client' + +module ActionWebService # :nodoc: + module API # :nodoc: + class Base # :nodoc: + def self.soap_client(endpoint_uri, options={}) + ActionWebService::Client::Soap.new self, endpoint_uri, options + end + end + end + + module Protocol # :nodoc: + module Soap # :nodoc: + def self.included(base) + base.register_protocol(SoapProtocol) + base.class_inheritable_option(:wsdl_service_name) + base.class_inheritable_option(:wsdl_namespace) + end + + class SoapProtocol < AbstractProtocol # :nodoc: + AWSEncoding = 'UTF-8' + XSDEncoding = 'UTF8' + + attr :marshaler + + def initialize(namespace=nil) + namespace ||= 'urn:ActionWebService' + @marshaler = SoapMarshaler.new namespace + end + + def self.create(controller) + SoapProtocol.new(controller.wsdl_namespace) + end + + def decode_action_pack_request(action_pack_request) + return nil unless soap_action = has_valid_soap_action?(action_pack_request) + service_name = action_pack_request.parameters['action'] + input_encoding = parse_charset(action_pack_request.env['HTTP_CONTENT_TYPE']) + protocol_options = { + :soap_action => soap_action, + :charset => input_encoding + } + decode_request(action_pack_request.raw_post, service_name, protocol_options) + end + + def encode_action_pack_request(service_name, public_method_name, raw_body, options={}) + request = super + request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name] + request + end + + def decode_request(raw_request, service_name, protocol_options={}) + envelope = SOAP::Processor.unmarshal(raw_request, :charset => protocol_options[:charset]) + unless envelope + raise ProtocolError, "Failed to parse SOAP request message" + end + request = envelope.body.request + method_name = request.elename.name + params = request.collect{ |k, v| marshaler.soap_to_ruby(request[k]) } + Request.new(self, method_name, params, service_name, nil, nil, protocol_options) + end + + def encode_request(method_name, params, param_types) + param_types.each{ |type| marshaler.register_type(type) } if param_types + qname = XSD::QName.new(marshaler.namespace, method_name) + param_def = [] + if param_types + params = param_types.zip(params).map do |type, param| + param_def << ['in', type.name, marshaler.lookup_type(type).mapping] + [type.name, marshaler.ruby_to_soap(param)] + end + else + params = [] + end + request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def) + request.set_param(params) + envelope = create_soap_envelope(request) + SOAP::Processor.marshal(envelope) + end + + def decode_response(raw_response) + envelope = SOAP::Processor.unmarshal(raw_response) + unless envelope + raise ProtocolError, "Failed to parse SOAP request message" + end + method_name = envelope.body.request.elename.name + return_value = envelope.body.response + return_value = marshaler.soap_to_ruby(return_value) unless return_value.nil? + [method_name, return_value] + end + + def encode_response(method_name, return_value, return_type, protocol_options={}) + if return_type + return_binding = marshaler.register_type(return_type) + marshaler.annotate_arrays(return_binding, return_value) + end + qname = XSD::QName.new(marshaler.namespace, method_name) + if return_value.nil? + response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) + else + if return_value.is_a?(Exception) + detail = SOAP::Mapping::SOAPException.new(return_value) + response = SOAP::SOAPFault.new( + SOAP::SOAPQName.new('%s:%s' % [SOAP::SOAPNamespaceTag, 'Server']), + SOAP::SOAPString.new(return_value.to_s), + SOAP::SOAPString.new(self.class.name), + marshaler.ruby_to_soap(detail)) + else + if return_type + param_def = [['retval', 'return', marshaler.lookup_type(return_type).mapping]] + response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def) + response.retval = marshaler.ruby_to_soap(return_value) + else + response = SOAP::RPC::SOAPMethodResponse.new(qname, nil) + end + end + end + envelope = create_soap_envelope(response) + + # FIXME: This is not thread-safe, but StringFactory_ in SOAP4R only + # reads target encoding from the XSD::Charset.encoding variable. + # This is required to ensure $KCODE strings are converted + # correctly to UTF-8 for any values of $KCODE. + previous_encoding = XSD::Charset.encoding + XSD::Charset.encoding = XSDEncoding + response_body = SOAP::Processor.marshal(envelope, :charset => AWSEncoding) + XSD::Charset.encoding = previous_encoding + + Response.new(response_body, "text/xml; charset=#{AWSEncoding}", return_value) + end + + def protocol_client(api, protocol_name, endpoint_uri, options={}) + return nil unless protocol_name == :soap + ActionWebService::Client::Soap.new(api, endpoint_uri, options) + end + + def register_api(api) + api.api_methods.each do |name, method| + method.expects.each{ |type| marshaler.register_type(type) } if method.expects + method.returns.each{ |type| marshaler.register_type(type) } if method.returns + end + end + + private + def has_valid_soap_action?(request) + return nil unless request.method == :post + soap_action = request.env['HTTP_SOAPACTION'] + return nil unless soap_action + soap_action = soap_action.dup + soap_action.gsub!(/^"/, '') + soap_action.gsub!(/"$/, '') + soap_action.strip! + return nil if soap_action.empty? + soap_action + end + + def create_soap_envelope(body) + header = SOAP::SOAPHeader.new + body = SOAP::SOAPBody.new(body) + SOAP::SOAPEnvelope.new(header, body) + end + + def parse_charset(content_type) + return AWSEncoding if content_type.nil? + if /^text\/xml(?:\s*;\s*charset=([^"]+|"[^"]+"))$/i =~ content_type + $1 + else + AWSEncoding + end + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb new file mode 100644 index 000000000..187339627 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/soap_protocol/marshaler.rb @@ -0,0 +1,235 @@ +require 'soap/mapping' + +module ActionWebService + module Protocol + module Soap + # Workaround for SOAP4R return values changing + class Registry < SOAP::Mapping::Registry + if SOAP::Version >= "1.5.4" + def find_mapped_soap_class(obj_class) + return @map.instance_eval { @obj2soap[obj_class][0] } + end + + def find_mapped_obj_class(soap_class) + return @map.instance_eval { @soap2obj[soap_class][0] } + end + end + end + + class SoapMarshaler + attr :namespace + attr :registry + + def initialize(namespace=nil) + @namespace = namespace || 'urn:ActionWebService' + @registry = Registry.new + @type2binding = {} + register_static_factories + end + + def soap_to_ruby(obj) + SOAP::Mapping.soap2obj(obj, @registry) + end + + def ruby_to_soap(obj) + soap = SOAP::Mapping.obj2soap(obj, @registry) + soap.elename = XSD::QName.new if SOAP::Version >= "1.5.5" && soap.elename == XSD::QName::EMPTY + soap + end + + def register_type(type) + return @type2binding[type] if @type2binding.has_key?(type) + + if type.array? + array_mapping = @registry.find_mapped_soap_class(Array) + qname = XSD::QName.new(@namespace, soap_type_name(type.element_type.type_class.name) + 'Array') + element_type_binding = register_type(type.element_type) + @type2binding[type] = SoapBinding.new(self, qname, type, array_mapping, element_type_binding) + elsif (mapping = @registry.find_mapped_soap_class(type.type_class) rescue nil) + qname = mapping[2] ? mapping[2][:type] : nil + qname ||= soap_base_type_name(mapping[0]) + @type2binding[type] = SoapBinding.new(self, qname, type, mapping) + else + qname = XSD::QName.new(@namespace, soap_type_name(type.type_class.name)) + @registry.add(type.type_class, + SOAP::SOAPStruct, + typed_struct_factory(type.type_class), + { :type => qname }) + mapping = @registry.find_mapped_soap_class(type.type_class) + @type2binding[type] = SoapBinding.new(self, qname, type, mapping) + end + + if type.structured? + type.each_member do |m_name, m_type| + register_type(m_type) + end + end + + @type2binding[type] + end + alias :lookup_type :register_type + + def annotate_arrays(binding, value) + if value.nil? + return + elsif binding.type.array? + mark_typed_array(value, binding.element_binding.qname) + if binding.element_binding.type.custom? + value.each do |element| + annotate_arrays(binding.element_binding, element) + end + end + elsif binding.type.structured? + binding.type.each_member do |name, type| + member_binding = register_type(type) + member_value = value.respond_to?('[]') ? value[name] : value.send(name) + annotate_arrays(member_binding, member_value) if type.custom? + end + end + end + + private + def typed_struct_factory(type_class) + if Object.const_defined?('ActiveRecord') + if type_class.ancestors.include?(ActiveRecord::Base) + qname = XSD::QName.new(@namespace, soap_type_name(type_class.name)) + type_class.instance_variable_set('@qname', qname) + return SoapActiveRecordStructFactory.new + end + end + SOAP::Mapping::Registry::TypedStructFactory + end + + def mark_typed_array(array, qname) + (class << array; self; end).class_eval do + define_method(:arytype) do + qname + end + end + end + + def soap_base_type_name(type) + xsd_type = type.ancestors.find{ |c| c.const_defined? 'Type' } + xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type + end + + def soap_type_name(type_name) + type_name.gsub(/::/, '..') + end + + def register_static_factories + @registry.add(ActionWebService::Base64, SOAP::SOAPBase64, SoapBase64Factory.new, nil) + mapping = @registry.find_mapped_soap_class(ActionWebService::Base64) + @type2binding[ActionWebService::Base64] = + SoapBinding.new(self, SOAP::SOAPBase64::Type, ActionWebService::Base64, mapping) + @registry.add(Array, SOAP::SOAPArray, SoapTypedArrayFactory.new, nil) + @registry.add(::BigDecimal, SOAP::SOAPDouble, SOAP::Mapping::Registry::BasetypeFactory, {:derived_class => true}) + end + end + + class SoapBinding + attr :qname + attr :type + attr :mapping + attr :element_binding + + def initialize(marshaler, qname, type, mapping, element_binding=nil) + @marshaler = marshaler + @qname = qname + @type = type + @mapping = mapping + @element_binding = element_binding + end + + def type_name + @type.custom? ? @qname.name : nil + end + + def qualified_type_name(ns=nil) + if @type.custom? + "#{ns ? ns : @qname.namespace}:#{@qname.name}" + else + ns = XSD::NS.new + ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag) + ns.assign(SOAP::EncodingNamespace, "soapenc") + xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')} + return ns.name(XSD::AnyTypeName) unless xsd_klass + ns.name(xsd_klass.const_get('Type')) + end + end + + def eql?(other) + @qname == other.qname + end + alias :== :eql? + + def hash + @qname.hash + end + end + + class SoapActiveRecordStructFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.is_a?(ActiveRecord::Base) + return nil + end + soap_obj = soap_class.new(obj.class.instance_variable_get('@qname')) + obj.class.columns.each do |column| + key = column.name.to_s + value = obj.send(key) + soap_obj[key] = SOAP::Mapping._obj2soap(value, map) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + unless node.type == obj_class.instance_variable_get('@qname') + return false + end + obj = obj_class.new + node.each do |key, value| + obj[key] = value.data + end + obj.instance_variable_set('@new_record', false) + return true, obj + end + end + + class SoapTypedArrayFactory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.respond_to?(:arytype) + return nil + end + soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype) + mark_marshalled_obj(obj, soap_obj) + obj.each do |item| + child = SOAP::Mapping._obj2soap(item, map) + soap_obj.add(child) + end + soap_obj + end + + def soap2obj(obj_class, node, info, map) + return false + end + end + + class SoapBase64Factory < SOAP::Mapping::Factory + def obj2soap(soap_class, obj, info, map) + unless obj.is_a?(ActionWebService::Base64) + return nil + end + return soap_class.new(obj) + end + + def soap2obj(obj_class, node, info, map) + unless node.type == SOAP::SOAPBase64::Type + return false + end + return true, obj_class.new(node.string) + end + end + + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb new file mode 100644 index 000000000..dfa4afc67 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/protocol/xmlrpc_protocol.rb @@ -0,0 +1,122 @@ +require 'xmlrpc/marshal' +require 'action_web_service/client/xmlrpc_client' + +module XMLRPC # :nodoc: + class FaultException # :nodoc: + alias :message :faultString + end + + class Create + def wrong_type(value) + if BigDecimal === value + [true, value.to_f] + else + false + end + end + end +end + +module ActionWebService # :nodoc: + module API # :nodoc: + class Base # :nodoc: + def self.xmlrpc_client(endpoint_uri, options={}) + ActionWebService::Client::XmlRpc.new self, endpoint_uri, options + end + end + end + + module Protocol # :nodoc: + module XmlRpc # :nodoc: + def self.included(base) + base.register_protocol(XmlRpcProtocol) + end + + class XmlRpcProtocol < AbstractProtocol # :nodoc: + def self.create(controller) + XmlRpcProtocol.new + end + + def decode_action_pack_request(action_pack_request) + service_name = action_pack_request.parameters['action'] + decode_request(action_pack_request.raw_post, service_name) + end + + def decode_request(raw_request, service_name) + method_name, params = XMLRPC::Marshal.load_call(raw_request) + Request.new(self, method_name, params, service_name) + rescue + return nil + end + + def encode_request(method_name, params, param_types) + if param_types + params = params.dup + param_types.each_with_index{ |type, i| params[i] = value_to_xmlrpc_wire_format(params[i], type) } + end + XMLRPC::Marshal.dump_call(method_name, *params) + end + + def decode_response(raw_response) + [nil, XMLRPC::Marshal.load_response(raw_response)] + end + + def encode_response(method_name, return_value, return_type, protocol_options={}) + if return_value && return_type + return_value = value_to_xmlrpc_wire_format(return_value, return_type) + end + return_value = false if return_value.nil? + raw_response = XMLRPC::Marshal.dump_response(return_value) + Response.new(raw_response, 'text/xml', return_value) + end + + def encode_multicall_response(responses, protocol_options={}) + result = responses.map do |return_value, return_type| + if return_value && return_type + return_value = value_to_xmlrpc_wire_format(return_value, return_type) + return_value = [return_value] unless return_value.nil? + end + return_value = false if return_value.nil? + return_value + end + raw_response = XMLRPC::Marshal.dump_response(result) + Response.new(raw_response, 'text/xml', result) + end + + def protocol_client(api, protocol_name, endpoint_uri, options={}) + return nil unless protocol_name == :xmlrpc + ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options) + end + + def value_to_xmlrpc_wire_format(value, value_type) + if value_type.array? + value.map{ |val| value_to_xmlrpc_wire_format(val, value_type.element_type) } + else + if value.is_a?(ActionWebService::Struct) + struct = {} + value.class.members.each do |name, type| + member_value = value[name] + next if member_value.nil? + struct[name.to_s] = value_to_xmlrpc_wire_format(member_value, type) + end + struct + elsif value.is_a?(ActiveRecord::Base) + struct = {} + value.attributes.each do |key, member_value| + next if member_value.nil? + struct[key.to_s] = member_value + end + struct + elsif value.is_a?(ActionWebService::Base64) + XMLRPC::Base64.new(value) + elsif value.is_a?(Exception) && !value.is_a?(XMLRPC::FaultException) + XMLRPC::FaultException.new(2, value.message) + else + value + end + end + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb b/vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb new file mode 100644 index 000000000..f94a7ee91 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/scaffolding.rb @@ -0,0 +1,283 @@ +require 'benchmark' +require 'pathname' + +module ActionWebService + module Scaffolding # :nodoc: + class ScaffoldingError < ActionWebServiceError # :nodoc: + end + + def self.included(base) + base.extend(ClassMethods) + end + + # Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The + # generated scaffold actions have default views to let you enter the method parameters and view the + # results. + # + # Example: + # + # class ApiController < ActionController + # web_service_scaffold :invoke + # end + # + # This example generates an +invoke+ action in the +ApiController+ that you can navigate to from + # your browser, select the API method, enter its parameters, and perform the invocation. + # + # If you want to customize the default views, create the following views in "app/views": + # + # * action_name/methods.erb + # * action_name/parameters.erb + # * action_name/result.erb + # * action_name/layout.erb + # + # Where action_name is the name of the action you gave to ClassMethods#web_service_scaffold. + # + # You can use the default views in RAILS_DIR/lib/action_web_service/templates/scaffolds as + # a guide. + module ClassMethods + # Generates web service invocation scaffolding for the current controller. The given action name + # can then be used as the entry point for invoking API methods from a web browser. + def web_service_scaffold(action_name) + add_template_helper(Helpers) + module_eval <<-"end_eval", __FILE__, __LINE__ + 1 + def #{action_name} + if request.method == :get + setup_invocation_assigns + render_invocation_scaffold 'methods' + end + end + + def #{action_name}_method_params + if request.method == :get + setup_invocation_assigns + render_invocation_scaffold 'parameters' + end + end + + def #{action_name}_submit + if request.method == :post + setup_invocation_assigns + protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap + case protocol_name + when :soap + @protocol = Protocol::Soap::SoapProtocol.create(self) + when :xmlrpc + @protocol = Protocol::XmlRpc::XmlRpcProtocol.create(self) + end + bm = Benchmark.measure do + @protocol.register_api(@scaffold_service.api) + post_params = params['method_params'] ? params['method_params'].dup : nil + params = [] + @scaffold_method.expects.each_with_index do |spec, i| + params << post_params[i.to_s] + end if @scaffold_method.expects + params = @scaffold_method.cast_expects(params) + method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name) + @method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects) + new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml) + prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name) + self.request = new_request + if @scaffold_container.dispatching_mode != :direct + request.parameters['action'] = @scaffold_service.name + end + dispatch_web_service_request + @method_response_xml = response.body + method_name, obj = @protocol.decode_response(@method_response_xml) + return if handle_invocation_exception(obj) + @method_return_value = @scaffold_method.cast_returns(obj) + end + @method_elapsed = bm.real + add_instance_variables_to_assigns + reset_invocation_response + render_invocation_scaffold 'result' + end + end + + private + def setup_invocation_assigns + @scaffold_class = self.class + @scaffold_action_name = "#{action_name}" + @scaffold_container = WebServiceModel::Container.new(self) + if params['service'] && params['method'] + @scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] } + @scaffold_method = @scaffold_service.api_methods[params['method']] + end + add_instance_variables_to_assigns + end + + def render_invocation_scaffold(action) + customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}" + default_template = scaffold_path(action) + if template_exists?(customized_template) + content = @template.render :file => customized_template + else + content = @template.render :file => default_template + end + @template.instance_variable_set("@content_for_layout", content) + if self.active_layout.nil? + render :file => scaffold_path("layout") + else + render :file => self.active_layout + end + end + + def scaffold_path(template_name) + File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".erb" + end + + def reset_invocation_response + erase_render_results + response.headers = ::ActionController::AbstractResponse::DEFAULT_HEADERS.merge("cookie" => []) + end + + def public_method_name(service_name, method_name) + if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) + service_name + '.' + method_name + else + method_name + end + end + + def prepare_request(new_request, service_name, method_name) + new_request.parameters.update(request.parameters) + request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) } + if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol) + new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}" + end + end + + def handle_invocation_exception(obj) + exception = nil + if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception) + exception = obj.detail.cause + elsif obj.is_a?(XMLRPC::FaultException) + exception = obj + end + return unless exception + reset_invocation_response + rescue_action(exception) + true + end + end_eval + end + end + + module Helpers # :nodoc: + def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false) + if type.array? + return content_tag('em', "Typed array input fields not supported yet (#{type.name})") + end + if type.structured? + return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured + parameters = "" + type.each_member do |member_name, member_type| + label = method_parameter_label(member_name, member_type) + nested_content = method_parameter_input_fields( + method, + member_type, + "#{field_name_base}[#{idx}][#{member_name}]", + idx, + true) + if member_type.custom? + parameters << content_tag('li', label) + parameters << content_tag('ul', nested_content) + else + parameters << content_tag('li', label + ' ' + nested_content) + end + end + content_tag('ul', parameters) + else + # If the data source was structured previously we already have the index set + field_name_base = "#{field_name_base}[#{idx}]" unless was_structured + + case type.type + when :int + text_field_tag "#{field_name_base}" + when :string + text_field_tag "#{field_name_base}" + when :base64 + text_area_tag "#{field_name_base}", nil, :size => "40x5" + when :bool + radio_button_tag("#{field_name_base}", "true") + " True" + + radio_button_tag("#{field_name_base}", "false") + "False" + when :float + text_field_tag "#{field_name_base}" + when :time, :datetime + time = Time.now + i = 0 + %w|year month day hour minute second|.map do |name| + i += 1 + send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true) + end.join + when :date + date = Date.today + i = 0 + %w|year month day|.map do |name| + i += 1 + send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true) + end.join + end + end + end + + def method_parameter_label(name, type) + name.to_s.capitalize + ' (' + type.human_name(false) + ')' + end + + def service_method_list(service) + action = @scaffold_action_name + '_method_params' + methods = service.api_methods_full.map do |desc, name| + content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name)) + end + content_tag("ul", methods.join("\n")) + end + end + + module WebServiceModel # :nodoc: + class Container # :nodoc: + attr :services + attr :dispatching_mode + + def initialize(real_container) + @real_container = real_container + @dispatching_mode = @real_container.class.web_service_dispatching_mode + @services = [] + if @dispatching_mode == :direct + @services << Service.new(@real_container.controller_name, @real_container) + else + @real_container.class.web_services.each do |name, obj| + @services << Service.new(name, @real_container.instance_eval{ web_service_object(name) }) + end + end + end + end + + class Service # :nodoc: + attr :name + attr :object + attr :api + attr :api_methods + attr :api_methods_full + + def initialize(name, real_service) + @name = name.to_s + @object = real_service + @api = @object.class.web_service_api + if @api.nil? + raise ScaffoldingError, "No web service API attached to #{object.class}" + end + @api_methods = {} + @api_methods_full = [] + @api.api_methods.each do |name, method| + @api_methods[method.public_name.to_s] = method + @api_methods_full << [method.to_s, method.public_name.to_s] + end + end + + def to_s + self.name.camelize + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/struct.rb b/vendor/plugins/actionwebservice/lib/action_web_service/struct.rb new file mode 100644 index 000000000..00eafc169 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/struct.rb @@ -0,0 +1,64 @@ +module ActionWebService + # To send structured types across the wire, derive from ActionWebService::Struct, + # and use +member+ to declare structure members. + # + # ActionWebService::Struct should be used in method signatures when you want to accept or return + # structured types that have no Active Record model class representations, or you don't + # want to expose your entire Active Record model to remote callers. + # + # === Example + # + # class Person < ActionWebService::Struct + # member :id, :int + # member :firstnames, [:string] + # member :lastname, :string + # member :email, :string + # end + # person = Person.new(:id => 5, :firstname => 'john', :lastname => 'doe') + # + # Active Record model classes are already implicitly supported in method + # signatures. + class Struct + # If a Hash is given as argument to an ActionWebService::Struct constructor, + # it can contain initial values for the structure member. + def initialize(values={}) + if values.is_a?(Hash) + values.map{|k,v| __send__('%s=' % k.to_s, v)} + end + end + + # The member with the given name + def [](name) + send(name.to_s) + end + + # Iterates through each member + def each_pair(&block) + self.class.members.each do |name, type| + yield name, self.__send__(name) + end + end + + class << self + # Creates a structure member with the specified +name+ and +type+. Generates + # accessor methods for reading and writing the member value. + def member(name, type) + name = name.to_sym + type = ActionWebService::SignatureTypes.canonical_signature_entry({ name => type }, 0) + write_inheritable_hash("struct_members", name => type) + class_eval <<-END + def #{name}; @#{name}; end + def #{name}=(value); @#{name} = value; end + END + end + + def members # :nodoc: + read_inheritable_attribute("struct_members") || {} + end + + def member_type(name) # :nodoc: + members[name.to_sym] + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb b/vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb new file mode 100644 index 000000000..4d1c2ed47 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/support/class_inheritable_options.rb @@ -0,0 +1,26 @@ +class Class # :nodoc: + def class_inheritable_option(sym, default_value=nil) + write_inheritable_attribute sym, default_value + class_eval <<-EOS + def self.#{sym}(value=nil) + if !value.nil? + write_inheritable_attribute(:#{sym}, value) + else + read_inheritable_attribute(:#{sym}) + end + end + + def self.#{sym}=(value) + write_inheritable_attribute(:#{sym}, value) + end + + def #{sym} + self.class.#{sym} + end + + def #{sym}=(value) + self.class.#{sym} = value + end + EOS + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb b/vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb new file mode 100644 index 000000000..66c86bf6d --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/support/signature_types.rb @@ -0,0 +1,226 @@ +module ActionWebService # :nodoc: + # Action Web Service supports the following base types in a signature: + # + # [:int] Represents an integer value, will be cast to an integer using Integer(value) + # [:string] Represents a string value, will be cast to an string using the to_s method on an object + # [:base64] Represents a Base 64 value, will contain the binary bytes of a Base 64 value sent by the caller + # [:bool] Represents a boolean value, whatever is passed will be cast to boolean (true, '1', 'true', 'y', 'yes' are taken to represent true; false, '0', 'false', 'n', 'no' and nil represent false) + # [:float] Represents a floating point value, will be cast to a float using Float(value) + # [:time] Represents a timestamp, will be cast to a Time object + # [:datetime] Represents a timestamp, will be cast to a DateTime object + # [:date] Represents a date, will be cast to a Date object + # + # For structured types, you'll need to pass in the Class objects of + # ActionWebService::Struct and ActiveRecord::Base derivatives. + module SignatureTypes + def canonical_signature(signature) # :nodoc: + return nil if signature.nil? + unless signature.is_a?(Array) + raise(ActionWebServiceError, "Expected signature to be an Array") + end + i = -1 + signature.map{ |spec| canonical_signature_entry(spec, i += 1) } + end + + def canonical_signature_entry(spec, i) # :nodoc: + orig_spec = spec + name = "param#{i}" + if spec.is_a?(Hash) + name, spec = spec.keys.first, spec.values.first + end + type = spec + if spec.is_a?(Array) + ArrayType.new(orig_spec, canonical_signature_entry(spec[0], 0), name) + else + type = canonical_type(type) + if type.is_a?(Symbol) + BaseType.new(orig_spec, type, name) + else + StructuredType.new(orig_spec, type, name) + end + end + end + + def canonical_type(type) # :nodoc: + type_name = symbol_name(type) || class_to_type_name(type) + type = type_name || type + return canonical_type_name(type) if type.is_a?(Symbol) + type + end + + def canonical_type_name(name) # :nodoc: + name = name.to_sym + case name + when :int, :integer, :fixnum, :bignum + :int + when :string, :text + :string + when :base64, :binary + :base64 + when :bool, :boolean + :bool + when :float, :double + :float + when :decimal + :decimal + when :time, :timestamp + :time + when :datetime + :datetime + when :date + :date + else + raise(TypeError, "#{name} is not a valid base type") + end + end + + def canonical_type_class(type) # :nodoc: + type = canonical_type(type) + type.is_a?(Symbol) ? type_name_to_class(type) : type + end + + def symbol_name(name) # :nodoc: + return name.to_sym if name.is_a?(Symbol) || name.is_a?(String) + nil + end + + def class_to_type_name(klass) # :nodoc: + klass = klass.class unless klass.is_a?(Class) + if derived_from?(Integer, klass) || derived_from?(Fixnum, klass) || derived_from?(Bignum, klass) + :int + elsif klass == String + :string + elsif klass == Base64 + :base64 + elsif klass == TrueClass || klass == FalseClass + :bool + elsif derived_from?(Float, klass) || derived_from?(Precision, klass) || derived_from?(Numeric, klass) + :float + elsif klass == Time + :time + elsif klass == DateTime + :datetime + elsif klass == Date + :date + else + nil + end + end + + def type_name_to_class(name) # :nodoc: + case canonical_type_name(name) + when :int + Integer + when :string + String + when :base64 + Base64 + when :bool + TrueClass + when :float + Float + when :decimal + BigDecimal + when :time + Time + when :date + Date + when :datetime + DateTime + else + nil + end + end + + def derived_from?(ancestor, child) # :nodoc: + child.ancestors.include?(ancestor) + end + + module_function :type_name_to_class + module_function :class_to_type_name + module_function :symbol_name + module_function :canonical_type_class + module_function :canonical_type_name + module_function :canonical_type + module_function :canonical_signature_entry + module_function :canonical_signature + module_function :derived_from? + end + + class BaseType # :nodoc: + include SignatureTypes + + attr :spec + attr :type + attr :type_class + attr :name + + def initialize(spec, type, name) + @spec = spec + @type = canonical_type(type) + @type_class = canonical_type_class(@type) + @name = name + end + + def custom? + false + end + + def array? + false + end + + def structured? + false + end + + def human_name(show_name=true) + type_type = array? ? element_type.type.to_s : self.type.to_s + str = array? ? (type_type + '[]') : type_type + show_name ? (str + " " + name.to_s) : str + end + end + + class ArrayType < BaseType # :nodoc: + attr :element_type + + def initialize(spec, element_type, name) + super(spec, Array, name) + @element_type = element_type + end + + def custom? + true + end + + def array? + true + end + end + + class StructuredType < BaseType # :nodoc: + def each_member + if @type_class.respond_to?(:members) + @type_class.members.each do |name, type| + yield name, type + end + elsif @type_class.respond_to?(:columns) + i = -1 + @type_class.columns.each do |column| + yield column.name, canonical_signature_entry(column.type, i += 1) + end + end + end + + def custom? + true + end + + def structured? + true + end + end + + class Base64 < String # :nodoc: + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb b/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb new file mode 100644 index 000000000..167613f68 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.erb @@ -0,0 +1,65 @@ + + + <%= @scaffold_class.wsdl_service_name %> Web Service + + + + +<%= @content_for_layout %> + + + diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml b/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/layout.rhtml new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb b/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb new file mode 100644 index 000000000..60dfe23f0 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/methods.erb @@ -0,0 +1,6 @@ +<% @scaffold_container.services.each do |service| %> + +

    API Methods for <%= service %>

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

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

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

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

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

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

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

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

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

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

    + +

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

    + +

    +Return Value:
    +

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

    + +

    +Request XML:
    +

    +<%= h @method_request_xml %>
    +
    +

    + +

    +Response XML:
    +

    +<%= h @method_response_xml %>
    +
    +

    + +

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

    diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml b/vendor/plugins/actionwebservice/lib/action_web_service/templates/scaffolds/result.rhtml new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb b/vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb new file mode 100644 index 000000000..7e714c941 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/test_invoke.rb @@ -0,0 +1,110 @@ +require 'test/unit' + +module Test # :nodoc: + module Unit # :nodoc: + class TestCase # :nodoc: + private + # invoke the specified API method + def invoke_direct(method_name, *args) + prepare_request('api', 'api', method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + alias_method :invoke, :invoke_direct + + # invoke the specified API method on the specified service + def invoke_delegated(service_name, method_name, *args) + prepare_request(service_name.to_s, service_name, method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + + # invoke the specified layered API method on the correct service + def invoke_layered(service_name, method_name, *args) + prepare_request('api', service_name, method_name, *args) + @controller.process(@request, @response) + decode_rpc_response + end + + # ---------------------- internal --------------------------- + + def prepare_request(action, service_name, api_method_name, *args) + @request.recycle! + @request.request_parameters['action'] = action + @request.env['REQUEST_METHOD'] = 'POST' + @request.env['HTTP_CONTENT_TYPE'] = 'text/xml' + @request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args) + case protocol + when ActionWebService::Protocol::Soap::SoapProtocol + soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}" + @request.env['HTTP_SOAPACTION'] = soap_action + when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + @request.env.delete('HTTP_SOAPACTION') + end + end + + def encode_rpc_call(service_name, api_method_name, *args) + case @controller.web_service_dispatching_mode + when :direct + api = @controller.class.web_service_api + when :delegated, :layered + api = @controller.web_service_object(service_name.to_sym).class.web_service_api + end + protocol.register_api(api) + method = api.api_methods[api_method_name.to_sym] + raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" if method && method.expects && args.length != method.expects.length + protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects) + end + + def decode_rpc_response + public_method_name, return_value = protocol.decode_response(@response.body) + exception = is_exception?(return_value) + raise exception if exception + return_value + end + + def public_method_name(service_name, api_method_name) + public_name = service_api(service_name).public_api_method_name(api_method_name) + if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol) + '%s.%s' % [service_name.to_s, public_name] + else + public_name + end + end + + def service_api(service_name) + case @controller.web_service_dispatching_mode + when :direct + @controller.class.web_service_api + when :delegated, :layered + @controller.web_service_object(service_name.to_sym).class.web_service_api + end + end + + def protocol + if @protocol.nil? + @protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) + else + case @protocol + when :xmlrpc + @protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller) + when :soap + @protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller) + else + @protocol + end + end + end + + def is_exception?(obj) + case protocol + when :soap, ActionWebService::Protocol::Soap::SoapProtocol + (obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \ + obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil + when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol + obj.is_a?(XMLRPC::FaultException) ? obj : nil + end + end + end + end +end diff --git a/vendor/plugins/actionwebservice/lib/action_web_service/version.rb b/vendor/plugins/actionwebservice/lib/action_web_service/version.rb new file mode 100644 index 000000000..a1b3d5929 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/action_web_service/version.rb @@ -0,0 +1,9 @@ +module ActionWebService + module VERSION #:nodoc: + MAJOR = 1 + MINOR = 2 + TINY = 5 + + STRING = [MAJOR, MINOR, TINY].join('.') + end +end diff --git a/vendor/plugins/actionwebservice/lib/actionwebservice.rb b/vendor/plugins/actionwebservice/lib/actionwebservice.rb new file mode 100644 index 000000000..25e3aa8e8 --- /dev/null +++ b/vendor/plugins/actionwebservice/lib/actionwebservice.rb @@ -0,0 +1 @@ +require 'action_web_service' diff --git a/vendor/plugins/actionwebservice/setup.rb b/vendor/plugins/actionwebservice/setup.rb new file mode 100644 index 000000000..aeef0d106 --- /dev/null +++ b/vendor/plugins/actionwebservice/setup.rb @@ -0,0 +1,1379 @@ +# +# setup.rb +# +# Copyright (c) 2000-2004 Minero Aoki +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Note: Originally licensed under LGPL v2+. Using MIT license for Rails +# with permission of Minero Aoki. + +# + +unless Enumerable.method_defined?(:map) # Ruby 1.4.6 + module Enumerable + alias map collect + end +end + +unless File.respond_to?(:read) # Ruby 1.6 + def File.read(fname) + open(fname) {|f| + return f.read + } + end +end + +def File.binread(fname) + open(fname, 'rb') {|f| + return f.read + } +end + +# for corrupted windows stat(2) +def File.dir?(path) + File.directory?((path[-1,1] == '/') ? path : path + '/') +end + + +class SetupError < StandardError; end + +def setup_rb_error(msg) + raise SetupError, msg +end + +# +# Config +# + +if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } + ARGV.delete(arg) + require arg.split(/=/, 2)[1] + $".push 'rbconfig.rb' +else + require 'rbconfig' +end + +def multipackage_install? + FileTest.directory?(File.dirname($0) + '/packages') +end + + +class ConfigItem + def initialize(name, template, default, desc) + @name = name.freeze + @template = template + @value = default + @default = default.dup.freeze + @description = desc + end + + attr_reader :name + attr_reader :description + + attr_accessor :default + alias help_default default + + def help_opt + "--#{@name}=#{@template}" + end + + def value + @value + end + + def eval(table) + @value.gsub(%r<\$([^/]+)>) { table[$1] } + end + + def set(val) + @value = check(val) + end + + private + + def check(val) + setup_rb_error "config: --#{name} requires argument" unless val + val + end +end + +class BoolItem < ConfigItem + def config_type + 'bool' + end + + def help_opt + "--#{@name}" + end + + private + + def check(val) + return 'yes' unless val + unless /\A(y(es)?|n(o)?|t(rue)?|f(alse))\z/i =~ val + setup_rb_error "config: --#{@name} accepts only yes/no for argument" + end + (/\Ay(es)?|\At(rue)/i =~ value) ? 'yes' : 'no' + end +end + +class PathItem < ConfigItem + def config_type + 'path' + end + + private + + def check(path) + setup_rb_error "config: --#{@name} requires argument" unless path + path[0,1] == '$' ? path : File.expand_path(path) + end +end + +class ProgramItem < ConfigItem + def config_type + 'program' + end +end + +class SelectItem < ConfigItem + def initialize(name, template, default, desc) + super + @ok = template.split('/') + end + + def config_type + 'select' + end + + private + + def check(val) + unless @ok.include?(val.strip) + setup_rb_error "config: use --#{@name}=#{@template} (#{val})" + end + val.strip + end +end + +class PackageSelectionItem < ConfigItem + def initialize(name, template, default, help_default, desc) + super name, template, default, desc + @help_default = help_default + end + + attr_reader :help_default + + def config_type + 'package' + end + + private + + def check(val) + unless File.dir?("packages/#{val}") + setup_rb_error "config: no such package: #{val}" + end + val + end +end + +class ConfigTable_class + + def initialize(items) + @items = items + @table = {} + items.each do |i| + @table[i.name] = i + end + ALIASES.each do |ali, name| + @table[ali] = @table[name] + end + end + + include Enumerable + + def each(&block) + @items.each(&block) + end + + def key?(name) + @table.key?(name) + end + + def lookup(name) + @table[name] or raise ArgumentError, "no such config item: #{name}" + end + + def add(item) + @items.push item + @table[item.name] = item + end + + def remove(name) + item = lookup(name) + @items.delete_if {|i| i.name == name } + @table.delete_if {|name, i| i.name == name } + item + end + + def new + dup() + end + + def savefile + '.config' + end + + def load + begin + t = dup() + File.foreach(savefile()) do |line| + k, v = *line.split(/=/, 2) + t[k] = v.strip + end + t + rescue Errno::ENOENT + setup_rb_error $!.message + "#{File.basename($0)} config first" + end + end + + def save + @items.each {|i| i.value } + File.open(savefile(), 'w') {|f| + @items.each do |i| + f.printf "%s=%s\n", i.name, i.value if i.value + end + } + end + + def [](key) + lookup(key).eval(self) + end + + def []=(key, val) + lookup(key).set val + end + +end + +c = ::Config::CONFIG + +rubypath = c['bindir'] + '/' + c['ruby_install_name'] + +major = c['MAJOR'].to_i +minor = c['MINOR'].to_i +teeny = c['TEENY'].to_i +version = "#{major}.#{minor}" + +# ruby ver. >= 1.4.4? +newpath_p = ((major >= 2) or + ((major == 1) and + ((minor >= 5) or + ((minor == 4) and (teeny >= 4))))) + +if c['rubylibdir'] + # V < 1.6.3 + _stdruby = c['rubylibdir'] + _siteruby = c['sitedir'] + _siterubyver = c['sitelibdir'] + _siterubyverarch = c['sitearchdir'] +elsif newpath_p + # 1.4.4 <= V <= 1.6.3 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = c['sitedir'] + _siterubyver = "$siteruby/#{version}" + _siterubyverarch = "$siterubyver/#{c['arch']}" +else + # V < 1.4.4 + _stdruby = "$prefix/lib/ruby/#{version}" + _siteruby = "$prefix/lib/ruby/#{version}/site_ruby" + _siterubyver = _siteruby + _siterubyverarch = "$siterubyver/#{c['arch']}" +end +libdir = '-* dummy libdir *-' +stdruby = '-* dummy rubylibdir *-' +siteruby = '-* dummy site_ruby *-' +siterubyver = '-* dummy site_ruby version *-' +parameterize = lambda {|path| + path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')\ + .sub(/\A#{Regexp.quote(libdir)}/, '$libdir')\ + .sub(/\A#{Regexp.quote(stdruby)}/, '$stdruby')\ + .sub(/\A#{Regexp.quote(siteruby)}/, '$siteruby')\ + .sub(/\A#{Regexp.quote(siterubyver)}/, '$siterubyver') +} +libdir = parameterize.call(c['libdir']) +stdruby = parameterize.call(_stdruby) +siteruby = parameterize.call(_siteruby) +siterubyver = parameterize.call(_siterubyver) +siterubyverarch = parameterize.call(_siterubyverarch) + +if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } + makeprog = arg.sub(/'/, '').split(/=/, 2)[1] +else + makeprog = 'make' +end + +common_conf = [ + PathItem.new('prefix', 'path', c['prefix'], + 'path prefix of target environment'), + PathItem.new('bindir', 'path', parameterize.call(c['bindir']), + 'the directory for commands'), + PathItem.new('libdir', 'path', libdir, + 'the directory for libraries'), + PathItem.new('datadir', 'path', parameterize.call(c['datadir']), + 'the directory for shared data'), + PathItem.new('mandir', 'path', parameterize.call(c['mandir']), + 'the directory for man pages'), + PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), + 'the directory for man pages'), + PathItem.new('stdruby', 'path', stdruby, + 'the directory for standard ruby libraries'), + PathItem.new('siteruby', 'path', siteruby, + 'the directory for version-independent aux ruby libraries'), + PathItem.new('siterubyver', 'path', siterubyver, + 'the directory for aux ruby libraries'), + PathItem.new('siterubyverarch', 'path', siterubyverarch, + 'the directory for aux ruby binaries'), + PathItem.new('rbdir', 'path', '$siterubyver', + 'the directory for ruby scripts'), + PathItem.new('sodir', 'path', '$siterubyverarch', + 'the directory for ruby extentions'), + PathItem.new('rubypath', 'path', rubypath, + 'the path to set to #! line'), + ProgramItem.new('rubyprog', 'name', rubypath, + 'the ruby program using for installation'), + ProgramItem.new('makeprog', 'name', makeprog, + 'the make program to compile ruby extentions'), + SelectItem.new('shebang', 'all/ruby/never', 'ruby', + 'shebang line (#!) editing mode'), + BoolItem.new('without-ext', 'yes/no', 'no', + 'does not compile/install ruby extentions') +] +class ConfigTable_class # open again + ALIASES = { + 'std-ruby' => 'stdruby', + 'site-ruby-common' => 'siteruby', # For backward compatibility + 'site-ruby' => 'siterubyver', # For backward compatibility + 'bin-dir' => 'bindir', + 'bin-dir' => 'bindir', + 'rb-dir' => 'rbdir', + 'so-dir' => 'sodir', + 'data-dir' => 'datadir', + 'ruby-path' => 'rubypath', + 'ruby-prog' => 'rubyprog', + 'ruby' => 'rubyprog', + 'make-prog' => 'makeprog', + 'make' => 'makeprog' + } +end +multipackage_conf = [ + PackageSelectionItem.new('with', 'name,name...', '', 'ALL', + 'package names that you want to install'), + PackageSelectionItem.new('without', 'name,name...', '', 'NONE', + 'package names that you do not want to install') +] +if multipackage_install? + ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf) +else + ConfigTable = ConfigTable_class.new(common_conf) +end + + +module MetaConfigAPI + + def eval_file_ifexist(fname) + instance_eval File.read(fname), fname, 1 if File.file?(fname) + end + + def config_names + ConfigTable.map {|i| i.name } + end + + def config?(name) + ConfigTable.key?(name) + end + + def bool_config?(name) + ConfigTable.lookup(name).config_type == 'bool' + end + + def path_config?(name) + ConfigTable.lookup(name).config_type == 'path' + end + + def value_config?(name) + case ConfigTable.lookup(name).config_type + when 'bool', 'path' + true + else + false + end + end + + def add_config(item) + ConfigTable.add item + end + + def add_bool_config(name, default, desc) + ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) + end + + def add_path_config(name, default, desc) + ConfigTable.add PathItem.new(name, 'path', default, desc) + end + + def set_config_default(name, default) + ConfigTable.lookup(name).default = default + end + + def remove_config(name) + ConfigTable.remove(name) + end + +end + + +# +# File Operations +# + +module FileOperations + + def mkdir_p(dirname, prefix = nil) + dirname = prefix + File.expand_path(dirname) if prefix + $stderr.puts "mkdir -p #{dirname}" if verbose? + return if no_harm? + + # does not check '/'... it's too abnormal case + dirs = File.expand_path(dirname).split(%r<(?=/)>) + if /\A[a-z]:\z/i =~ dirs[0] + disk = dirs.shift + dirs[0] = disk + dirs[0] + end + dirs.each_index do |idx| + path = dirs[0..idx].join('') + Dir.mkdir path unless File.dir?(path) + end + end + + def rm_f(fname) + $stderr.puts "rm -f #{fname}" if verbose? + return if no_harm? + + if File.exist?(fname) or File.symlink?(fname) + File.chmod 0777, fname + File.unlink fname + end + end + + def rm_rf(dn) + $stderr.puts "rm -rf #{dn}" if verbose? + return if no_harm? + + Dir.chdir dn + Dir.foreach('.') do |fn| + next if fn == '.' + next if fn == '..' + if File.dir?(fn) + verbose_off { + rm_rf fn + } + else + verbose_off { + rm_f fn + } + end + end + Dir.chdir '..' + Dir.rmdir dn + end + + def move_file(src, dest) + File.unlink dest if File.exist?(dest) + begin + File.rename src, dest + rescue + File.open(dest, 'wb') {|f| f.write File.binread(src) } + File.chmod File.stat(src).mode, dest + File.unlink src + end + end + + def install(from, dest, mode, prefix = nil) + $stderr.puts "install #{from} #{dest}" if verbose? + return if no_harm? + + realdest = prefix ? prefix + File.expand_path(dest) : dest + realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) + str = File.binread(from) + if diff?(str, realdest) + verbose_off { + rm_f realdest if File.exist?(realdest) + } + File.open(realdest, 'wb') {|f| + f.write str + } + File.chmod mode, realdest + + File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| + if prefix + f.puts realdest.sub(prefix, '') + else + f.puts realdest + end + } + end + end + + def diff?(new_content, path) + return true unless File.exist?(path) + new_content != File.binread(path) + end + + def command(str) + $stderr.puts str if verbose? + system str or raise RuntimeError, "'system #{str}' failed" + end + + def ruby(str) + command config('rubyprog') + ' ' + str + end + + def make(task = '') + command config('makeprog') + ' ' + task + end + + def extdir?(dir) + File.exist?(dir + '/MANIFEST') + end + + def all_files_in(dirname) + Dir.open(dirname) {|d| + return d.select {|ent| File.file?("#{dirname}/#{ent}") } + } + end + + REJECT_DIRS = %w( + CVS SCCS RCS CVS.adm .svn + ) + + def all_dirs_in(dirname) + Dir.open(dirname) {|d| + return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS + } + end + +end + + +# +# Main Installer +# + +module HookUtils + + def run_hook(name) + try_run_hook "#{curr_srcdir()}/#{name}" or + try_run_hook "#{curr_srcdir()}/#{name}.rb" + end + + def try_run_hook(fname) + return false unless File.file?(fname) + begin + instance_eval File.read(fname), fname, 1 + rescue + setup_rb_error "hook #{fname} failed:\n" + $!.message + end + true + end + +end + + +module HookScriptAPI + + def get_config(key) + @config[key] + end + + alias config get_config + + def set_config(key, val) + @config[key] = val + end + + # + # srcdir/objdir (works only in the package directory) + # + + #abstract srcdir_root + #abstract objdir_root + #abstract relpath + + def curr_srcdir + "#{srcdir_root()}/#{relpath()}" + end + + def curr_objdir + "#{objdir_root()}/#{relpath()}" + end + + def srcfile(path) + "#{curr_srcdir()}/#{path}" + end + + def srcexist?(path) + File.exist?(srcfile(path)) + end + + def srcdirectory?(path) + File.dir?(srcfile(path)) + end + + def srcfile?(path) + File.file? srcfile(path) + end + + def srcentries(path = '.') + Dir.open("#{curr_srcdir()}/#{path}") {|d| + return d.to_a - %w(. ..) + } + end + + def srcfiles(path = '.') + srcentries(path).select {|fname| + File.file?(File.join(curr_srcdir(), path, fname)) + } + end + + def srcdirectories(path = '.') + srcentries(path).select {|fname| + File.dir?(File.join(curr_srcdir(), path, fname)) + } + end + +end + + +class ToplevelInstaller + + Version = '3.3.1' + Copyright = 'Copyright (c) 2000-2004 Minero Aoki' + + TASKS = [ + [ 'all', 'do config, setup, then install' ], + [ 'config', 'saves your configurations' ], + [ 'show', 'shows current configuration' ], + [ 'setup', 'compiles ruby extentions and others' ], + [ 'install', 'installs files' ], + [ 'clean', "does `make clean' for each extention" ], + [ 'distclean',"does `make distclean' for each extention" ] + ] + + def ToplevelInstaller.invoke + instance().invoke + end + + @singleton = nil + + def ToplevelInstaller.instance + @singleton ||= new(File.dirname($0)) + @singleton + end + + include MetaConfigAPI + + def initialize(ardir_root) + @config = nil + @options = { 'verbose' => true } + @ardir = File.expand_path(ardir_root) + end + + def inspect + "#<#{self.class} #{__id__()}>" + end + + def invoke + run_metaconfigs + case task = parsearg_global() + when nil, 'all' + @config = load_config('config') + parsearg_config + init_installers + exec_config + exec_setup + exec_install + else + @config = load_config(task) + __send__ "parsearg_#{task}" + init_installers + __send__ "exec_#{task}" + end + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + end + + def load_config(task) + case task + when 'config' + ConfigTable.new + when 'clean', 'distclean' + if File.exist?(ConfigTable.savefile) + then ConfigTable.load + else ConfigTable.new + end + else + ConfigTable.load + end + end + + def init_installers + @installer = Installer.new(@config, @options, @ardir, File.expand_path('.')) + end + + # + # Hook Script API bases + # + + def srcdir_root + @ardir + end + + def objdir_root + '.' + end + + def relpath + '.' + end + + # + # Option Parsing + # + + def parsearg_global + valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/ + + while arg = ARGV.shift + case arg + when /\A\w+\z/ + setup_rb_error "invalid task: #{arg}" unless valid_task =~ arg + return arg + + when '-q', '--quiet' + @options['verbose'] = false + + when '--verbose' + @options['verbose'] = true + + when '-h', '--help' + print_usage $stdout + exit 0 + + when '-v', '--version' + puts "#{File.basename($0)} version #{Version}" + exit 0 + + when '--copyright' + puts Copyright + exit 0 + + else + setup_rb_error "unknown global option '#{arg}'" + end + end + + nil + end + + + def parsearg_no_options + unless ARGV.empty? + setup_rb_error "#{task}: unknown options: #{ARGV.join ' '}" + end + end + + alias parsearg_show parsearg_no_options + alias parsearg_setup parsearg_no_options + alias parsearg_clean parsearg_no_options + alias parsearg_distclean parsearg_no_options + + def parsearg_config + re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/ + @options['config-opt'] = [] + + while i = ARGV.shift + if /\A--?\z/ =~ i + @options['config-opt'] = ARGV.dup + break + end + m = re.match(i) or setup_rb_error "config: unknown option #{i}" + name, value = *m.to_a[1,2] + @config[name] = value + end + end + + def parsearg_install + @options['no-harm'] = false + @options['install-prefix'] = '' + while a = ARGV.shift + case a + when /\A--no-harm\z/ + @options['no-harm'] = true + when /\A--prefix=(.*)\z/ + path = $1 + path = File.expand_path(path) unless path[0,1] == '/' + @options['install-prefix'] = path + else + setup_rb_error "install: unknown option #{a}" + end + end + end + + def print_usage(out) + out.puts 'Typical Installation Procedure:' + out.puts " $ ruby #{File.basename $0} config" + out.puts " $ ruby #{File.basename $0} setup" + out.puts " # ruby #{File.basename $0} install (may require root privilege)" + out.puts + out.puts 'Detailed Usage:' + out.puts " ruby #{File.basename $0} " + out.puts " ruby #{File.basename $0} [] []" + + fmt = " %-24s %s\n" + out.puts + out.puts 'Global options:' + out.printf fmt, '-q,--quiet', 'suppress message outputs' + out.printf fmt, ' --verbose', 'output messages verbosely' + out.printf fmt, '-h,--help', 'print this message' + out.printf fmt, '-v,--version', 'print version and quit' + out.printf fmt, ' --copyright', 'print copyright and quit' + out.puts + out.puts 'Tasks:' + TASKS.each do |name, desc| + out.printf fmt, name, desc + end + + fmt = " %-24s %s [%s]\n" + out.puts + out.puts 'Options for CONFIG or ALL:' + ConfigTable.each do |item| + out.printf fmt, item.help_opt, item.description, item.help_default + end + out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" + out.puts + out.puts 'Options for INSTALL:' + out.printf fmt, '--no-harm', 'only display what to do if given', 'off' + out.printf fmt, '--prefix=path', 'install path prefix', '$prefix' + out.puts + end + + # + # Task Handlers + # + + def exec_config + @installer.exec_config + @config.save # must be final + end + + def exec_setup + @installer.exec_setup + end + + def exec_install + @installer.exec_install + end + + def exec_show + ConfigTable.each do |i| + printf "%-20s %s\n", i.name, i.value + end + end + + def exec_clean + @installer.exec_clean + end + + def exec_distclean + @installer.exec_distclean + end + +end + + +class ToplevelInstallerMulti < ToplevelInstaller + + include HookUtils + include HookScriptAPI + include FileOperations + + def initialize(ardir) + super + @packages = all_dirs_in("#{@ardir}/packages") + raise 'no package exists' if @packages.empty? + end + + def run_metaconfigs + eval_file_ifexist "#{@ardir}/metaconfig" + @packages.each do |name| + eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig" + end + end + + def init_installers + @installers = {} + @packages.each do |pack| + @installers[pack] = Installer.new(@config, @options, + "#{@ardir}/packages/#{pack}", + "packages/#{pack}") + end + + with = extract_selection(config('with')) + without = extract_selection(config('without')) + @selected = @installers.keys.select {|name| + (with.empty? or with.include?(name)) \ + and not without.include?(name) + } + end + + def extract_selection(list) + a = list.split(/,/) + a.each do |name| + setup_rb_error "no such package: #{name}" unless @installers.key?(name) + end + a + end + + def print_usage(f) + super + f.puts 'Included packages:' + f.puts ' ' + @packages.sort.join(' ') + f.puts + end + + # + # multi-package metaconfig API + # + + attr_reader :packages + + def declare_packages(list) + raise 'package list is empty' if list.empty? + list.each do |name| + raise "directory packages/#{name} does not exist"\ + unless File.dir?("#{@ardir}/packages/#{name}") + end + @packages = list + end + + # + # Task Handlers + # + + def exec_config + run_hook 'pre-config' + each_selected_installers {|inst| inst.exec_config } + run_hook 'post-config' + @config.save # must be final + end + + def exec_setup + run_hook 'pre-setup' + each_selected_installers {|inst| inst.exec_setup } + run_hook 'post-setup' + end + + def exec_install + run_hook 'pre-install' + each_selected_installers {|inst| inst.exec_install } + run_hook 'post-install' + end + + def exec_clean + rm_f ConfigTable.savefile + run_hook 'pre-clean' + each_selected_installers {|inst| inst.exec_clean } + run_hook 'post-clean' + end + + def exec_distclean + rm_f ConfigTable.savefile + run_hook 'pre-distclean' + each_selected_installers {|inst| inst.exec_distclean } + run_hook 'post-distclean' + end + + # + # lib + # + + def each_selected_installers + Dir.mkdir 'packages' unless File.dir?('packages') + @selected.each do |pack| + $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose'] + Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") + Dir.chdir "packages/#{pack}" + yield @installers[pack] + Dir.chdir '../..' + end + end + + def verbose? + @options['verbose'] + end + + def no_harm? + @options['no-harm'] + end + +end + + +class Installer + + FILETYPES = %w( bin lib ext data ) + + include HookScriptAPI + include HookUtils + include FileOperations + + def initialize(config, opt, srcroot, objroot) + @config = config + @options = opt + @srcdir = File.expand_path(srcroot) + @objdir = File.expand_path(objroot) + @currdir = '.' + end + + def inspect + "#<#{self.class} #{File.basename(@srcdir)}>" + end + + # + # Hook Script API base methods + # + + def srcdir_root + @srcdir + end + + def objdir_root + @objdir + end + + def relpath + @currdir + end + + # + # configs/options + # + + def no_harm? + @options['no-harm'] + end + + def verbose? + @options['verbose'] + end + + def verbose_off + begin + save, @options['verbose'] = @options['verbose'], false + yield + ensure + @options['verbose'] = save + end + end + + # + # TASK config + # + + def exec_config + exec_task_traverse 'config' + end + + def config_dir_bin(rel) + end + + def config_dir_lib(rel) + end + + def config_dir_ext(rel) + extconf if extdir?(curr_srcdir()) + end + + def extconf + opt = @options['config-opt'].join(' ') + command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}" + end + + def config_dir_data(rel) + end + + # + # TASK setup + # + + def exec_setup + exec_task_traverse 'setup' + end + + def setup_dir_bin(rel) + all_files_in(curr_srcdir()).each do |fname| + adjust_shebang "#{curr_srcdir()}/#{fname}" + end + end + + def adjust_shebang(path) + return if no_harm? + tmpfile = File.basename(path) + '.tmp' + begin + File.open(path, 'rb') {|r| + first = r.gets + return unless File.basename(config('rubypath')) == 'ruby' + return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby' + $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose? + File.open(tmpfile, 'wb') {|w| + w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath')) + w.write r.read + } + move_file tmpfile, File.basename(path) + } + ensure + File.unlink tmpfile if File.exist?(tmpfile) + end + end + + def setup_dir_lib(rel) + end + + def setup_dir_ext(rel) + make if extdir?(curr_srcdir()) + end + + def setup_dir_data(rel) + end + + # + # TASK install + # + + def exec_install + rm_f 'InstalledFiles' + exec_task_traverse 'install' + end + + def install_dir_bin(rel) + install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755 + end + + def install_dir_lib(rel) + install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644 + end + + def install_dir_ext(rel) + return unless extdir?(curr_srcdir()) + install_files ruby_extentions('.'), + "#{config('sodir')}/#{File.dirname(rel)}", + 0555 + end + + def install_dir_data(rel) + install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644 + end + + def install_files(list, dest, mode) + mkdir_p dest, @options['install-prefix'] + list.each do |fname| + install fname, dest, mode, @options['install-prefix'] + end + end + + def ruby_scripts + collect_filenames_auto().select {|n| /\.rb\z/ =~ n } + end + + # picked up many entries from cvs-1.11.1/src/ignore.c + reject_patterns = %w( + core RCSLOG tags TAGS .make.state + .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb + *~ *.old *.bak *.BAK *.orig *.rej _$* *$ + + *.org *.in .* + ) + mapping = { + '.' => '\.', + '$' => '\$', + '#' => '\#', + '*' => '.*' + } + REJECT_PATTERNS = Regexp.new('\A(?:' + + reject_patterns.map {|pat| + pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] } + }.join('|') + + ')\z') + + def collect_filenames_auto + mapdir((existfiles() - hookfiles()).reject {|fname| + REJECT_PATTERNS =~ fname + }) + end + + def existfiles + all_files_in(curr_srcdir()) | all_files_in('.') + end + + def hookfiles + %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| + %w( config setup install clean ).map {|t| sprintf(fmt, t) } + }.flatten + end + + def mapdir(filelist) + filelist.map {|fname| + if File.exist?(fname) # objdir + fname + else # srcdir + File.join(curr_srcdir(), fname) + end + } + end + + def ruby_extentions(dir) + Dir.open(dir) {|d| + ents = d.select {|fname| /\.#{::Config::CONFIG['DLEXT']}\z/ =~ fname } + if ents.empty? + setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" + end + return ents + } + end + + # + # TASK clean + # + + def exec_clean + exec_task_traverse 'clean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def clean_dir_bin(rel) + end + + def clean_dir_lib(rel) + end + + def clean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'clean' if File.file?('Makefile') + end + + def clean_dir_data(rel) + end + + # + # TASK distclean + # + + def exec_distclean + exec_task_traverse 'distclean' + rm_f ConfigTable.savefile + rm_f 'InstalledFiles' + end + + def distclean_dir_bin(rel) + end + + def distclean_dir_lib(rel) + end + + def distclean_dir_ext(rel) + return unless extdir?(curr_srcdir()) + make 'distclean' if File.file?('Makefile') + end + + # + # lib + # + + def exec_task_traverse(task) + run_hook "pre-#{task}" + FILETYPES.each do |type| + if config('without-ext') == 'yes' and type == 'ext' + $stderr.puts 'skipping ext/* by user option' if verbose? + next + end + traverse task, type, "#{task}_dir_#{type}" + end + run_hook "post-#{task}" + end + + def traverse(task, rel, mid) + dive_into(rel) { + run_hook "pre-#{task}" + __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') + all_dirs_in(curr_srcdir()).each do |d| + traverse task, "#{rel}/#{d}", mid + end + run_hook "post-#{task}" + } + end + + def dive_into(rel) + return unless File.dir?("#{@srcdir}/#{rel}") + + dir = File.basename(rel) + Dir.mkdir dir unless File.dir?(dir) + prevdir = Dir.pwd + Dir.chdir dir + $stderr.puts '---> ' + rel if verbose? + @currdir = rel + yield + Dir.chdir prevdir + $stderr.puts '<--- ' + rel if verbose? + @currdir = File.dirname(rel) + end + +end + + +if $0 == __FILE__ + begin + if multipackage_install? + ToplevelInstallerMulti.invoke + else + ToplevelInstaller.invoke + end + rescue SetupError + raise if $DEBUG + $stderr.puts $!.message + $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." + exit 1 + end +end diff --git a/vendor/plugins/acts_as_list/README b/vendor/plugins/acts_as_list/README new file mode 100644 index 000000000..36ae3188e --- /dev/null +++ b/vendor/plugins/acts_as_list/README @@ -0,0 +1,23 @@ +ActsAsList +========== + +This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table. + + +Example +======= + + class TodoList < ActiveRecord::Base + has_many :todo_items, :order => "position" + end + + class TodoItem < ActiveRecord::Base + belongs_to :todo_list + acts_as_list :scope => :todo_list + end + + todo_list.first.move_to_bottom + todo_list.last.move_higher + + +Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license \ No newline at end of file diff --git a/vendor/plugins/acts_as_list/init.rb b/vendor/plugins/acts_as_list/init.rb new file mode 100644 index 000000000..eb87e8790 --- /dev/null +++ b/vendor/plugins/acts_as_list/init.rb @@ -0,0 +1,3 @@ +$:.unshift "#{File.dirname(__FILE__)}/lib" +require 'active_record/acts/list' +ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List } diff --git a/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb b/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb new file mode 100644 index 000000000..00d86928d --- /dev/null +++ b/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb @@ -0,0 +1,256 @@ +module ActiveRecord + module Acts #:nodoc: + module List #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list. + # The class that has this specified needs to have a +position+ column defined as an integer on + # the mapped database table. + # + # Todo list example: + # + # class TodoList < ActiveRecord::Base + # has_many :todo_items, :order => "position" + # end + # + # class TodoItem < ActiveRecord::Base + # belongs_to :todo_list + # acts_as_list :scope => :todo_list + # end + # + # todo_list.first.move_to_bottom + # todo_list.last.move_higher + module ClassMethods + # Configuration options are: + # + # * +column+ - specifies the column name to use for keeping the position integer (default: +position+) + # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id + # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible + # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. + # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' + def acts_as_list(options = {}) + configuration = { :column => "position", :scope => "1 = 1" } + configuration.update(options) if options.is_a?(Hash) + + configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ + + if configuration[:scope].is_a?(Symbol) + scope_condition_method = %( + def scope_condition + if #{configuration[:scope].to_s}.nil? + "#{configuration[:scope].to_s} IS NULL" + else + "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" + end + end + ) + else + scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" + end + + class_eval <<-EOV + include ActiveRecord::Acts::List::InstanceMethods + + def acts_as_list_class + ::#{self.name} + end + + def position_column + '#{configuration[:column]}' + end + + #{scope_condition_method} + + before_destroy :remove_from_list + before_create :add_to_list_bottom + EOV + end + end + + # All the methods available to a record that has had acts_as_list specified. Each method works + # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter + # lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is + # the first in the list of all chapters. + module InstanceMethods + # Insert the item at the given position (defaults to the top position of 1). + def insert_at(position = 1) + insert_at_position(position) + end + + # Swap positions with the next lower item, if one exists. + def move_lower + return unless lower_item + + acts_as_list_class.transaction do + lower_item.decrement_position + increment_position + end + end + + # Swap positions with the next higher item, if one exists. + def move_higher + return unless higher_item + + acts_as_list_class.transaction do + higher_item.increment_position + decrement_position + end + end + + # Move to the bottom of the list. If the item is already in the list, the items below it have their + # position adjusted accordingly. + def move_to_bottom + return unless in_list? + acts_as_list_class.transaction do + decrement_positions_on_lower_items + assume_bottom_position + end + end + + # Move to the top of the list. If the item is already in the list, the items above it have their + # position adjusted accordingly. + def move_to_top + return unless in_list? + acts_as_list_class.transaction do + increment_positions_on_higher_items + assume_top_position + end + end + + # Removes the item from the list. + def remove_from_list + if in_list? + decrement_positions_on_lower_items + update_attribute position_column, nil + end + end + + # Increase the position of this item without adjusting the rest of the list. + def increment_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i + 1 + end + + # Decrease the position of this item without adjusting the rest of the list. + def decrement_position + return unless in_list? + update_attribute position_column, self.send(position_column).to_i - 1 + end + + # Return +true+ if this object is the first in the list. + def first? + return false unless in_list? + self.send(position_column) == 1 + end + + # Return +true+ if this object is the last in the list. + def last? + return false unless in_list? + self.send(position_column) == bottom_position_in_list + end + + # Return the next higher item in the list. + def higher_item + return nil unless in_list? + acts_as_list_class.find(:first, :conditions => + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" + ) + end + + # Return the next lower item in the list. + def lower_item + return nil unless in_list? + acts_as_list_class.find(:first, :conditions => + "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" + ) + end + + # Test if this record is in a list + def in_list? + !send(position_column).nil? + end + + private + def add_to_list_top + increment_positions_on_all_items + end + + def add_to_list_bottom + self[position_column] = bottom_position_in_list.to_i + 1 + end + + # Overwrite this method to define the scope of the list changes + def scope_condition() "1" end + + # Returns the bottom position number in the list. + # bottom_position_in_list # => 2 + def bottom_position_in_list(except = nil) + item = bottom_item(except) + item ? item.send(position_column) : 0 + end + + # Returns the bottom item + def bottom_item(except = nil) + conditions = scope_condition + conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except + acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC") + end + + # Forces item to assume the bottom position in the list. + def assume_bottom_position + update_attribute(position_column, bottom_position_in_list(self).to_i + 1) + end + + # Forces item to assume the top position in the list. + def assume_top_position + update_attribute(position_column, 1) + end + + # This has the effect of moving all the higher items up one. + def decrement_positions_on_higher_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}" + ) + end + + # This has the effect of moving all the lower items up one. + def decrement_positions_on_lower_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the higher items down one. + def increment_positions_on_higher_items + return unless in_list? + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}" + ) + end + + # This has the effect of moving all the lower items down one. + def increment_positions_on_lower_items(position) + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" + ) + end + + # Increments position (position_column) of all items in the list. + def increment_positions_on_all_items + acts_as_list_class.update_all( + "#{position_column} = (#{position_column} + 1)", "#{scope_condition}" + ) + end + + def insert_at_position(position) + remove_from_list + increment_positions_on_lower_items(position) + self.update_attribute(position_column, position) + end + end + end + end +end diff --git a/vendor/plugins/acts_as_list/test/list_test.rb b/vendor/plugins/acts_as_list/test/list_test.rb new file mode 100644 index 000000000..e89cb8e12 --- /dev/null +++ b/vendor/plugins/acts_as_list/test/list_test.rb @@ -0,0 +1,332 @@ +require 'test/unit' + +require 'rubygems' +gem 'activerecord', '>= 1.15.4.7794' +require 'active_record' + +require "#{File.dirname(__FILE__)}/../init" + +ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") + +def setup_db + ActiveRecord::Schema.define(:version => 1) do + create_table :mixins do |t| + t.column :pos, :integer + t.column :parent_id, :integer + t.column :created_at, :datetime + t.column :updated_at, :datetime + end + end +end + +def teardown_db + ActiveRecord::Base.connection.tables.each do |table| + ActiveRecord::Base.connection.drop_table(table) + end +end + +class Mixin < ActiveRecord::Base +end + +class ListMixin < Mixin + acts_as_list :column => "pos", :scope => :parent + + def self.table_name() "mixins" end +end + +class ListMixinSub1 < ListMixin +end + +class ListMixinSub2 < ListMixin +end + +class ListWithStringScopeMixin < ActiveRecord::Base + acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}' + + def self.table_name() "mixins" end +end + + +class ListTest < Test::Unit::TestCase + + def setup + setup_db + (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 } + end + + def teardown + teardown_db + end + + def test_reordering + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(2).move_lower + assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(2).move_higher + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(1).move_to_bottom + assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(1).move_to_top + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(2).move_to_bottom + assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(4).move_to_top + assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + end + + def test_move_to_bottom_with_next_to_last_item + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + ListMixin.find(3).move_to_bottom + assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + end + + def test_next_prev + assert_equal ListMixin.find(2), ListMixin.find(1).lower_item + assert_nil ListMixin.find(1).higher_item + assert_equal ListMixin.find(3), ListMixin.find(4).higher_item + assert_nil ListMixin.find(4).lower_item + end + + def test_injection + item = ListMixin.new(:parent_id => 1) + assert_equal "parent_id = 1", item.scope_condition + assert_equal "pos", item.position_column + end + + def test_insert + new = ListMixin.create(:parent_id => 20) + assert_equal 1, new.pos + assert new.first? + assert new.last? + + new = ListMixin.create(:parent_id => 20) + assert_equal 2, new.pos + assert !new.first? + assert new.last? + + new = ListMixin.create(:parent_id => 20) + assert_equal 3, new.pos + assert !new.first? + assert new.last? + + new = ListMixin.create(:parent_id => 0) + assert_equal 1, new.pos + assert new.first? + assert new.last? + end + + def test_insert_at + new = ListMixin.create(:parent_id => 20) + assert_equal 1, new.pos + + new = ListMixin.create(:parent_id => 20) + assert_equal 2, new.pos + + new = ListMixin.create(:parent_id => 20) + assert_equal 3, new.pos + + new4 = ListMixin.create(:parent_id => 20) + assert_equal 4, new4.pos + + new4.insert_at(3) + assert_equal 3, new4.pos + + new.reload + assert_equal 4, new.pos + + new.insert_at(2) + assert_equal 2, new.pos + + new4.reload + assert_equal 4, new4.pos + + new5 = ListMixin.create(:parent_id => 20) + assert_equal 5, new5.pos + + new5.insert_at(1) + assert_equal 1, new5.pos + + new4.reload + assert_equal 5, new4.pos + end + + def test_delete_middle + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(2).destroy + + assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + assert_equal 1, ListMixin.find(1).pos + assert_equal 2, ListMixin.find(3).pos + assert_equal 3, ListMixin.find(4).pos + + ListMixin.find(1).destroy + + assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + assert_equal 1, ListMixin.find(3).pos + assert_equal 2, ListMixin.find(4).pos + end + + def test_with_string_based_scope + new = ListWithStringScopeMixin.create(:parent_id => 500) + assert_equal 1, new.pos + assert new.first? + assert new.last? + end + + def test_nil_scope + new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create + new2.move_higher + assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos') + end + + + def test_remove_from_list_should_then_fail_in_list? + assert_equal true, ListMixin.find(1).in_list? + ListMixin.find(1).remove_from_list + assert_equal false, ListMixin.find(1).in_list? + end + + def test_remove_from_list_should_set_position_to_nil + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(2).remove_from_list + + assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + assert_equal 1, ListMixin.find(1).pos + assert_equal nil, ListMixin.find(2).pos + assert_equal 2, ListMixin.find(3).pos + assert_equal 3, ListMixin.find(4).pos + end + + def test_remove_before_destroy_does_not_shift_lower_items_twice + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + ListMixin.find(2).remove_from_list + ListMixin.find(2).destroy + + assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id) + + assert_equal 1, ListMixin.find(1).pos + assert_equal 2, ListMixin.find(3).pos + assert_equal 3, ListMixin.find(4).pos + end + +end + +class ListSubTest < Test::Unit::TestCase + + def setup + setup_db + (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 } + end + + def teardown + teardown_db + end + + def test_reordering + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(2).move_lower + assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(2).move_higher + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(1).move_to_bottom + assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(1).move_to_top + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(2).move_to_bottom + assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(4).move_to_top + assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + end + + def test_move_to_bottom_with_next_to_last_item + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + ListMixin.find(3).move_to_bottom + assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + end + + def test_next_prev + assert_equal ListMixin.find(2), ListMixin.find(1).lower_item + assert_nil ListMixin.find(1).higher_item + assert_equal ListMixin.find(3), ListMixin.find(4).higher_item + assert_nil ListMixin.find(4).lower_item + end + + def test_injection + item = ListMixin.new("parent_id"=>1) + assert_equal "parent_id = 1", item.scope_condition + assert_equal "pos", item.position_column + end + + def test_insert_at + new = ListMixin.create("parent_id" => 20) + assert_equal 1, new.pos + + new = ListMixinSub1.create("parent_id" => 20) + assert_equal 2, new.pos + + new = ListMixinSub2.create("parent_id" => 20) + assert_equal 3, new.pos + + new4 = ListMixin.create("parent_id" => 20) + assert_equal 4, new4.pos + + new4.insert_at(3) + assert_equal 3, new4.pos + + new.reload + assert_equal 4, new.pos + + new.insert_at(2) + assert_equal 2, new.pos + + new4.reload + assert_equal 4, new4.pos + + new5 = ListMixinSub1.create("parent_id" => 20) + assert_equal 5, new5.pos + + new5.insert_at(1) + assert_equal 1, new5.pos + + new4.reload + assert_equal 5, new4.pos + end + + def test_delete_middle + assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + ListMixin.find(2).destroy + + assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + assert_equal 1, ListMixin.find(1).pos + assert_equal 2, ListMixin.find(3).pos + assert_equal 3, ListMixin.find(4).pos + + ListMixin.find(1).destroy + + assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id) + + assert_equal 1, ListMixin.find(3).pos + assert_equal 2, ListMixin.find(4).pos + end + +end diff --git a/vendor/plugins/acts_as_tree/README b/vendor/plugins/acts_as_tree/README new file mode 100644 index 000000000..a6cc6a904 --- /dev/null +++ b/vendor/plugins/acts_as_tree/README @@ -0,0 +1,26 @@ +acts_as_tree +============ + +Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children +association. This requires that you have a foreign key column, which by default is called +parent_id+. + + class Category < ActiveRecord::Base + acts_as_tree :order => "name" + end + + Example: + root + \_ child1 + \_ subchild1 + \_ subchild2 + + root = Category.create("name" => "root") + child1 = root.children.create("name" => "child1") + subchild1 = child1.children.create("name" => "subchild1") + + root.parent # => nil + child1.parent # => root + root.children # => [child1] + root.children.first.children.first # => subchild1 + +Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license \ No newline at end of file diff --git a/vendor/plugins/acts_as_tree/Rakefile b/vendor/plugins/acts_as_tree/Rakefile new file mode 100644 index 000000000..da091d9dd --- /dev/null +++ b/vendor/plugins/acts_as_tree/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test acts_as_tree plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for acts_as_tree plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'acts_as_tree' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/acts_as_tree/init.rb b/vendor/plugins/acts_as_tree/init.rb new file mode 100644 index 000000000..0901ddb4a --- /dev/null +++ b/vendor/plugins/acts_as_tree/init.rb @@ -0,0 +1 @@ +ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree diff --git a/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb b/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb new file mode 100644 index 000000000..1f00e90a9 --- /dev/null +++ b/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module Acts + module Tree + def self.included(base) + base.extend(ClassMethods) + end + + # Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children + # association. This requires that you have a foreign key column, which by default is called +parent_id+. + # + # class Category < ActiveRecord::Base + # acts_as_tree :order => "name" + # end + # + # Example: + # root + # \_ child1 + # \_ subchild1 + # \_ subchild2 + # + # root = Category.create("name" => "root") + # child1 = root.children.create("name" => "child1") + # subchild1 = child1.children.create("name" => "subchild1") + # + # root.parent # => nil + # child1.parent # => root + # root.children # => [child1] + # root.children.first.children.first # => subchild1 + # + # In addition to the parent and children associations, the following instance methods are added to the class + # after calling acts_as_tree: + # * siblings - Returns all the children of the parent, excluding the current node ([subchild2] when called on subchild1) + # * self_and_siblings - Returns all the children of the parent, including the current node ([subchild1, subchild2] when called on subchild1) + # * ancestors - Returns all the ancestors of the current node ([child1, root] when called on subchild2) + # * root - Returns the root of the current node (root when called on subchild2) + module ClassMethods + # Configuration options are: + # + # * foreign_key - specifies the column name to use for tracking of the tree (default: +parent_id+) + # * order - makes it possible to sort the children according to this SQL snippet. + # * counter_cache - keeps a count in a +children_count+ column if set to +true+ (default: +false+). + def acts_as_tree(options = {}) + configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil } + configuration.update(options) if options.is_a?(Hash) + + belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache] + has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy + + class_eval <<-EOV + include ActiveRecord::Acts::Tree::InstanceMethods + + def self.roots + find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) + end + + def self.root + find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}}) + end + EOV + end + end + + module InstanceMethods + # Returns list of ancestors, starting from parent until root. + # + # subchild1.ancestors # => [child1, root] + def ancestors + node, nodes = self, [] + nodes << node = node.parent while node.parent + nodes + end + + # Returns the root node of the tree. + def root + node = self + node = node.parent while node.parent + node + end + + # Returns all siblings of the current node. + # + # subchild1.siblings # => [subchild2] + def siblings + self_and_siblings - [self] + end + + # Returns all siblings and a reference to the current node. + # + # subchild1.self_and_siblings # => [subchild1, subchild2] + def self_and_siblings + parent ? parent.children : self.class.roots + end + end + end + end +end diff --git a/vendor/plugins/acts_as_tree/test/abstract_unit.rb b/vendor/plugins/acts_as_tree/test/abstract_unit.rb new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb b/vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb new file mode 100644 index 000000000..018c58e1f --- /dev/null +++ b/vendor/plugins/acts_as_tree/test/acts_as_tree_test.rb @@ -0,0 +1,219 @@ +require 'test/unit' + +require 'rubygems' +require 'active_record' + +$:.unshift File.dirname(__FILE__) + '/../lib' +require File.dirname(__FILE__) + '/../init' + +class Test::Unit::TestCase + def assert_queries(num = 1) + $query_count = 0 + yield + ensure + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." + end + + def assert_no_queries(&block) + assert_queries(0, &block) + end +end + +ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") + +# AR keeps printing annoying schema statements +$stdout = StringIO.new + +def setup_db + ActiveRecord::Base.logger + ActiveRecord::Schema.define(:version => 1) do + create_table :mixins do |t| + t.column :type, :string + t.column :parent_id, :integer + end + end +end + +def teardown_db + ActiveRecord::Base.connection.tables.each do |table| + ActiveRecord::Base.connection.drop_table(table) + end +end + +class Mixin < ActiveRecord::Base +end + +class TreeMixin < Mixin + acts_as_tree :foreign_key => "parent_id", :order => "id" +end + +class TreeMixinWithoutOrder < Mixin + acts_as_tree :foreign_key => "parent_id" +end + +class RecursivelyCascadedTreeMixin < Mixin + acts_as_tree :foreign_key => "parent_id" + has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id +end + +class TreeTest < Test::Unit::TestCase + + def setup + setup_db + @root1 = TreeMixin.create! + @root_child1 = TreeMixin.create! :parent_id => @root1.id + @child1_child = TreeMixin.create! :parent_id => @root_child1.id + @root_child2 = TreeMixin.create! :parent_id => @root1.id + @root2 = TreeMixin.create! + @root3 = TreeMixin.create! + end + + def teardown + teardown_db + end + + def test_children + assert_equal @root1.children, [@root_child1, @root_child2] + assert_equal @root_child1.children, [@child1_child] + assert_equal @child1_child.children, [] + assert_equal @root_child2.children, [] + end + + def test_parent + assert_equal @root_child1.parent, @root1 + assert_equal @root_child1.parent, @root_child2.parent + assert_nil @root1.parent + end + + def test_delete + assert_equal 6, TreeMixin.count + @root1.destroy + assert_equal 2, TreeMixin.count + @root2.destroy + @root3.destroy + assert_equal 0, TreeMixin.count + end + + def test_insert + @extra = @root1.children.create + + assert @extra + + assert_equal @extra.parent, @root1 + + assert_equal 3, @root1.children.size + assert @root1.children.include?(@extra) + assert @root1.children.include?(@root_child1) + assert @root1.children.include?(@root_child2) + end + + def test_ancestors + assert_equal [], @root1.ancestors + assert_equal [@root1], @root_child1.ancestors + assert_equal [@root_child1, @root1], @child1_child.ancestors + assert_equal [@root1], @root_child2.ancestors + assert_equal [], @root2.ancestors + assert_equal [], @root3.ancestors + end + + def test_root + assert_equal @root1, TreeMixin.root + assert_equal @root1, @root1.root + assert_equal @root1, @root_child1.root + assert_equal @root1, @child1_child.root + assert_equal @root1, @root_child2.root + assert_equal @root2, @root2.root + assert_equal @root3, @root3.root + end + + def test_roots + assert_equal [@root1, @root2, @root3], TreeMixin.roots + end + + def test_siblings + assert_equal [@root2, @root3], @root1.siblings + assert_equal [@root_child2], @root_child1.siblings + assert_equal [], @child1_child.siblings + assert_equal [@root_child1], @root_child2.siblings + assert_equal [@root1, @root3], @root2.siblings + assert_equal [@root1, @root2], @root3.siblings + end + + def test_self_and_siblings + assert_equal [@root1, @root2, @root3], @root1.self_and_siblings + assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings + assert_equal [@child1_child], @child1_child.self_and_siblings + assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings + assert_equal [@root1, @root2, @root3], @root2.self_and_siblings + assert_equal [@root1, @root2, @root3], @root3.self_and_siblings + end +end + +class TreeTestWithEagerLoading < Test::Unit::TestCase + + def setup + teardown_db + setup_db + @root1 = TreeMixin.create! + @root_child1 = TreeMixin.create! :parent_id => @root1.id + @child1_child = TreeMixin.create! :parent_id => @root_child1.id + @root_child2 = TreeMixin.create! :parent_id => @root1.id + @root2 = TreeMixin.create! + @root3 = TreeMixin.create! + + @rc1 = RecursivelyCascadedTreeMixin.create! + @rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id + @rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id + @rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id + end + + def teardown + teardown_db + end + + def test_eager_association_loading + roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id") + assert_equal [@root1, @root2, @root3], roots + assert_no_queries do + assert_equal 2, roots[0].children.size + assert_equal 0, roots[1].children.size + assert_equal 0, roots[2].children.size + end + end + + def test_eager_association_loading_with_recursive_cascading_three_levels_has_many + root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :children => { :children => :children } }, :order => 'mixins.id') + assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first } + end + + def test_eager_association_loading_with_recursive_cascading_three_levels_has_one + root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :first_child => { :first_child => :first_child } }, :order => 'mixins.id') + assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child } + end + + def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to + leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC') + assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent } + end +end + +class TreeTestWithoutOrder < Test::Unit::TestCase + + def setup + setup_db + @root1 = TreeMixinWithoutOrder.create! + @root2 = TreeMixinWithoutOrder.create! + end + + def teardown + teardown_db + end + + def test_root + assert [@root1, @root2].include?(TreeMixinWithoutOrder.root) + end + + def test_roots + assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots + end +end diff --git a/vendor/plugins/acts_as_tree/test/database.yml b/vendor/plugins/acts_as_tree/test/database.yml new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/acts_as_tree/test/fixtures/mixin.rb b/vendor/plugins/acts_as_tree/test/fixtures/mixin.rb new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/acts_as_tree/test/fixtures/mixins.yml b/vendor/plugins/acts_as_tree/test/fixtures/mixins.yml new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/acts_as_tree/test/schema.rb b/vendor/plugins/acts_as_tree/test/schema.rb new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/plugins/classic_pagination/CHANGELOG b/vendor/plugins/classic_pagination/CHANGELOG new file mode 100644 index 000000000..d7d11f129 --- /dev/null +++ b/vendor/plugins/classic_pagination/CHANGELOG @@ -0,0 +1,152 @@ +* Exported the changelog of Pagination code for historical reference. + +* Imported some patches from Rails Trac (others closed as "wontfix"): + #8176, #7325, #7028, #4113. Documentation is much cleaner now and there + are some new unobtrusive features! + +* Extracted Pagination from Rails trunk (r6795) + +# +# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb +# +# Generated by Trac 0.10.3 +# 05/20/07 23:48:02 +# + +09/03/06 23:28:54 david [4953] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Docs and deprecation + +08/07/06 12:40:14 bitsweat [4715] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Deprecate direct usage of @params. Update ActionView::Base for + instance var deprecation. + +06/21/06 02:16:11 rick [4476] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Fix indent in pagination documentation. Closes #4990. [Kevin Clark] + +04/25/06 17:42:48 marcel [4268] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Remove all remaining references to @params in the documentation. + +03/16/06 06:38:08 rick [3899] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + trivial documentation patch for #pagination_links [Francois + Beausoleil] closes #4258 + +02/20/06 03:15:22 david [3620] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/test/activerecord/pagination_test.rb (modified) + * trunk/activerecord/CHANGELOG (modified) + * trunk/activerecord/lib/active_record/base.rb (modified) + * trunk/activerecord/test/base_test.rb (modified) + Added :count option to pagination that'll make it possible for the + ActiveRecord::Base.count call to using something else than * for the + count. Especially important for count queries using DISTINCT #3839 + [skaes]. Added :select option to Base.count that'll allow you to + select something else than * to be counted on. Especially important + for count queries using DISTINCT (closes #3839) [skaes]. + +02/09/06 09:17:40 nzkoz [3553] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/test/active_record_unit.rb (added) + * trunk/actionpack/test/activerecord (added) + * trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added) + * trunk/actionpack/test/activerecord/pagination_test.rb (added) + * trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted) + * trunk/actionpack/test/fixtures/companies.yml (added) + * trunk/actionpack/test/fixtures/company.rb (added) + * trunk/actionpack/test/fixtures/db_definitions (added) + * trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added) + * trunk/actionpack/test/fixtures/developer.rb (added) + * trunk/actionpack/test/fixtures/developers_projects.yml (added) + * trunk/actionpack/test/fixtures/developers.yml (added) + * trunk/actionpack/test/fixtures/project.rb (added) + * trunk/actionpack/test/fixtures/projects.yml (added) + * trunk/actionpack/test/fixtures/replies.yml (added) + * trunk/actionpack/test/fixtures/reply.rb (added) + * trunk/actionpack/test/fixtures/topic.rb (added) + * trunk/actionpack/test/fixtures/topics.yml (added) + * Fix pagination problems when using include + * Introduce Unit Tests for pagination + * Allow count to work with :include by using count distinct. + + [Kevin Clark & Jeremy Hopple] + +11/05/05 02:10:29 bitsweat [2878] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Update paginator docs. Closes #2744. + +10/16/05 15:42:03 minam [2649] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Update/clean up AP documentation (rdoc) + +08/31/05 00:13:10 ulysses [2078] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Add option to specify the singular name used by pagination. Closes + #1960 + +08/23/05 14:24:15 minam [2041] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + Add support for :include with pagination (subject to existing + constraints for :include with :limit and :offset) #1478 + [michael@schubert.cx] + +07/15/05 20:27:38 david [1839] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + More pagination speed #1334 [Stefan Kaes] + +07/14/05 08:02:01 david [1832] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + * trunk/actionpack/test/controller/addresses_render_test.rb (modified) + Made pagination faster #1334 [Stefan Kaes] + +04/13/05 05:40:22 david [1159] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/activerecord/lib/active_record/base.rb (modified) + Fixed pagination to work with joins #1034 [scott@sigkill.org] + +04/02/05 09:11:17 david [1067] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_controller/scaffolding.rb (modified) + * trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified) + * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified) + * trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified) + Added pagination for scaffolding (10 items per page) #964 + [mortonda@dgrmm.net] + +03/31/05 14:46:11 david [1048] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Improved the message display on the exception handler pages #963 + [Johan Sorensen] + +03/27/05 00:04:07 david [1017] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Fixed that pagination_helper would ignore :params #947 [Sebastian + Kanthak] + +03/22/05 13:09:44 david [976] + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Fixed documentation and prepared for 0.11.0 release + +03/21/05 14:35:36 david [967] + * trunk/actionpack/lib/action_controller/pagination.rb (modified) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified) + Tweaked the documentation + +03/20/05 23:12:05 david [949] + * trunk/actionpack/CHANGELOG (modified) + * trunk/actionpack/lib/action_controller.rb (modified) + * trunk/actionpack/lib/action_controller/pagination.rb (added) + * trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added) + * trunk/activesupport/lib/active_support/core_ext/kernel.rb (added) + Added pagination support through both a controller and helper add-on + #817 [Sam Stephenson] diff --git a/vendor/plugins/classic_pagination/README b/vendor/plugins/classic_pagination/README new file mode 100644 index 000000000..e94904974 --- /dev/null +++ b/vendor/plugins/classic_pagination/README @@ -0,0 +1,18 @@ +Pagination +========== + +To install: + + script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination + +This code was extracted from Rails trunk after the release 1.2.3. +WARNING: this code is dead. It is unmaintained, untested and full of cruft. + +There is a much better pagination plugin called will_paginate. +Install it like this and glance through the README: + + script/plugin install svn://errtheblog.com/svn/plugins/will_paginate + +It doesn't have the same API, but is in fact much nicer. You can +have both plugins installed until you change your controller/view code that +handles pagination. Then, simply uninstall classic_pagination. diff --git a/vendor/plugins/classic_pagination/Rakefile b/vendor/plugins/classic_pagination/Rakefile new file mode 100644 index 000000000..c7e374b56 --- /dev/null +++ b/vendor/plugins/classic_pagination/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the classic_pagination plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the classic_pagination plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Pagination' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/classic_pagination/init.rb b/vendor/plugins/classic_pagination/init.rb new file mode 100644 index 000000000..25e552f2a --- /dev/null +++ b/vendor/plugins/classic_pagination/init.rb @@ -0,0 +1,33 @@ +#-- +# Copyright (c) 2004-2006 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'pagination' +require 'pagination_helper' + +ActionController::Base.class_eval do + include ActionController::Pagination +end + +ActionView::Base.class_eval do + include ActionView::Helpers::PaginationHelper +end diff --git a/vendor/plugins/classic_pagination/install.rb b/vendor/plugins/classic_pagination/install.rb new file mode 100644 index 000000000..adf746f8b --- /dev/null +++ b/vendor/plugins/classic_pagination/install.rb @@ -0,0 +1 @@ +puts "\n\n" + File.read(File.dirname(__FILE__) + '/README') diff --git a/vendor/plugins/classic_pagination/lib/pagination.rb b/vendor/plugins/classic_pagination/lib/pagination.rb new file mode 100644 index 000000000..b6e9cf4bc --- /dev/null +++ b/vendor/plugins/classic_pagination/lib/pagination.rb @@ -0,0 +1,405 @@ +module ActionController + # === Action Pack pagination for Active Record collections + # + # The Pagination module aids in the process of paging large collections of + # Active Record objects. It offers macro-style automatic fetching of your + # model for multiple views, or explicit fetching for single actions. And if + # the magic isn't flexible enough for your needs, you can create your own + # paginators with a minimal amount of code. + # + # The Pagination module can handle as much or as little as you wish. In the + # controller, have it automatically query your model for pagination; or, + # if you prefer, create Paginator objects yourself. + # + # Pagination is included automatically for all controllers. + # + # For help rendering pagination links, see + # ActionView::Helpers::PaginationHelper. + # + # ==== Automatic pagination for every action in a controller + # + # class PersonController < ApplicationController + # model :person + # + # paginate :people, :order => 'last_name, first_name', + # :per_page => 20 + # + # # ... + # end + # + # Each action in this controller now has access to a @people + # instance variable, which is an ordered collection of model objects for the + # current page (at most 20, sorted by last name and first name), and a + # @person_pages Paginator instance. The current page is determined + # by the params[:page] variable. + # + # ==== Pagination for a single action + # + # def list + # @person_pages, @people = + # paginate :people, :order => 'last_name, first_name' + # end + # + # Like the previous example, but explicitly creates @person_pages + # and @people for a single action, and uses the default of 10 items + # per page. + # + # ==== Custom/"classic" pagination + # + # def list + # @person_pages = Paginator.new self, Person.count, 10, params[:page] + # @people = Person.find :all, :order => 'last_name, first_name', + # :limit => @person_pages.items_per_page, + # :offset => @person_pages.current.offset + # end + # + # Explicitly creates the paginator from the previous example and uses + # Paginator#to_sql to retrieve @people from the model. + # + module Pagination + unless const_defined?(:OPTIONS) + # A hash holding options for controllers using macro-style pagination + OPTIONS = Hash.new + + # The default options for pagination + DEFAULT_OPTIONS = { + :class_name => nil, + :singular_name => nil, + :per_page => 10, + :conditions => nil, + :order_by => nil, + :order => nil, + :join => nil, + :joins => nil, + :count => nil, + :include => nil, + :select => nil, + :group => nil, + :parameter => 'page' + } + else + DEFAULT_OPTIONS[:group] = nil + end + + def self.included(base) #:nodoc: + super + base.extend(ClassMethods) + end + + def self.validate_options!(collection_id, options, in_action) #:nodoc: + options.merge!(DEFAULT_OPTIONS) {|key, old, new| old} + + valid_options = DEFAULT_OPTIONS.keys + valid_options << :actions unless in_action + + unknown_option_keys = options.keys - valid_options + raise ActionController::ActionControllerError, + "Unknown options: #{unknown_option_keys.join(', ')}" unless + unknown_option_keys.empty? + + options[:singular_name] ||= Inflector.singularize(collection_id.to_s) + options[:class_name] ||= Inflector.camelize(options[:singular_name]) + end + + # Returns a paginator and a collection of Active Record model instances + # for the paginator's current page. This is designed to be used in a + # single action; to automatically paginate multiple actions, consider + # ClassMethods#paginate. + # + # +options+ are: + # :singular_name:: the singular name to use, if it can't be inferred by singularizing the collection name + # :class_name:: the class name to use, if it can't be inferred by + # camelizing the singular name + # :per_page:: the maximum number of items to include in a + # single page. Defaults to 10 + # :conditions:: optional conditions passed to Model.find(:all, *params) and + # Model.count + # :order:: optional order parameter passed to Model.find(:all, *params) + # :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) + # :joins:: optional joins parameter passed to Model.find(:all, *params) + # and Model.count + # :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) + # and Model.count + # :include:: optional eager loading parameter passed to Model.find(:all, *params) + # and Model.count + # :select:: :select parameter passed to Model.find(:all, *params) + # + # :count:: parameter passed as :select option to Model.count(*params) + # + # :group:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records + # + def paginate(collection_id, options={}) + Pagination.validate_options!(collection_id, options, true) + paginator_and_collection_for(collection_id, options) + end + + # These methods become class methods on any controller + module ClassMethods + # Creates a +before_filter+ which automatically paginates an Active + # Record model for all actions in a controller (or certain actions if + # specified with the :actions option). + # + # +options+ are the same as PaginationHelper#paginate, with the addition + # of: + # :actions:: an array of actions for which the pagination is + # active. Defaults to +nil+ (i.e., every action) + def paginate(collection_id, options={}) + Pagination.validate_options!(collection_id, options, false) + module_eval do + before_filter :create_paginators_and_retrieve_collections + OPTIONS[self] ||= Hash.new + OPTIONS[self][collection_id] = options + end + end + end + + def create_paginators_and_retrieve_collections #:nodoc: + Pagination::OPTIONS[self.class].each do |collection_id, options| + next unless options[:actions].include? action_name if + options[:actions] + + paginator, collection = + paginator_and_collection_for(collection_id, options) + + paginator_name = "@#{options[:singular_name]}_pages" + self.instance_variable_set(paginator_name, paginator) + + collection_name = "@#{collection_id.to_s}" + self.instance_variable_set(collection_name, collection) + end + end + + # Returns the total number of items in the collection to be paginated for + # the +model+ and given +conditions+. Override this method to implement a + # custom counter. + def count_collection_for_pagination(model, options) + model.count(:conditions => options[:conditions], + :joins => options[:join] || options[:joins], + :include => options[:include], + :select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count])) + end + + # Returns a collection of items for the given +model+ and +options[conditions]+, + # ordered by +options[order]+, for the current page in the given +paginator+. + # Override this method to implement a custom finder. + def find_collection_for_pagination(model, options, paginator) + model.find(:all, :conditions => options[:conditions], + :order => options[:order_by] || options[:order], + :joins => options[:join] || options[:joins], :include => options[:include], + :select => options[:select], :limit => options[:per_page], + :group => options[:group], :offset => paginator.current.offset) + end + + protected :create_paginators_and_retrieve_collections, + :count_collection_for_pagination, + :find_collection_for_pagination + + def paginator_and_collection_for(collection_id, options) #:nodoc: + klass = options[:class_name].constantize + page = params[options[:parameter]] + count = count_collection_for_pagination(klass, options) + paginator = Paginator.new(self, count, options[:per_page], page) + collection = find_collection_for_pagination(klass, options, paginator) + + return paginator, collection + end + + private :paginator_and_collection_for + + # A class representing a paginator for an Active Record collection. + class Paginator + include Enumerable + + # Creates a new Paginator on the given +controller+ for a set of items + # of size +item_count+ and having +items_per_page+ items per page. + # Raises ArgumentError if items_per_page is out of bounds (i.e., less + # than or equal to zero). The page CGI parameter for links defaults to + # "page" and can be overridden with +page_parameter+. + def initialize(controller, item_count, items_per_page, current_page=1) + raise ArgumentError, 'must have at least one item per page' if + items_per_page <= 0 + + @controller = controller + @item_count = item_count || 0 + @items_per_page = items_per_page + @pages = {} + + self.current_page = current_page + end + attr_reader :controller, :item_count, :items_per_page + + # Sets the current page number of this paginator. If +page+ is a Page + # object, its +number+ attribute is used as the value; if the page does + # not belong to this Paginator, an ArgumentError is raised. + def current_page=(page) + if page.is_a? Page + raise ArgumentError, 'Page/Paginator mismatch' unless + page.paginator == self + end + page = page.to_i + @current_page_number = has_page_number?(page) ? page : 1 + end + + # Returns a Page object representing this paginator's current page. + def current_page + @current_page ||= self[@current_page_number] + end + alias current :current_page + + # Returns a new Page representing the first page in this paginator. + def first_page + @first_page ||= self[1] + end + alias first :first_page + + # Returns a new Page representing the last page in this paginator. + def last_page + @last_page ||= self[page_count] + end + alias last :last_page + + # Returns the number of pages in this paginator. + def page_count + @page_count ||= @item_count.zero? ? 1 : + (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1) + end + + alias length :page_count + + # Returns true if this paginator contains the page of index +number+. + def has_page_number?(number) + number >= 1 and number <= page_count + end + + # Returns a new Page representing the page with the given index + # +number+. + def [](number) + @pages[number] ||= Page.new(self, number) + end + + # Successively yields all the paginator's pages to the given block. + def each(&block) + page_count.times do |n| + yield self[n+1] + end + end + + # A class representing a single page in a paginator. + class Page + include Comparable + + # Creates a new Page for the given +paginator+ with the index + # +number+. If +number+ is not in the range of valid page numbers or + # is not a number at all, it defaults to 1. + def initialize(paginator, number) + @paginator = paginator + @number = number.to_i + @number = 1 unless @paginator.has_page_number? @number + end + attr_reader :paginator, :number + alias to_i :number + + # Compares two Page objects and returns true when they represent the + # same page (i.e., their paginators are the same and they have the + # same page number). + def ==(page) + return false if page.nil? + @paginator == page.paginator and + @number == page.number + end + + # Compares two Page objects and returns -1 if the left-hand page comes + # before the right-hand page, 0 if the pages are equal, and 1 if the + # left-hand page comes after the right-hand page. Raises ArgumentError + # if the pages do not belong to the same Paginator object. + def <=>(page) + raise ArgumentError unless @paginator == page.paginator + @number <=> page.number + end + + # Returns the item offset for the first item in this page. + def offset + @paginator.items_per_page * (@number - 1) + end + + # Returns the number of the first item displayed. + def first_item + offset + 1 + end + + # Returns the number of the last item displayed. + def last_item + [@paginator.items_per_page * @number, @paginator.item_count].min + end + + # Returns true if this page is the first page in the paginator. + def first? + self == @paginator.first + end + + # Returns true if this page is the last page in the paginator. + def last? + self == @paginator.last + end + + # Returns a new Page object representing the page just before this + # page, or nil if this is the first page. + def previous + if first? then nil else @paginator[@number - 1] end + end + + # Returns a new Page object representing the page just after this + # page, or nil if this is the last page. + def next + if last? then nil else @paginator[@number + 1] end + end + + # Returns a new Window object for this page with the specified + # +padding+. + def window(padding=2) + Window.new(self, padding) + end + + # Returns the limit/offset array for this page. + def to_sql + [@paginator.items_per_page, offset] + end + + def to_param #:nodoc: + @number.to_s + end + end + + # A class for representing ranges around a given page. + class Window + # Creates a new Window object for the given +page+ with the specified + # +padding+. + def initialize(page, padding=2) + @paginator = page.paginator + @page = page + self.padding = padding + end + attr_reader :paginator, :page + + # Sets the window's padding (the number of pages on either side of the + # window page). + def padding=(padding) + @padding = padding < 0 ? 0 : padding + # Find the beginning and end pages of the window + @first = @paginator.has_page_number?(@page.number - @padding) ? + @paginator[@page.number - @padding] : @paginator.first + @last = @paginator.has_page_number?(@page.number + @padding) ? + @paginator[@page.number + @padding] : @paginator.last + end + attr_reader :padding, :first, :last + + # Returns an array of Page objects in the current window. + def pages + (@first.number..@last.number).to_a.collect! {|n| @paginator[n]} + end + alias to_a :pages + end + end + + end +end diff --git a/vendor/plugins/classic_pagination/lib/pagination_helper.rb b/vendor/plugins/classic_pagination/lib/pagination_helper.rb new file mode 100644 index 000000000..069d77566 --- /dev/null +++ b/vendor/plugins/classic_pagination/lib/pagination_helper.rb @@ -0,0 +1,135 @@ +module ActionView + module Helpers + # Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally + # also build your links manually using ActionView::Helpers::AssetHelper#link_to like so: + # + # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %> + # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %> + module PaginationHelper + unless const_defined?(:DEFAULT_OPTIONS) + DEFAULT_OPTIONS = { + :name => :page, + :window_size => 2, + :always_show_anchors => true, + :link_to_current_page => false, + :params => {} + } + end + + # Creates a basic HTML link bar for the given +paginator+. Links will be created + # for the next and/or previous page and for a number of other pages around the current + # pages position. The +html_options+ hash is passed to +link_to+ when the links are created. + # + # ==== Options + # :name:: the routing name for this paginator + # (defaults to +page+) + # :prefix:: prefix for pagination links + # (i.e. Older Pages: 1 2 3 4) + # :suffix:: suffix for pagination links + # (i.e. 1 2 3 4 <- Older Pages) + # :window_size:: the number of pages to show around + # the current page (defaults to 2) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # :params:: any additional routing parameters + # for page URLs + # + # ==== Examples + # # We'll assume we have a paginator setup in @person_pages... + # + # pagination_links(@person_pages) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :link_to_current_page => true) + # # => 1 2 3 ... 10 + # + # pagination_links(@person_pages, :always_show_anchors => false) + # # => 1 2 3 + # + # pagination_links(@person_pages, :window_size => 1) + # # => 1 2 ... 10 + # + # pagination_links(@person_pages, :params => { :viewer => "flash" }) + # # => 1 2 3 ... + # # 10 + def pagination_links(paginator, options={}, html_options={}) + name = options[:name] || DEFAULT_OPTIONS[:name] + params = (options[:params] || DEFAULT_OPTIONS[:params]).clone + + prefix = options[:prefix] || '' + suffix = options[:suffix] || '' + + pagination_links_each(paginator, options, prefix, suffix) do |n| + params[name] = n + link_to(n.to_s, params, html_options) + end + end + + # Iterate through the pages of a given +paginator+, invoking a + # block for each page number that needs to be rendered as a link. + # + # ==== Options + # :window_size:: the number of pages to show around + # the current page (defaults to +2+) + # :always_show_anchors:: whether or not the first and last + # pages should always be shown + # (defaults to +true+) + # :link_to_current_page:: whether or not the current page + # should be linked to (defaults to + # +false+) + # + # ==== Example + # # Turn paginated links into an Ajax call + # pagination_links_each(paginator, page_options) do |link| + # options = { :url => {:action => 'list'}, :update => 'results' } + # html_options = { :href => url_for(:action => 'list') } + # + # link_to_remote(link.to_s, options, html_options) + # end + def pagination_links_each(paginator, options, prefix = nil, suffix = nil) + options = DEFAULT_OPTIONS.merge(options) + link_to_current_page = options[:link_to_current_page] + always_show_anchors = options[:always_show_anchors] + + current_page = paginator.current_page + window_pages = current_page.window(options[:window_size]).pages + return if window_pages.length <= 1 unless link_to_current_page + + first, last = paginator.first, paginator.last + + html = '' + + html << prefix if prefix + + if always_show_anchors and not (wp_first = window_pages[0]).first? + html << yield(first.number) + html << ' ... ' if wp_first.number - first.number > 1 + html << ' ' + end + + window_pages.each do |page| + if current_page == page && !link_to_current_page + html << page.number.to_s + else + html << yield(page.number) + end + html << ' ' + end + + if always_show_anchors and not (wp_last = window_pages[-1]).last? + html << ' ... ' if last.number - wp_last.number > 1 + html << yield(last.number) + end + + html << suffix if suffix + + html + end + + end # PaginationHelper + end # Helpers +end # ActionView diff --git a/vendor/plugins/classic_pagination/test/fixtures/companies.yml b/vendor/plugins/classic_pagination/test/fixtures/companies.yml new file mode 100644 index 000000000..707f72abc --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/companies.yml @@ -0,0 +1,24 @@ +thirty_seven_signals: + id: 1 + name: 37Signals + rating: 4 + +TextDrive: + id: 2 + name: TextDrive + rating: 4 + +PlanetArgon: + id: 3 + name: Planet Argon + rating: 4 + +Google: + id: 4 + name: Google + rating: 4 + +Ionist: + id: 5 + name: Ioni.st + rating: 4 \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/company.rb b/vendor/plugins/classic_pagination/test/fixtures/company.rb new file mode 100644 index 000000000..0d1c29b90 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/company.rb @@ -0,0 +1,9 @@ +class Company < ActiveRecord::Base + attr_protected :rating + set_sequence_name :companies_nonstd_seq + + validates_presence_of :name + def validate + errors.add('rating', 'rating should not be 2') if rating == 2 + end +end \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/developer.rb b/vendor/plugins/classic_pagination/test/fixtures/developer.rb new file mode 100644 index 000000000..f5e5b901f --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/developer.rb @@ -0,0 +1,7 @@ +class Developer < ActiveRecord::Base + has_and_belongs_to_many :projects +end + +class DeVeLoPeR < ActiveRecord::Base + set_table_name "developers" +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/developers.yml b/vendor/plugins/classic_pagination/test/fixtures/developers.yml new file mode 100644 index 000000000..308bf75de --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/developers.yml @@ -0,0 +1,21 @@ +david: + id: 1 + name: David + salary: 80000 + +jamis: + id: 2 + name: Jamis + salary: 150000 + +<% for digit in 3..10 %> +dev_<%= digit %>: + id: <%= digit %> + name: fixture_<%= digit %> + salary: 100000 +<% end %> + +poor_jamis: + id: 11 + name: Jamis + salary: 9000 \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml b/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml new file mode 100644 index 000000000..cee359c7c --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/developers_projects.yml @@ -0,0 +1,13 @@ +david_action_controller: + developer_id: 1 + project_id: 2 + joined_on: 2004-10-10 + +david_active_record: + developer_id: 1 + project_id: 1 + joined_on: 2004-10-10 + +jamis_active_record: + developer_id: 2 + project_id: 1 \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/project.rb b/vendor/plugins/classic_pagination/test/fixtures/project.rb new file mode 100644 index 000000000..2b53d39ed --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + has_and_belongs_to_many :developers, :uniq => true +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/projects.yml b/vendor/plugins/classic_pagination/test/fixtures/projects.yml new file mode 100644 index 000000000..02800c782 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/projects.yml @@ -0,0 +1,7 @@ +action_controller: + id: 2 + name: Active Controller + +active_record: + id: 1 + name: Active Record diff --git a/vendor/plugins/classic_pagination/test/fixtures/replies.yml b/vendor/plugins/classic_pagination/test/fixtures/replies.yml new file mode 100644 index 000000000..284c9c079 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/replies.yml @@ -0,0 +1,13 @@ +witty_retort: + id: 1 + topic_id: 1 + content: Birdman is better! + created_at: <%= 6.hours.ago.to_s(:db) %> + updated_at: nil + +another: + id: 2 + topic_id: 2 + content: Nuh uh! + created_at: <%= 1.hour.ago.to_s(:db) %> + updated_at: nil \ No newline at end of file diff --git a/vendor/plugins/classic_pagination/test/fixtures/reply.rb b/vendor/plugins/classic_pagination/test/fixtures/reply.rb new file mode 100644 index 000000000..ea84042b9 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/reply.rb @@ -0,0 +1,5 @@ +class Reply < ActiveRecord::Base + belongs_to :topic, :include => [:replies] + + validates_presence_of :content +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/schema.sql b/vendor/plugins/classic_pagination/test/fixtures/schema.sql new file mode 100644 index 000000000..b4e7539d1 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/schema.sql @@ -0,0 +1,42 @@ +CREATE TABLE 'companies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'rating' INTEGER DEFAULT 1 +); + +CREATE TABLE 'replies' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'content' text, + 'created_at' datetime, + 'updated_at' datetime, + 'topic_id' integer +); + +CREATE TABLE 'topics' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'title' varchar(255), + 'subtitle' varchar(255), + 'content' text, + 'created_at' datetime, + 'updated_at' datetime +); + +CREATE TABLE 'developers' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL, + 'salary' INTEGER DEFAULT 70000, + 'created_at' DATETIME DEFAULT NULL, + 'updated_at' DATETIME DEFAULT NULL +); + +CREATE TABLE 'projects' ( + 'id' INTEGER PRIMARY KEY NOT NULL, + 'name' TEXT DEFAULT NULL +); + +CREATE TABLE 'developers_projects' ( + 'developer_id' INTEGER NOT NULL, + 'project_id' INTEGER NOT NULL, + 'joined_on' DATE DEFAULT NULL, + 'access_level' INTEGER DEFAULT 1 +); diff --git a/vendor/plugins/classic_pagination/test/fixtures/topic.rb b/vendor/plugins/classic_pagination/test/fixtures/topic.rb new file mode 100644 index 000000000..0beeecf28 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/topic.rb @@ -0,0 +1,3 @@ +class Topic < ActiveRecord::Base + has_many :replies, :include => [:user], :dependent => :destroy +end diff --git a/vendor/plugins/classic_pagination/test/fixtures/topics.yml b/vendor/plugins/classic_pagination/test/fixtures/topics.yml new file mode 100644 index 000000000..61ea02d76 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/fixtures/topics.yml @@ -0,0 +1,22 @@ +futurama: + id: 1 + title: Isnt futurama awesome? + subtitle: It really is, isnt it. + content: I like futurama + created_at: <%= 1.day.ago.to_s(:db) %> + updated_at: + +harvey_birdman: + id: 2 + title: Harvey Birdman is the king of all men + subtitle: yup + content: It really is + created_at: <%= 2.hours.ago.to_s(:db) %> + updated_at: + +rails: + id: 3 + title: Rails is nice + subtitle: It makes me happy + content: except when I have to hack internals to fix pagination. even then really. + created_at: <%= 20.minutes.ago.to_s(:db) %> diff --git a/vendor/plugins/classic_pagination/test/helper.rb b/vendor/plugins/classic_pagination/test/helper.rb new file mode 100644 index 000000000..3f76d5a76 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/helper.rb @@ -0,0 +1,117 @@ +require 'test/unit' + +unless defined?(ActiveRecord) + plugin_root = File.join(File.dirname(__FILE__), '..') + + # first look for a symlink to a copy of the framework + if framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p } + puts "found framework root: #{framework_root}" + # this allows for a plugin to be tested outside an app + $:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib" + else + # is the plugin installed in an application? + app_root = plugin_root + '/../../..' + + if File.directory? app_root + '/config' + puts 'using config/boot.rb' + ENV['RAILS_ENV'] = 'test' + require File.expand_path(app_root + '/config/boot') + else + # simply use installed gems if available + puts 'using rubygems' + require 'rubygems' + gem 'actionpack'; gem 'activerecord' + end + end + + %w(action_pack active_record action_controller active_record/fixtures action_controller/test_process).each {|f| require f} + + Dependencies.load_paths.unshift "#{plugin_root}/lib" +end + +# Define the connector +class ActiveRecordTestConnector + cattr_accessor :able_to_connect + cattr_accessor :connected + + # Set our defaults + self.connected = false + self.able_to_connect = true + + class << self + def setup + unless self.connected || !self.able_to_connect + setup_connection + load_schema + require_fixture_models + self.connected = true + end + rescue Exception => e # errors from ActiveRecord setup + $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" + #$stderr.puts " #{e.backtrace.join("\n ")}\n" + self.able_to_connect = false + end + + private + + def setup_connection + if Object.const_defined?(:ActiveRecord) + defaults = { :database => ':memory:' } + begin + options = defaults.merge :adapter => 'sqlite3', :timeout => 500 + ActiveRecord::Base.establish_connection(options) + ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options } + ActiveRecord::Base.connection + rescue Exception # errors from establishing a connection + $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.' + options = defaults.merge :adapter => 'sqlite' + ActiveRecord::Base.establish_connection(options) + ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options } + ActiveRecord::Base.connection + end + + Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) + else + raise "Can't setup connection since ActiveRecord isn't loaded." + end + end + + # Load actionpack sqlite tables + def load_schema + File.read(File.dirname(__FILE__) + "/fixtures/schema.sql").split(';').each do |sql| + ActiveRecord::Base.connection.execute(sql) unless sql.blank? + end + end + + def require_fixture_models + Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f} + end + end +end + +# Test case for inheritance +class ActiveRecordTestCase < Test::Unit::TestCase + # Set our fixture path + if ActiveRecordTestConnector.able_to_connect + self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" + self.use_transactional_fixtures = false + end + + def self.fixtures(*args) + super if ActiveRecordTestConnector.connected + end + + def run(*args) + super if ActiveRecordTestConnector.connected + end + + # Default so Test::Unit::TestCase doesn't complain + def test_truth + end +end + +ActiveRecordTestConnector.setup +ActionController::Routing::Routes.reload rescue nil +ActionController::Routing::Routes.draw do |map| + map.connect ':controller/:action/:id' +end diff --git a/vendor/plugins/classic_pagination/test/pagination_helper_test.rb b/vendor/plugins/classic_pagination/test/pagination_helper_test.rb new file mode 100644 index 000000000..d8394a793 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/pagination_helper_test.rb @@ -0,0 +1,38 @@ +require File.dirname(__FILE__) + '/helper' +require File.dirname(__FILE__) + '/../init' + +class PaginationHelperTest < Test::Unit::TestCase + include ActionController::Pagination + include ActionView::Helpers::PaginationHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + + def setup + @controller = Class.new do + attr_accessor :url, :request + def url_for(options, *parameters_for_method_reference) + url + end + end + @controller = @controller.new + @controller.url = "http://www.example.com" + end + + def test_pagination_links + total, per_page, page = 30, 10, 1 + output = pagination_links Paginator.new(@controller, total, per_page, page) + assert_equal "1 2 3 ", output + end + + def test_pagination_links_with_prefix + total, per_page, page = 30, 10, 1 + output = pagination_links Paginator.new(@controller, total, per_page, page), :prefix => 'Newer ' + assert_equal "Newer 1 2 3 ", output + end + + def test_pagination_links_with_suffix + total, per_page, page = 30, 10, 1 + output = pagination_links Paginator.new(@controller, total, per_page, page), :suffix => 'Older' + assert_equal "1 2 3 Older", output + end +end diff --git a/vendor/plugins/classic_pagination/test/pagination_test.rb b/vendor/plugins/classic_pagination/test/pagination_test.rb new file mode 100644 index 000000000..16a6f1d84 --- /dev/null +++ b/vendor/plugins/classic_pagination/test/pagination_test.rb @@ -0,0 +1,177 @@ +require File.dirname(__FILE__) + '/helper' +require File.dirname(__FILE__) + '/../init' + +class PaginationTest < ActiveRecordTestCase + fixtures :topics, :replies, :developers, :projects, :developers_projects + + class PaginationController < ActionController::Base + if respond_to? :view_paths= + self.view_paths = [ "#{File.dirname(__FILE__)}/../fixtures/" ] + else + self.template_root = [ "#{File.dirname(__FILE__)}/../fixtures/" ] + end + + def simple_paginate + @topic_pages, @topics = paginate(:topics) + render :nothing => true + end + + def paginate_with_per_page + @topic_pages, @topics = paginate(:topics, :per_page => 1) + render :nothing => true + end + + def paginate_with_order + @topic_pages, @topics = paginate(:topics, :order => 'created_at asc') + render :nothing => true + end + + def paginate_with_order_by + @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc') + render :nothing => true + end + + def paginate_with_include_and_order + @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc') + render :nothing => true + end + + def paginate_with_conditions + @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago]) + render :nothing => true + end + + def paginate_with_class_name + @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR") + render :nothing => true + end + + def paginate_with_singular_name + @developer_pages, @developers = paginate() + render :nothing => true + end + + def paginate_with_joins + @developer_pages, @developers = paginate(:developers, + :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join + @developer_pages, @developers = paginate(:developers, + :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', + :conditions => 'project_id=1') + render :nothing => true + end + + def paginate_with_join_and_count + @developer_pages, @developers = paginate(:developers, + :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id', + :conditions => 'project_id=1', + :count => "d.id") + render :nothing => true + end + + def paginate_with_join_and_group + @developer_pages, @developers = paginate(:developers, + :join => 'INNER JOIN developers_projects ON developers.id = developers_projects.developer_id', + :group => 'developers.id') + render :nothing => true + end + + def rescue_errors(e) raise e end + + def rescue_action(e) raise end + + end + + def setup + @controller = PaginationController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + super + end + + # Single Action Pagination Tests + + def test_simple_paginate + get :simple_paginate + assert_equal 1, assigns(:topic_pages).page_count + assert_equal 3, assigns(:topics).size + end + + def test_paginate_with_per_page + get :paginate_with_per_page + assert_equal 1, assigns(:topics).size + assert_equal 3, assigns(:topic_pages).page_count + end + + def test_paginate_with_order + get :paginate_with_order + expected = [topics(:futurama), + topics(:harvey_birdman), + topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_order_by + get :paginate_with_order + expected = assigns(:topics) + get :paginate_with_order_by + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_conditions + get :paginate_with_conditions + expected = [topics(:rails)] + assert_equal expected, assigns(:topics) + assert_equal 1, assigns(:topic_pages).page_count + end + + def test_paginate_with_class_name + get :paginate_with_class_name + + assert assigns(:developers).size > 0 + assert_equal DeVeLoPeR, assigns(:developers).first.class + end + + def test_paginate_with_joins + get :paginate_with_joins + assert_equal 2, assigns(:developers).size + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end + + def test_paginate_with_join_and_conditions + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_join_and_count + get :paginate_with_joins + expected = assigns(:developers) + get :paginate_with_join_and_count + assert_equal expected, assigns(:developers) + end + + def test_paginate_with_include_and_order + get :paginate_with_include_and_order + expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10) + assert_equal expected, assigns(:topics) + end + + def test_paginate_with_join_and_group + get :paginate_with_join_and_group + assert_equal 2, assigns(:developers).size + assert_equal 2, assigns(:developer_pages).item_count + developer_names = assigns(:developers).map { |d| d.name } + assert developer_names.include?('David') + assert developer_names.include?('Jamis') + end +end From c00b8fd12e12820b827721e36984f1172362ebd9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 10 Dec 2007 18:39:57 +0000 Subject: [PATCH 054/710] Doc and version changes for 0.6.1 release. git-svn-id: http://redmine.rubyforge.org/svn/trunk@976 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/CHANGELOG | 59 ++++++++++++++++++++++++++++++++++++++++++ doc/INSTALL | 2 +- doc/UPGRADING | 1 - lib/redmine/version.rb | 2 +- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 1d7e0f6a0..f5614f6f9 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -5,6 +5,65 @@ Copyright (C) 2006-2007 Jean-Philippe Lang http://www.redmine.org/ +== 2007-12-10 v0.6.1 + +* Rails 2.0 compatibility +* Custom fields can now be displayed as columns on the issue list +* Added version details view (accessible from the roadmap) +* Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account) +* Added per-project tracker selection. Trackers can be selected on project settings +* Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums +* Forums: messages can now be edited/deleted (explicit permissions need to be given) +* Forums: topics can be locked so that no reply can be added +* Forums: topics can be marked as sticky so that they always appear at the top of the list +* Forums: attachments can now be added to replies +* Added time zone support +* Added a setting to choose the account activation strategy (available in application settings) +* Added 'Classic' theme (inspired from the v0.51 design) +* Added an alternate theme which provides issue list colorization based on issues priority +* Added Bazaar SCM adapter +* Added Annotate/Blame view in the repository browser (except for Darcs SCM) +* Diff style (inline or side by side) automatically saved as a user preference +* Added issues status changes on the activity view (by Cyril Mougel) +* Added forums topics on the activity view (disabled by default) +* Added an option on 'My account' for users who dont want to be notified of changes that they make +* Trac importer now supports mysql and postgresql databases +* Trac importer improvements (by Mat Trudel) +* 'fixed version' field can now be displayed on the issue list +* Added a couple of new formats for the 'date format' setting +* Added Traditional Chinese translation (by Shortie Lo) +* Added Russian translation (iGor kMeta) +* Project name format limitation removed (name can now contain any character) +* Project identifier maximum length changed from 12 to 20 +* Changed the maximum length of LDAP account to 255 characters +* Removed the 12 characters limit on passwords +* Added wiki macros support +* Performance improvement on workflow setup screen +* More detailed html title on several views +* Custom fields can now be reordered +* Search engine: search can be restricted to an exact phrase by using quotation marks +* Added custom fields marked as 'For all projects' to the csv export of the cross project issue list +* Email notifications are now sent as Blind carbon copy by default +* Fixed: all members (including non active) should be deleted when deleting a project +* Fixed: Error on wiki syntax link (accessible from wiki/edit) +* Fixed: 'quick jump to a revision' form on the revisions list +* Fixed: error on admin/info if there's more than 1 plugin installed +* Fixed: svn or ldap password can be found in clear text in the html source in editing mode +* Fixed: 'Assigned to' drop down list is not sorted +* Fixed: 'View all issues' link doesn't work on issues/show +* Fixed: error on account/register when validation fails +* Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter' +* Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt) +* Fixed: Wrong feed URLs on the home page +* Fixed: Update of time entry fails when the issue has been moved to an other project +* Fixed: Error when moving an issue without changing its tracker (Postgresql) +* Fixed: Changes not recorded when using :pserver string (CVS adapter) +* Fixed: admin should be able to move issues to any project +* Fixed: adding an attachment is not possible when changing the status of an issue +* Fixed: No mime-types in documents/files downloading +* Fixed: error when sorting the messages if theres only one board for the project +* Fixed: 'me' doesn't appear in the drop down filters on a project issue list. + == 2007-11-04 v0.6.0 * Permission model refactoring. diff --git a/doc/INSTALL b/doc/INSTALL index 4da5daef9..5eb4d5c68 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -7,7 +7,7 @@ http://www.redmine.org/ == Requirements -* Ruby on Rails 1.2.2 or higher (this release won't work with Rails 2.0) +* Ruby on Rails 1.2.5 or 2.0.1 * A database (see compatibility below) Optional: diff --git a/doc/UPGRADING b/doc/UPGRADING index 5ccffedf7..7cd085fa7 100644 --- a/doc/UPGRADING +++ b/doc/UPGRADING @@ -21,7 +21,6 @@ http://www.redmine.org/ 6. Copy the RAILS_ROOT/files directory content into your new installation Note 1: Rails 1.2.2 or higher is required for version 0.4.2 and later. -This release won't work with Rails 2.0 Note 2: when upgrading your code with svn update, don't forget to clear the application cache (RAILS_ROOT/tmp/cache) before restarting. diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb index f09287e7c..dd0f4f82c 100644 --- a/lib/redmine/version.rb +++ b/lib/redmine/version.rb @@ -4,7 +4,7 @@ module Redmine module VERSION #:nodoc: MAJOR = 0 MINOR = 6 - TINY = 0 + TINY = 1 def self.revision revision = nil From 7e9c454478797cfaa4f5590188980ca045c9d5e5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 10 Dec 2007 20:58:24 +0000 Subject: [PATCH 055/710] Fixed: 'LDAP account password is too long' error when leaving the field empty on creation. git-svn-id: http://redmine.rubyforge.org/svn/trunk@977 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/auth_source.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/auth_source.rb b/app/models/auth_source.rb index 2f651ade5..47c121a13 100644 --- a/app/models/auth_source.rb +++ b/app/models/auth_source.rb @@ -20,7 +20,8 @@ class AuthSource < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name - validates_length_of :name, :host, :account_password, :maximum => 60 + 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 From e29539df9c2b8f31b46ac20a7fb8337a5b4bc4de Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 20:45:32 +0000 Subject: [PATCH 056/710] Fixed: 'assigned to me' filter broken. git-svn-id: http://redmine.rubyforge.org/svn/trunk@979 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index 4133abd88..ded8be770 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -54,7 +54,6 @@ class Query < ActiveRecord::Base serialize :column_names attr_protected :project, :user - attr_accessor :executed_by validates_presence_of :name, :on => :save validates_length_of :name, :maximum => 255 @@ -112,8 +111,7 @@ class Query < ActiveRecord::Base def initialize(attributes = nil) super attributes self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } - @executed_by = User.current.logged? ? User.current : nil - set_language_if_valid(executed_by.language) if executed_by + set_language_if_valid(User.current.language) end def validate @@ -145,12 +143,12 @@ class Query < ActiveRecord::Base "done_ratio" => { :type => :integer, :order => 13 }} user_values = [] - user_values << ["<< #{l(:label_me)} >>", "me"] if executed_by + user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? if project user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } - elsif executed_by + else # members of the user's projects - user_values += executed_by.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } + user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } end @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty? @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty? @@ -267,7 +265,7 @@ class Query < ActiveRecord::Base elsif project clause << "#{Issue.table_name}.project_id=%d" % project.id else - clause << Project.visible_by(executed_by) + clause << Project.visible_by(User.current) end # filters clauses @@ -292,7 +290,7 @@ class Query < ActiveRecord::Base # "me" value subsitution if %w(assigned_to_id author_id).include?(field) - v.push(executed_by ? executed_by.id.to_s : "0") if v.delete("me") + v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") end case operator_for field From 3539bef96b06d12248ef9d164be1e0fe0b536181 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 20:49:02 +0000 Subject: [PATCH 057/710] Updated Chinese translation (Shortie Lo). git-svn-id: http://redmine.rubyforge.org/svn/trunk@980 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/zh-tw.yml | 66 +++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 7d9cdc957..af0fd3ed4 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -5,8 +5,8 @@ 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_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 小時 @@ -46,10 +46,10 @@ general_text_No: 'No' general_text_Yes: 'Yes' general_text_no: 'no' general_text_yes: 'yes' -general_lang_name: 'Traditional Chinese (繁體中文)' +general_lang_name: 'Chinese (繁體中文)' general_csv_separator: ',' -general_csv_encoding: ISO-8859-1 -general_pdf_encoding: big5 +general_csv_encoding: Big5 +general_pdf_encoding: Big5 general_day_names: 星期一,星期二,星期三,星期四,星期五,星期六,星期日 general_first_day_of_week: '7' @@ -62,9 +62,9 @@ notice_account_unknown_email: Unknown user. notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you. notice_account_activated: Your account has been activated. You can now log in. -notice_successful_create: Successful creation. -notice_successful_update: Successful update. -notice_successful_delete: Successful deletion. +notice_successful_create: 建立成功 +notice_successful_update: 更新成功 +notice_successful_delete: 刪除成功 notice_successful_connection: Successful connection. notice_file_not_found: The page you were trying to access doesn't exist or has been removed. notice_locking_conflict: Data have been updated by another user. @@ -122,7 +122,7 @@ field_subject: 主旨 field_due_date: 完成日期 field_assigned_to: 分派給 field_priority: 重要性 -field_fixed_version: Fixed version +field_fixed_version: 版本 field_user: 用戶 field_role: 角色 field_homepage: 網站首頁 @@ -151,21 +151,21 @@ field_attr_lastname: Lastname attribute field_attr_mail: Email attribute field_onthefly: On-the-fly user creation field_start_date: 開始日期 -field_done_ratio: %% 已完成 +field_done_ratio: 完成百分比 field_auth_source: 認證模式 field_hide_mail: 隱藏我的電子郵件 field_comments: 註解 field_url: URL -field_start_page: Start page +field_start_page: 首頁 field_subproject: 子專案 field_hours: 小時 -field_activity: Activity +field_activity: 活動 field_spent_on: 日期 field_identifier: 代碼 field_is_filter: Used as a filter field_issue_to_id: Related issue field_delay: 逾期 -field_assignable: Issues can be assigned to this role +field_assignable: 項目可被分派至此角色 field_redirect_existing_links: Redirect existing links field_estimated_hours: 預估工時 field_column_names: Columns @@ -304,9 +304,9 @@ label_open_issues: 進行中 label_open_issues_plural: 進行中 label_closed_issues: 已結束 label_closed_issues_plural: 已結束 -label_total: Total +label_total: 總計 label_permissions: 權限 -label_current_status: Current status +label_current_status: 目前狀態 label_new_statuses_allowed: New statuses allowed label_all: 全部 label_none: 空值 @@ -314,11 +314,11 @@ label_nobody: nobody label_next: Next label_previous: Previous label_used_by: Used by -label_details: Details +label_details: 明細 label_add_note: 加入一個新筆記 label_per_page: 每頁 label_calendar: 日曆 -label_months_from: months from +label_months_from: 個月, 開始月份 label_gantt: 甘特圖 label_internal: Internal label_last_changes: 最近 %d 個變更 @@ -360,7 +360,7 @@ label_latest_revision: 最新版次 label_latest_revision_plural: 最近版次清單 label_view_revisions: 檢視版次清單 label_max_size: 最大長度 -label_on: 'on' +label_on: 總共 label_sort_highest: 移動至開頭 label_sort_higher: 往上移動 label_sort_lower: 往下移動 @@ -368,7 +368,7 @@ label_sort_lowest: 移動至結尾 label_roadmap: 版本藍圖 label_roadmap_due_in: 倒數天數: label_roadmap_overdue: %s 逾期 -label_roadmap_no_issues: No issues for this version +label_roadmap_no_issues: 此版本尚未包含任何項目 label_search: 搜尋 label_result_plural: 結果 label_all_words: All words @@ -382,13 +382,13 @@ label_index_by_date: 依日期索引 label_current_version: 現行版本 label_preview: 預覽 label_feed_plural: Feeds -label_changes_details: Details of all changes +label_changes_details: 所有變更的明細 label_issue_tracking: 項目追蹤 label_spent_time: 耗用時間 -label_f_hour: %.2f hour -label_f_hour_plural: %.2f hours +label_f_hour: %.2f 小時 +label_f_hour_plural: %.2f 小時 label_time_tracking: Time tracking -label_change_plural: Changes +label_change_plural: 變更 label_statistics: Statistics label_commits_per_month: Commits per month label_commits_per_author: Commits per author @@ -405,11 +405,11 @@ label_loading: 載入中... label_relation_new: 建立新關聯 label_relation_delete: 刪除關聯 label_relates_to: 關聯至 -label_duplicates: duplicates -label_blocks: blocks -label_blocked_by: blocked by -label_precedes: precedes -label_follows: follows +label_duplicates: 已重複 +label_blocks: 阻擋 +label_blocked_by: 被阻擋 +label_precedes: 優先於 +label_follows: 跟隨於 label_end_to_start: end to start label_end_to_end: end to end label_start_to_start: start to start @@ -501,9 +501,9 @@ text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 means no restriction text_project_destroy_confirmation: Are you sure you want to delete this project and all related data ? text_workflow_edit: Select a role and a tracker to edit the workflow -text_are_you_sure: Are you sure ? -text_journal_changed: changed from %s to %s -text_journal_set_to: set to %s +text_are_you_sure: 確定執行? +text_journal_changed: 從 %s 變更為 %s +text_journal_set_to: 設定為 %s text_journal_deleted: deleted text_tip_task_begin_day: task beginning this day text_tip_task_end_day: task ending this day @@ -541,8 +541,8 @@ default_doc_category_tech: 技術文件 default_priority_low: 低 default_priority_normal: 正常 default_priority_high: 高 -default_priority_urgent: 急 -default_priority_immediate: 特急 +default_priority_urgent: 速 +default_priority_immediate: 急 default_activity_design: 設計 default_activity_development: 開發 From 76ed8cc200c5a0ddcb5484efd53f31f577c9e2f6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 20:56:22 +0000 Subject: [PATCH 058/710] Added some functional tests (projects and repositories). git-svn-id: http://redmine.rubyforge.org/svn/trunk@981 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/fixtures/repositories.yml | 6 +- test/functional/projects_controller_test.rb | 57 +++++++++++- .../repositories_controller_test.rb | 18 ++++ ...repositories_subversion_controller_test.rb | 91 +++++++++++++++++++ test/unit/repository_bazaar_test.rb | 2 +- test/unit/repository_subversion_test.rb | 2 +- 6 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 test/functional/repositories_subversion_controller_test.rb diff --git a/test/fixtures/repositories.yml b/test/fixtures/repositories.yml index 46afed245..d86e301c9 100644 --- a/test/fixtures/repositories.yml +++ b/test/fixtures/repositories.yml @@ -1,11 +1,12 @@ --- repositories_001: project_id: 1 - url: svn://localhost/test + url: file:///<%= RAILS_ROOT.gsub(%r{config\/\.\.}, '') %>/tmp/test/subversion_repository id: 10 - root_url: svn://localhost + root_url: file:///<%= RAILS_ROOT.gsub(%r{config\/\.\.}, '') %>/tmp/test/subversion_repository password: "" login: "" + type: Subversion repositories_002: project_id: 2 url: svn://localhost/test @@ -13,3 +14,4 @@ repositories_002: root_url: svn://localhost password: "" login: "" + type: Subversion diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index d98e0d97b..52c33ddd7 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -22,7 +22,7 @@ require 'projects_controller' class ProjectsController; def rescue_action(e) raise e end; end class ProjectsControllerTest < Test::Unit::TestCase - fixtures :projects, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations + fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details, :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations def setup @controller = ProjectsController.new @@ -41,6 +41,10 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'list' assert_not_nil assigns(:project_tree) + # Root project as hash key + assert assigns(:project_tree).has_key?(Project.find(1)) + # Subproject in corresponding value + assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) end def test_show @@ -86,6 +90,21 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'roadmap' assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version doesn't appear + assert !assigns(:versions).include?(Version.find(1)) + end + + def test_roadmap_with_completed_versions + get :roadmap, :id => 1, :completed => 1 + assert_response :success + assert_template 'roadmap' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version appears + assert assigns(:versions).include?(Version.find(1)) end def test_activity @@ -120,6 +139,42 @@ class ProjectsControllerTest < Test::Unit::TestCase } end + def test_calendar + get :calendar, :id => 1 + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + end + + def test_calendar_with_subprojects + get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] + assert_response :success + assert_template 'calendar' + assert_not_nil assigns(:calendar) + end + + def test_gantt + get :gantt, :id => 1 + assert_response :success + assert_template 'gantt.rhtml' + assert_not_nil assigns(:events) + end + + def test_gantt_with_subprojects + get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2] + assert_response :success + assert_template 'gantt.rhtml' + assert_not_nil assigns(:events) + end + + def test_gantt_export_to_pdf + get :gantt, :id => 1, :format => 'pdf' + assert_response :success + assert_template 'gantt.rfpdf' + assert_equal 'application/pdf', @response.content_type + assert_not_nil assigns(:events) + end + def test_archive @request.session[:user_id] = 1 # admin post :archive, :id => 1 diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb index d5ccc660d..2f0459505 100644 --- a/test/functional/repositories_controller_test.rb +++ b/test/functional/repositories_controller_test.rb @@ -31,6 +31,13 @@ class RepositoriesControllerTest < Test::Unit::TestCase User.current = nil end + def test_revisions + get :revisions, :id => 1 + assert_response :success + assert_template 'revisions' + assert_not_nil assigns(:changesets) + end + def test_revision_with_before_nil_and_afer_normal get :revision, {:id => 1, :rev => 1} assert_response :success @@ -43,4 +50,15 @@ class RepositoriesControllerTest < Test::Unit::TestCase } end + def test_graph_commits_per_month + get :graph, :id => 1, :graph => 'commits_per_month' + assert_response :success + assert_equal 'image/svg+xml', @response.content_type + end + + def test_graph_commits_per_author + get :graph, :id => 1, :graph => 'commits_per_author' + assert_response :success + assert_equal 'image/svg+xml', @response.content_type + end end diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb new file mode 100644 index 000000000..9cc7048df --- /dev/null +++ b/test/functional/repositories_subversion_controller_test.rb @@ -0,0 +1,91 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'repositories_controller' + +# Re-raise errors caught by the controller. +class RepositoriesController; def rescue_action(e) raise e end; end + +class RepositoriesControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers + + # No '..' in the repository path for svn + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/subversion_repository' + + def setup + @controller = RepositoriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + if File.directory?(REPOSITORY_PATH) + def test_show + get :show, :id => 1 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_not_nil assigns(:changesets) + end + + def test_browse_root + get :browse, :id => 1 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + entry = assigns(:entries).detect {|e| e.name == 'subversion_test'} + assert_equal 'dir', entry.kind + end + + def test_browse_directory + get :browse, :id => 1, :path => ['subversion_test'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'} + assert_equal 'file', entry.kind + assert_equal 'subversion_test/helloworld.c', entry.path + end + + def test_entry + get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'] + assert_response :success + assert_template 'entry' + end + + def test_entry_download + get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw' + assert_response :success + end + + def test_diff + get :diff, :id => 1, :rev => 3 + assert_response :success + assert_template 'diff' + end + + def test_annotate + get :annotate, :id => 1, :path => ['subversion_test', 'helloworld.c'] + assert_response :success + assert_template 'annotate' + end + else + puts "Subversion test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb index 68a1ef5e3..22a190990 100644 --- a/test/unit/repository_bazaar_test.rb +++ b/test/unit/repository_bazaar_test.rb @@ -81,7 +81,7 @@ class RepositoryBazaarTest < Test::Unit::TestCase assert_equal 'mkdir', annotate.lines[0] end else - puts "Bazaar test repository NOT FOUND. Skipping tests !!!" + puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end end end diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb index 592eb4ffa..879feece8 100644 --- a/test/unit/repository_subversion_test.rb +++ b/test/unit/repository_subversion_test.rb @@ -49,7 +49,7 @@ class RepositorySubversionTest < Test::Unit::TestCase assert_equal 8, @repository.changesets.count end else - puts "Subversion test repository NOT FOUND. Skipping tests !!!" + puts "Subversion test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end end end From d5cc40c9b6b70b45efbe7ca284203274ccf1b53a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 21:03:36 +0000 Subject: [PATCH 059/710] Fixed: calendar and gantt broken with Rails 2.0 git-svn-id: http://redmine.rubyforge.org/svn/trunk@982 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 70b1eccd1..84eeefee4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -79,7 +79,8 @@ class Project < ActiveRecord::Base conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"] end conditions ||= ["#{Issue.table_name}.project_id = ?", id] - Issue.with_scope :find => { :conditions => conditions } do + # Quick and dirty fix for Rails 2 compatibility + Issue.send(:with_scope, :find => { :conditions => conditions }) do yield end end From bd8eded6702add06ae55cc960fe3b9cc378a9fd9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 21:10:04 +0000 Subject: [PATCH 060/710] Fixed: 500 error when validation fails on issue edition with no custom fields. git-svn-id: http://redmine.rubyforge.org/svn/trunk@983 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 081d8f895..901e1432b 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -91,6 +91,7 @@ class IssuesController < ApplicationController def edit @priorities = Enumeration::get_values('IPRI') + @custom_values = [] 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 From 63ef594033da88cf0e18839f706d86e353f3bd5b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 22:22:33 +0000 Subject: [PATCH 061/710] Added some functional tests (issues). git-svn-id: http://redmine.rubyforge.org/svn/trunk@984 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/functional/issues_controller_test.rb | 67 +++++++++++++++++++++++ test/unit/repository_bazaar_test.rb | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 484ebc62b..b9d232bfd 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -83,4 +83,71 @@ class IssuesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:changes) assert_equal 'application/atom+xml', @response.content_type end + + def test_show + get :show, :id => 1 + assert_response :success + assert_template 'show.rhtml' + assert_not_nil assigns(:issue) + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 1 + assert_response :success + assert_template 'edit' + assert_not_nil assigns(:issue) + assert_equal Issue.find(1), assigns(:issue) + end + + def test_post_edit + @request.session[:user_id] = 2 + post :edit, :id => 1, :issue => {:subject => 'Modified subject'} + assert_redirected_to 'issues/show/1' + assert_equal 'Modified subject', Issue.find(1).subject + end + + def test_post_change_status + issue = Issue.find(1) + assert_equal 1, issue.status_id + @request.session[:user_id] = 2 + post :change_status, :id => 1, + :new_status_id => 2, + :issue => { :assigned_to_id => 3 }, + :notes => 'Assigned to dlopper', + :confirm => 1 + assert_redirected_to 'issues/show/1' + issue.reload + assert_equal 2, issue.status_id + j = issue.journals.find(:first, :order => 'created_on DESC') + assert_equal 'Assigned to dlopper', j.notes + assert_equal 2, j.details.size + end + + def test_context_menu + @request.session[:user_id] = 2 + get :context_menu, :id => 1 + assert_response :success + assert_template 'context_menu' + end + + def test_destroy + @request.session[:user_id] = 2 + post :destroy, :id => 1 + assert_redirected_to 'projects/1/issues' + assert_nil Issue.find_by_id(1) + end + + def test_destroy_attachment + issue = Issue.find(3) + a = issue.attachments.size + @request.session[:user_id] = 2 + post :destroy_attachment, :id => 3, :attachment_id => 1 + assert_redirected_to 'issues/show/3' + assert_nil Attachment.find_by_id(1) + issue.reload + assert_equal((a-1), issue.attachments.size) + j = issue.journals.find(:first, :order => 'created_on DESC') + assert_equal 'attachment', j.details.first.property + end end diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb index 22a190990..6b21607bc 100644 --- a/test/unit/repository_bazaar_test.rb +++ b/test/unit/repository_bazaar_test.rb @@ -21,7 +21,7 @@ class RepositoryBazaarTest < Test::Unit::TestCase fixtures :projects # No '..' in the repository path - REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + 'tmp/test/bazaar_repository' + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository' def setup @project = Project.find(1) From ab4ff48abc9b2402c2dc24805fd0d56b871fa5fe Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 22:23:34 +0000 Subject: [PATCH 062/710] Added /coverage and /vendor/rails to svn-ignore. git-svn-id: http://redmine.rubyforge.org/svn/trunk@985 e93f8b46-1217-0410-a6f0-8f06a7374b81 From 86d756d22d6c191920970165d2dc40d1e823351d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 12 Dec 2007 22:57:20 +0000 Subject: [PATCH 063/710] News comments are now textilized. git-svn-id: http://redmine.rubyforge.org/svn/trunk@986 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/news/show.rhtml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index e4d7da4db..2b71c48ad 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -30,16 +30,17 @@
    <%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
    - <%= simple_format(auto_link(h(comment.comments)))%> + <%= textilizable(comment.comments) %> <% end if @news.comments_count > 0 %> <% if authorize_for 'news', 'add_comment' %>

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

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

    <%= submit_tag l(:button_add) %>

    <% end %> <% end %> -<% set_html_title h(@news.title) -%> +<% set_html_title(h(@news.title)) -%> From 48949f979a7d132ccbbc2239cd9e4ecf35fdeef5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 13 Dec 2007 18:52:09 +0000 Subject: [PATCH 064/710] Added some functional tests and a CVS test repository. git-svn-id: http://redmine.rubyforge.org/svn/trunk@987 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/admin_controller.rb | 2 +- doc/RUNNING_TESTS | 4 + .../repositories/cvs_repository.tar.gz | Bin 0 -> 12248 bytes test/fixtures/roles.yml | 2 + test/functional/account_controller_test.rb | 73 ++++++++++++++++++ test/functional/admin_controller_test.rb | 61 +++++++++++++++ test/functional/issues_controller_test.rb | 12 ++- test/functional/messages_controller_test.rb | 50 ++++++++++++ test/functional/my_controller_test.rb | 2 +- ...repositories_subversion_controller_test.rb | 2 +- test/functional/users_controller_test.rb | 62 +++++++++++++++ test/functional/versions_controller_test.rb | 31 ++++++++ test/functional/welcome_controller_test.rb | 49 ++++++++++++ test/unit/repository_cvs_test.rb | 60 ++++++++++++++ 14 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/repositories/cvs_repository.tar.gz create mode 100644 test/functional/account_controller_test.rb create mode 100644 test/functional/admin_controller_test.rb create mode 100644 test/functional/users_controller_test.rb create mode 100644 test/functional/welcome_controller_test.rb create mode 100644 test/unit/repository_cvs_test.rb diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 58d6115ee..5ad3d696b 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -48,7 +48,7 @@ class AdminController < ApplicationController def mail_options @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted) if request.post? - settings = (params[:settings] || {}).dup + settings = (params[:settings] || {}).dup.symbolize_keys settings[:notified_events] ||= [] settings.each { |name, value| Setting[name] = value } flash[:notice] = l(:notice_successful_update) diff --git a/doc/RUNNING_TESTS b/doc/RUNNING_TESTS index bd72ac71a..eb8787d95 100644 --- a/doc/RUNNING_TESTS +++ b/doc/RUNNING_TESTS @@ -8,6 +8,10 @@ Subversion svnadmin create tmp/test/subversion_repository gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load tmp/test/subversion_repository +CVS +--- +gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test + Bazaar ------ gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test \ No newline at end of file diff --git a/test/fixtures/repositories/cvs_repository.tar.gz b/test/fixtures/repositories/cvs_repository.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..638b166b5e8ef53821313bb1e3f4e5ecb4637377 GIT binary patch literal 12248 zcmZA71yfy3v@Ys}yM*BG65I(MT!L$mpuydJ;qDNETX1&>?(QzZ-Q8i$n{V%Ps&1Ws zFspm?nB#rA$)XSuEBuV4K`16}&IV3D9PFJfUF@AaSzL^qKq z*11KQd86-0lQR)v6QtNOIA$JQdi;BBfk(1xdqTSeT%Z2>HI-xVLmt7p0#IpN54;z# zYPh}MlufUP{H$(;S?O>a3b1|dNwJ+TThJ`$2EVVJhw1=kwyGY@xA~-W#~_2nJ}ar`?(1duXH7A{^~1ljR*LFX1&i3w*L%}7#A;r>=PGG6jIr_AhhE0?zMDt#nM|o%MvgpIMb><= zM1~q8EGM+P(*1;59Zq2K2y~cu z5SclU^j?d0N50u-;k)B7Zo*`$;g`FtN45;59uO1q7h0I$O~c)g&$q0GDq+_F44poK z#bxx-o%&*rRvQ!b1Wgd0#^GApR6US%k>0Dkn zu|;t`X;4?F-AhZiuoKDvzQ%_MeWPoeEdg4mb5aMN$3nAC3hdfuPi7e!a9qOHYIJG`p3G3vageyR!>M1reg+^ zGCd=HGC?$#_W64^DdMZP(dVDVaY?wt;9~Q8PD_)BH-tQcj1*wOEf#Q3?8IXILB7$E z>2E`n9UIgmd@=4G+ZK0b2%F>-RAIdpYdg21m1yvkO!C-xD1uMW2|o3r-3}@1;jN*Lwe%=zIe^x7Y!6H_xA#|8+5$vm zgZ2!TV@Dky{9rY%v@3jRs{N_lX$f6HbLx#BkK)xqd>D&&idCwxX>53X5E??^+LEwB zlw>{;ke1Sq_WLe+eMZKqDqbx4-~kG5DYK`aXu&Rh^WrX2`N zb#wJ2vm%w-@5TEUgfRh62NMsBnn{G|LfGv)aNtPTUqOmc+sohn9`5&VXCqV9-vuGp zE5|hNrft5tl)C;8$4^qcVU>O|HXTdGVMhaj96SWHTpFk3{tGw zQD_~;yRhb=yO1?D8yl^jEE~pP8J7qsW|tdoW`bx1&9}Q zk1`9}oQPMTJ5-oiNd_n7E=kn=Z}_0}M4Dsu40*d@xC8 z4Z=|W=;s5SH(s&Nzx^|k`Gj%Hy@4P*E*$%TJ96eMped=RP0ND-T z0O8J(2!?%*aWul8?ciLQ0y!C`ELl-+{D-4XvI|v~XvqWav>I*4@*7_~ zzWS*hNi-O^7zwm7f1lBVZtUR~e7iX{jJpV;wA4)Bfbs8RpQv4qcH3%AblDiOx}m>; za%sm*4{%}mWU?6*_LcX-mLJNs7A&Y@2w!YarJK|T1wk$KTE_+773xw}d=>pc9FFwa z8+{5h?4pQmil)b3$G`J^m(^4XB2vb}9R83zq}sxCiW&4h#S>O$QBe{uS74GXD-oGG zWP~!=2J!UxmU{+RgHi@iJ*Rz z+9ZY^jld7@1U_g?k#@`N|EAzckG~h(J1*TR#t*7vr0RAi-Ovz63TO5GtJbNgl`zPF zP&ZpGx`AH1AG?pgvF9+FiPWK)Jl4mbD!lE9`P;T%@#pfPc*markyQQZFDPAQb{}2= zp=X9VnjHtB(l8L`3BL|$gj2P$A*|OfqlsajD2|}%DV>w^OL{#*2sFp*0tW_4$n~(q zx&z|vc{&UES_I40+1bqyC-KhiA?t<%9u!N07Nan^YT(e-FbduM4^SgO=9s? z6t{^9us(wir~i4Rp6>{s^Cn-2;RL_v)>RLzT=l;uAKZY+e}6>>I<-fEIKqj=hWF}? zs)IK$o@^FSx}X^bKIxy*`dTR?MX7oeRlsMWY1%k`g3l!I<;QMcBLx%mVt!nkCcg#| z^t>E#$$*VeEeyARPH36O%mf z7*lsqgbRy|#>~FNvad8;7e#e#ewYUcqd;1+Ym_LyCPumcz;s~fej*O#2;Wky_i1uC z`OKn{6k#39w>tuh{!oTo=%+!}+XLwy0EgH##| zIAm9IerR_3@IyWp`X4uEyY8{jxa{;K1<>4K&aOa#ZUDGTp|pNi>m!#Qf;fb^k-4=x+}S_la#4YZXp)P4+rC_ z)KE_|IJR|@{OtR!Tx4=$wxX{bzz@T$_$%Rd3?v9$n)B(yIO{f?l*mu8pooa|Ln#)m zqG{e0^#0Ems5D2eHp!>@5*i!z^a27cBRpiSZ7xYTS^ShS5Y(OJ`tt1$`rkPQSwhq@ zF$fgM@k3WuA3~MYc`(U@F6}@JqKm$m^)AT0#>iVgVNWQo3nwj3tIa@&V$IkuMUFD$3 zDhPv&cthJxe&25h0|OI4_lY!V#^!6m>m9r~9)?a9OZFIn*E41s`N3k2K+>{`uq&~{ zB&69V_Fs(oUG4*EnPQpvBk#{vKp3)Slx6wI$PtB3R7>U`>o4>$2%Eo*1Eq`h`lSXf zS&D3$ka-F77e`aD&-nfa_Fl0z-oy^#Jjr2dANT061*9Q=r3w5?*>pxAK(S(jI*BJ` z1t2<4eyb*Fbb9WJD!?bNez3X4c!&2Qb9POvX)f7>iRyQ5LuVMIuCiP4H%gau+NLJ3 z%-5E3oTSYU;h~&8)Vk{VzT;oHyeYSs&!Co2XoHvQSIW8Y!!2J@iZWA%UaA$56u~8J z%x#49I;J{qd&KpNXL8S{WEr-iO1XXsbHbEm8r}30Y;eTc9Dt{Y1hK-wD1bIoRR zkOQ)5qgnh6F8@CD{4aCt{L38DOWUiMPsLXT9GDm{mF zn4X`-UuVUh$?((kK>7#3LPg;opmG7UWnd=(E`zW9D|qB9&Fj9)a9M9jy$JOGF=U zb2dSW?V+Ic_BRzZ)4i=~)1JrZY?~zxP!xBxbm!&VMXRG*!FXrgyMpqbj`_Lbc>eB( zMYTORA@=S#)J1Hh_r z?f`5!TwnRhNDK;wg7p~?36yc{UBrJ<^C!7kiO4s@SHtyoiy%P%k_j578wkx`&@Pbt zeA2l%s|H`&O8}!;5JzBR65SMX6>W4613z1P!x^e~v!8ewE7vJ#^zYntRCWD+-E8y{ z@&(#{<^Y$}4A0e?-hNl+B*p9IoRsxHoVIxroGfmoKjbj53Bnnf{fVL6R$HCYY-jBJ z@T&Efpj@XL@>?G3=)?H8yUYV<3OvM|PZRqXQntz%8qmX#P1gxu_=(j81rK=OzxG8@=1x!r$}J=|d52RF?;^ z^;W~bKMSxKS4dFkR*0|Ulw6D3knHux6p|s4OO2q_HJ2i#BCNiASgz}n7BNDh9=MkM z;HtlVe>G4v*<#T273WhWw)eCNwLbPnGYn4J-e4lObIx=mgUR976RAeOo+&5o(yd3# zf;i z^R$!j?_kK>!sD`HAFhP@SqS!W&#X}|`=Epc!Ht7?N&(m-wr33G9#~ zD|EyIA1a=j{oHC(0BD{!Ew~oFfaXQ3e`6&c0~p{11MVXb)^!_M6Zr@5IXw;Aa z{?1a}P`B7t?Rr6CY+zvZN2jak*_uEvLEJqWLE=_6qE{GPL3;9UgR!a8$& z`MegCxsPv-)8X{FcLaIMG|YZ6gtx-O4#keApQrpwhHJuvRd*_ z@n&igA1{Y_Jj~SioQG^aI`4PQ)pH<0?gvz#F~q>DkTnZlr`LbI&CBm?AC?cea;YGV z8~Ro4cpE)`t}6^B$*P>F+gp5wZM6<&;ja-(QBR+o#(ic z|80`dPL^IFq7xHK=({|ZkFg4dJl=3BnC-~RnIkB3;hE9hB^Ki*Ge5xdIc4 z?}#d)0@i6u^=a;EhRWFPhUY$gGML1Paw^*Wx;gIQJ|W@%vWiq(mZ&=ZN@v9V(D%RW z!}qBhHi!1-SN%ectud!w??L-nf-UUr2->9HHmu zg{UDBVS$cLjcwiqXPRT^q-u2=6$7ZQ z-xF7vIW4P8#s>8WtBUZK-y0Kr9CJzg`B4vZo@f=qRJIkXXX?GBPbvXE&*ly@fb;7DcU95H7&v)*!KWF{wbEMbe$L5El zJ6r6+doQ-J^Z6%uzY~7er2`x<^+J5WH5kI|_i(m&^FLT}bk2G~y6J-UI&?zN;UmQw za$S%NhB`Lh?Z6NhipHsLVm?(;EfeP45HVAD%QPMNZQKj9&n{yX>V!nd;53|nqTLK_G}olvBK}Et@5dnubDVzWP5?S?M;oH9*+M|GM5i; z%7?p~NDCQvO}(&pxDAgplNbL#QWE9$mPK%$_1XHT;>iNFb=vwNEF$^6!Sx-fU1Thj zSnB4@0->eNL+`a5KNe356b%&fO&U|kSRFe1(T_A~|Go445|B&f^KUiPfC9D^xkNN0 zAzv|JWcl$zk67&>NM69rqr~XE>GKZ{Bm}%LDG4j)b>pPS{1JWD&X({%JeIReqZ!io z`EI{~24!HZ`yt9Q0yv2x0m$4<|sCjID z?RnL9`O?;94OM+D)ht8P=8;3w`p}}0U2H_OctW)>t9&L@e^M%ELOp9jHDf|KZ9?(a zy68{+l97w^AHRppatH0o2h585?aKS4%Xab#u zJLxeS=?SZ?9gFEPv#m{&WpT3N1EHPTxz&RN!?>$f@S|3ttyWTHR#NFF2azZ33z^jg zJ1TpE@0WvJ_g7!%Z=09cQ`UhJZZ(L=f|(zrIjNw4Y{>5N9`x|!qpLcihg(R}hw-~b zn=~tf?!xM4SYXc=dd#D12k5b3P&CM1l&uz>#=qdswfe@jHL@XGB>tx#+>IL_OVa;> zPZo|>rNyr>S2o{w3dQws{!g`2{Hn}$0`({_-?p_Dn2ig&=tLwt3r;Fs?it~g9UFvH zlf6Tr`QFh0gXv_ z?K5@@>1LJBQ2RGwWTcP-*#@CetJ7vfz8AD=*tdf=lWIorX)6Xq_1%BdCl|O7+U;dF zDeTp&(<}wI2nxJ!udL7xkX=dVl;z)gF%9#DpDNYaRbA3CfRFE0EhC*)5#1bE|m4mQb3*09F%xpX{46N0L4jVD+f_m z2o453yoo=0ky(&k+++g27<_8b4^oAOy_rUM;z#De>=)L zQg3b5pQYFs6r^VE@8siMg76-7V$^AN_^q|O!qDXksp;W4A6|dtVP`7K53qWHkePyex9N-%^1dxA3v9+ z)a3iDKiC^$kHgrG8;29?I}IkDxUNu-79$@1e`shGCYHV779`CoT7Hg{U15KCgJVxBI2LeXt!bOmpiw zYOXETiXEbjR$t?3?<$ju?P4yE?$D|27`Kjn`^+C!;6lxTgh>9lZ%3sWZ z3?aSy%RP9f>%u~dzx-|cDWKie(J!OeXw-PeE&`F~B@=dB2tIeAf)Fv|O0jz0u)Z7K zsXY(f-rhdDmjy?B{6^m{3aRoFMaP}6`v76Ga>V{g`!J=k9MY@7r=ao$i*%nOT;8|G z=T2P ztzdBag$$``eMuZ`9bV~JL3;g>5=Hu4#<0Koyx&0yF~g)Y>_3faGs8zu`Yd&<3U+E| z6}1H!L&xI&xbGF%dNY-v6i&^?w=py%7Zz6*CSdmac!@U2BBcIBDGT50Svir4Y2iPM z@})cB+X{TQ{~SBieHC4W968|YW;v-`V6e3F zi%*^T#nObV8}0v6)WkZKad3#6J+(AcAHsLT;@jF4mYj~^B)O=4K=Dk3{9f&*OJ!^d za7(;McsHj>pp~$p_nuLEcv-PN_ZT&{{*Cx((F^s38+#bWGuA6)Q z*SWI7kxeT}(Fppia##}_7r}N~#nyPM#R0lN#AE`%R<6lP_1IMi0b<4ZZ6~v%zK@4vfvd7EI9*H|H4B7V9R(= zm*}*D_~)yso*5+z5z;SQhXNrR){T;MjgU2cK^rBMCIa(VUHL|VisHTF-Ga|IWa}*S zsw%j)OdY`b*LA?UA?|JJbRbQ1e0~ zV{Jjd)oO17I-XeSzy>11iKDZP`ec122uGbe|BUUB^?!`*cXP11&lSZBVe-=;6j6&@ zUJnMfwZ%nXdn$Wtb_x?^r8F})d2?d^Zc##r@d z#|r~{N$^Kv$_Su;1V~pNKZ#VHs8Id-*ZCinfKBWJ(*UO?_|HT}nNxGZ>J4_ z5;B-M+Jp|Hcr}~v`_B`g<%<<(pHtXF@6q(IyYbZxi;`&n3 z(NIoz7RJ1j{uC24Y$u2w8qB}-rlX)pGwcfW=cwoB|GJzHCt3z29^0^pi~W&-MblG) zDFY%l>q?R&su89PH^I>hmik60z`6)d~08Lu)?*oKDl5Q(;NWa}eL9)!;PWlJZLy7boZ2adB%{?%(6 zFjq!W_||pvq|_$MZus?72!W95Fav&IXR)5%0U4Bh)d4H2huVT((u&~0`M^dXyEamd zxQ|=#H_(NbNoMQ`V0i;u#dw`SlsNxq*7^5&F{x|XqrTZJe1EDHs#Fg??dR}MBmr0Y znoHo6A)vZw1I%x=e@*6=*kGuBn)MI`XT-gB=^BC}S_E4Zu5|DcIRu{&LZEBH$uO!F zyHYHHGw@l#tKO-byqmZPt|lP5dzv$XyOrv=5=w-YR-kgCGD77&yuk`~43r_Xn5X@2gLrBwchPEjHxlOo06-+w_0& zqqPM=>>G8QuSI-Qn}9<|SDWnZqJfvDJ;xnq1gK;DpB;As>lOQtU2ciV#@JQvI@}z} zoyeoP8%WX*sH3LMU{o1~qkO*n-T?eY20l1o^eO2=EG8@wayl5fEfGTw(>q4;Bf$34>@t~&8y9@0=e2L9`c4F?ini4F_CnKJ-sHK+`PCd=JMsic^HWgn&kf^g}5mApC zu~s^=?+WG?6nC1hB6O#1Q)Y|4B|}>E#md_$Tb*BAzJ{9*a#N7QPEycU^72P{gk$uW(%KF1Uw^Qw?T2@=;6~9&piJysRIgYxL4R#&^$7;cBrRl`nM6yqCDyoi!BE% z>CIX_TwYksQe3(qLLPcM6!S%C&iM--Z8LfLBnb}piJPBvU)7#4zKsP{tYOAjwXJ_2 zfGpW*o=W>m#>SQ{{7ZMIvEy=&#K43O*cwrAd@2@o(EyODpBY~Q<5~8+5EXqR_)K8o zFMlWEsE%3j2p=xr0l5GY14i!ap`*OfmcodVjf(lQ z^3?EPE3drLqr2<+^6Ndfn>0@QYd&Do&JeIRd3*c|dAy#>zZXG+F3*6%si=iWpSz)4i-1CX-rW;Y zs)5ReFAgKkCy|-V9g$1lP>5M>0ox_9SNk@d^4%!RN-GFtPI1=mYEz|AwQ@p2j<%r- z7L?s-DdPEZx2bS>81<6j4F_VS*-Q{qmwv3Kbf;BHpSr4&CwO;+#On+GyNwoJr~TNu zhGMt!W2;sm{VHT3Xeg}Y9@_hz%#v>(4`!@Mii%+p&BZa#TJ9`Sf;Z#pCXVih_CjrH ziwXa0SPHYjCh6X?IR-t)>NNMvZ@5lEwRyH!&;gq7`QNME0mf^p^^LlLD953u)}6m6 zaznp-`kaF|pJ5(sx}lY75Lh)Z`aiZuORy*ny-?NqoM40Eci3Q_ckQr)x-4S4UH|U0>&p8>+{a@SbZcWYhfn>FW70M%_f}Xdl4$?*uz5ZW_REx z_xJ$EpiVk{ONu`<`l`6q>F4~uzEJ$>eI}iE#L0YTe;!g~E9g9Fj&YbHiWQqxu_g#j zIWnz@%-(;1R`Me0G%?D+#%)Ij4;V`rY_Iidm=yOql{!@<8rC$|C;OH^?5nMbI$8b9 zBJTOHwRh}yl3Bftb!Q&Ep-YScqv|)=eg1SI7^K!o+ZkRyD)aaP9aQ$dj+880ImnXm zsQrwXI5;@`di$ywn9iMBB8R3av46q<0|kNCr^pq`XC?c!9^ng76f<+Bhuj*$5^o1}1DeTdkGfWl2KTt1G{GGqBb~yC zVe0uRWDu>!pm0e*Ya&*LJzEQ{1zldhtShVe~|q;A|g>2iV?Xu{Lj!jlh*$7@oJwLGKT zGl2b+Kd_j`WFYNXenBmGx7Rg@>q9%`roH|o*el(dlqb?v$Z)9`FE>FYxWabS#Tmga h8~*%3Ci^D2Sa6>~$#YaK`$GH$Ox!7t0cwE){eOE(X;lCK literal 0 HcmV?d00001 diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index bc4d65641..70490286f 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -76,6 +76,8 @@ roles_001: - :edit_wiki_pages - :delete_wiki_pages - :add_messages + - :edit_messages + - :delete_messages - :manage_boards - :view_files - :manage_files diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb new file mode 100644 index 000000000..a923de3ea --- /dev/null +++ b/test/functional/account_controller_test.rb @@ -0,0 +1,73 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'account_controller' + +# Re-raise errors caught by the controller. +class AccountController; def rescue_action(e) raise e end; end + +class AccountControllerTest < Test::Unit::TestCase + fixtures :users + + def setup + @controller = AccountController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show + get :show, :id => 2 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:user) + end + + def test_show_inactive + get :show, :id => 5 + assert_response 404 + assert_nil assigns(:user) + end + + def test_login_with_wrong_password + post :login, :login => 'admin', :password => 'bad' + assert_response :success + assert_template 'login' + assert_tag 'div', + :attributes => { :class => "flash error" }, + :content => /Invalid user or password/ + end + + def test_autologin + Setting.autologin = "7" + Token.delete_all + post :login, :login => 'admin', :password => 'admin', :autologin => 1 + assert_redirected_to 'my/page' + token = Token.find :first + assert_not_nil token + assert_equal User.find_by_login('admin'), token.user + assert_equal 'autologin', token.action + end + + def test_logout + @request.session[:user_id] = 2 + get :logout + assert_redirected_to '' + assert_nil @request.session[:user_id] + end +end diff --git a/test/functional/admin_controller_test.rb b/test/functional/admin_controller_test.rb new file mode 100644 index 000000000..d49fe2dda --- /dev/null +++ b/test/functional/admin_controller_test.rb @@ -0,0 +1,61 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'admin_controller' + +# Re-raise errors caught by the controller. +class AdminController; def rescue_action(e) raise e end; end + +class AdminControllerTest < Test::Unit::TestCase + fixtures :projects, :users + + def setup + @controller = AdminController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_get_mail_options + get :mail_options + assert_response :success + assert_template 'mail_options' + end + + def test_post_mail_options + post :mail_options, :settings => {'mail_from' => 'functional@test.foo'} + assert_redirected_to 'admin/mail_options' + assert_equal 'functional@test.foo', Setting.mail_from + end + + def test_test_email + get :test_email + assert_redirected_to 'admin/mail_options' + mail = ActionMailer::Base.deliveries.last + assert_kind_of TMail::Mail, mail + user = User.find(1) + assert_equal [user.mail], mail.bcc + end + + def test_info + get :info + assert_response :success + assert_template 'info' + end +end diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index b9d232bfd..638362dbe 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -22,7 +22,17 @@ require 'issues_controller' class IssuesController; def rescue_action(e) raise e end; end class IssuesControllerTest < Test::Unit::TestCase - fixtures :projects, :users, :roles, :members, :issues, :enabled_modules, :enumerations + fixtures :projects, + :users, + :roles, + :members, + :issues, + :issue_statuses, + :trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments def setup @controller = IssuesController.new diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb index 25fc1363e..dcfe0caa7 100644 --- a/test/functional/messages_controller_test.rb +++ b/test/functional/messages_controller_test.rb @@ -40,10 +40,60 @@ class MessagesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:topic) end + def test_show_message_not_found + get :show, :board_id => 1, :id => 99999 + assert_response 404 + end + + def test_get_new + @request.session[:user_id] = 2 + get :new, :board_id => 1 + assert_response :success + assert_template 'new' + end + + def test_post_new + @request.session[:user_id] = 2 + post :new, :board_id => 1, + :message => { :subject => 'Test created message', + :content => 'Message body'} + assert_redirected_to 'messages/show' + message = Message.find_by_subject('Test created message') + assert_not_nil message + assert_equal 'Message body', message.content + assert_equal 2, message.author_id + assert_equal 1, message.board_id + end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :board_id => 1, :id => 1 + assert_response :success + assert_template 'edit' + end + + def test_post_edit + @request.session[:user_id] = 2 + post :edit, :board_id => 1, :id => 1, + :message => { :subject => 'New subject', + :content => 'New body'} + assert_redirected_to 'messages/show' + message = Message.find(1) + assert_equal 'New subject', message.subject + assert_equal 'New body', message.content + end + def test_reply @request.session[:user_id] = 2 post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' } assert_redirected_to 'messages/show' assert Message.find_by_subject('Test reply') end + + def test_destroy_topic + @request.session[:user_id] = 2 + post :destroy, :board_id => 1, :id => 1 + assert_redirected_to 'boards/show' + assert_nil Message.find_by_id(1) + end end diff --git a/test/functional/my_controller_test.rb b/test/functional/my_controller_test.rb index 5df2932ed..c1349ace4 100644 --- a/test/functional/my_controller_test.rb +++ b/test/functional/my_controller_test.rb @@ -22,7 +22,7 @@ require 'my_controller' class MyController; def rescue_action(e) raise e end; end class MyControllerTest < Test::Unit::TestCase - fixtures :users + fixtures :users, :issues, :issue_statuses, :trackers, :enumerations def setup @controller = MyController.new diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index 9cc7048df..d7ce45640 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -21,7 +21,7 @@ require 'repositories_controller' # Re-raise errors caught by the controller. class RepositoriesController; def rescue_action(e) raise e end; end -class RepositoriesControllerTest < Test::Unit::TestCase +class RepositoriesSubversionControllerTest < Test::Unit::TestCase fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers # No '..' in the repository path for svn diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb new file mode 100644 index 000000000..8629a7131 --- /dev/null +++ b/test/functional/users_controller_test.rb @@ -0,0 +1,62 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'users_controller' + +# Re-raise errors caught by the controller. +class UsersController; def rescue_action(e) raise e end; end + +class UsersControllerTest < Test::Unit::TestCase + fixtures :users, :projects, :members + + def setup + @controller = UsersController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_index + get :index + assert_response :success + assert_template 'list' + end + + def test_list + get :list + assert_response :success + assert_template 'list' + assert_not_nil assigns(:users) + # active users only + assert_nil assigns(:users).detect {|u| !u.active?} + end + + def test_edit_membership + post :edit_membership, :id => 2, :membership_id => 1, + :membership => { :role_id => 2} + assert_redirected_to 'users/edit/2' + assert_equal 2, Member.find(1).role_id + end + + def test_destroy_membership + post :destroy_membership, :id => 2, :membership_id => 1 + assert_redirected_to 'users/edit/2' + assert_nil Member.find_by_id(1) + end +end diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb index e8327938a..17ebd3518 100644 --- a/test/functional/versions_controller_test.rb +++ b/test/functional/versions_controller_test.rb @@ -39,4 +39,35 @@ class VersionsControllerTest < Test::Unit::TestCase assert_tag :tag => 'h2', :content => /1.0/ end + + def test_get_edit + @request.session[:user_id] = 2 + get :edit, :id => 2 + assert_response :success + assert_template 'edit' + end + + def test_post_edit + @request.session[:user_id] = 2 + post :edit, :id => 2, + :version => { :name => 'New version name', + :effective_date => Date.today.strftime("%Y-%m-%d")} + assert_redirected_to 'projects/settings/1' + version = Version.find(2) + assert_equal 'New version name', version.name + assert_equal Date.today, version.effective_date + end + + def test_destroy + @request.session[:user_id] = 2 + post :destroy, :id => 2 + assert_redirected_to 'projects/settings/1' + assert_nil Version.find_by_id(2) + end + + def test_issue_status_by + xhr :get, :status_by, :id => 2 + assert_response :success + assert_template '_issue_counts' + end end diff --git a/test/functional/welcome_controller_test.rb b/test/functional/welcome_controller_test.rb new file mode 100644 index 000000000..18146c6aa --- /dev/null +++ b/test/functional/welcome_controller_test.rb @@ -0,0 +1,49 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'welcome_controller' + +# Re-raise errors caught by the controller. +class WelcomeController; def rescue_action(e) raise e end; end + +class WelcomeControllerTest < Test::Unit::TestCase + fixtures :projects, :news + + def setup + @controller = WelcomeController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_index + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:news) + assert_not_nil assigns(:projects) + assert !assigns(:projects).include?(Project.find(:first, :conditions => {:is_public => false})) + end + + def test_browser_language + Setting.default_language = 'en' + @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3' + get :index + assert_equal :fr, @controller.current_language + end +end diff --git a/test/unit/repository_cvs_test.rb b/test/unit/repository_cvs_test.rb new file mode 100644 index 000000000..3f6db06eb --- /dev/null +++ b/test/unit/repository_cvs_test.rb @@ -0,0 +1,60 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'pp' +class RepositoryCvsTest < Test::Unit::TestCase + fixtures :projects + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository' + REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/ + # CVS module + MODULE_NAME = 'test' + + def setup + @project = Project.find(1) + assert @repository = Repository::Cvs.create(:project => @project, + :root_url => REPOSITORY_PATH, + :url => MODULE_NAME) + end + + if File.directory?(REPOSITORY_PATH) + def test_fetch_changesets_from_scratch + @repository.fetch_changesets + @repository.reload + + assert_equal 5, @repository.changesets.count + assert_equal 14, @repository.changes.count + assert_equal 'Two files changed', @repository.changesets.find_by_revision(3).comments + end + + def test_fetch_changesets_incremental + @repository.fetch_changesets + # Remove changesets with revision > 2 + @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy) + @repository.reload + assert_equal 2, @repository.changesets.count + + @repository.fetch_changesets + assert_equal 5, @repository.changesets.count + end + else + puts "CVS test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end From 17c7886791a7dd33daaa8ed0e334f733afefc4a1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 13 Dec 2007 18:53:34 +0000 Subject: [PATCH 065/710] Removed unused UsersController#destroy. git-svn-id: http://redmine.rubyforge.org/svn/trunk@988 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/users_controller.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index cf0128d7c..3f3adb57d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -109,12 +109,4 @@ class UsersController < ApplicationController end redirect_to :action => 'edit', :id => @user and return end - - def destroy - User.find(params[:id]).destroy - redirect_to :action => 'list' - rescue - flash[:error] = "Unable to delete user" - redirect_to :action => 'list' - end end From eb7cbd481e44fcb636957acc7896aefa52743f70 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 17:31:24 +0000 Subject: [PATCH 066/710] Added some tests for projects controller and helper. git-svn-id: http://redmine.rubyforge.org/svn/trunk@989 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/projects_helper.rb | 4 +- test/fixtures/issues.yml | 2 + test/functional/projects_controller_test.rb | 48 ++++++++++++++++++++- test/unit/helpers/projects_helper_test.rb | 41 ++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 test/unit/helpers/projects_helper_test.rb diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4b1b1a954..df4b9c334 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -179,9 +179,9 @@ module ProjectsHelper end # today red line - if Date.today >= @date_from and Date.today <= @date_to + if Date.today >= date_from and Date.today <= date_to gc.stroke('red') - x = (Date.today-@date_from+1)*zoom + subject_width + x = (Date.today-date_from+1)*zoom + subject_width gc.line(x, headers_heigth, x, headers_heigth + g_height-1) end diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml index 6649849d8..fc5b48dee 100644 --- a/test/fixtures/issues.yml +++ b/test/fixtures/issues.yml @@ -41,6 +41,8 @@ issues_003: assigned_to_id: author_id: 2 status_id: 1 + start_date: <%= 1.day.from_now.to_date.to_s(:db) %> + due_date: <%= 40.day.ago.to_date.to_s(:db) %> issues_004: created_on: 2006-07-19 21:07:27 +02:00 project_id: 2 diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 52c33ddd7..b6ac59141 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -52,7 +52,37 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'show' assert_not_nil assigns(:project) + end + + def test_settings + @request.session[:user_id] = 2 # manager + get :settings, :id => 1 + assert_response :success + assert_template 'settings' + end + + def test_edit + @request.session[:user_id] = 2 # manager + post :edit, :id => 1, :project => {:name => 'Test changed name'} + assert_redirected_to 'projects/settings/1' + project = Project.find(1) + assert_equal 'Test changed name', project.name + end + + def test_get_destroy + @request.session[:user_id] = 1 # admin + get :destroy, :id => 1 + assert_response :success + assert_template 'destroy' + assert_not_nil Project.find_by_id(1) end + + def test_post_destroy + @request.session[:user_id] = 1 # admin + post :destroy, :id => 1, :confirm => 1 + assert_redirected_to 'admin/projects' + assert_nil Project.find_by_id(1) + end def test_list_documents get :list_documents, :id => 1 @@ -70,7 +100,23 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes end - + + def test_move_issues_to_another_project + @request.session[:user_id] = 1 + post :move_issues, :id => 1, :issue_ids => [1, 2], :new_project_id => 2 + assert_redirected_to 'projects/1/issues' + assert_equal 2, Issue.find(1).project_id + assert_equal 2, Issue.find(2).project_id + end + + def test_move_issues_to_another_tracker + @request.session[:user_id] = 1 + post :move_issues, :id => 1, :issue_ids => [1, 2], :new_tracker_id => 3 + assert_redirected_to 'projects/1/issues' + assert_equal 3, Issue.find(1).tracker_id + assert_equal 3, Issue.find(2).tracker_id + end + def test_list_files get :list_files, :id => 1 assert_response :success diff --git a/test/unit/helpers/projects_helper_test.rb b/test/unit/helpers/projects_helper_test.rb new file mode 100644 index 000000000..d76d92bc9 --- /dev/null +++ b/test/unit/helpers/projects_helper_test.rb @@ -0,0 +1,41 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../../test_helper' + +class ProjectsHelperTest < HelperTestCase + include ProjectsHelper + include ActionView::Helpers::TextHelper + fixtures :projects, :trackers, :issue_statuses, :issues, :enumerations, :users, :issue_categories + + def setup + super + end + + if Object.const_defined?(:Magick) + def test_gantt_image + assert gantt_image(Issue.find(:all, :conditions => "start_date IS NOT NULL AND due_date IS NOT NULL"), Date.today, 6, 2) + end + + def test_gantt_image_with_days + assert gantt_image(Issue.find(:all, :conditions => "start_date IS NOT NULL AND due_date IS NOT NULL"), Date.today, 3, 4) + end + else + puts "RMagick not installed. Skipping tests !!!" + def test_fake; assert true end + end +end From 86319feef23618f59da13b450a085e40019a43e0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 17:33:05 +0000 Subject: [PATCH 067/710] Added ApplicationController#attach_files as a common method to attach files in all actions. git-svn-id: http://redmine.rubyforge.org/svn/trunk@990 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/application.rb | 13 +++++++++++++ app/controllers/documents_controller.rb | 10 ++-------- app/controllers/issues_controller.rb | 20 ++++---------------- app/controllers/messages_controller.rb | 12 +++--------- app/controllers/projects_controller.rb | 20 ++++---------------- app/controllers/wiki_controller.rb | 6 +----- 6 files changed, 27 insertions(+), 54 deletions(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index e186455a3..ad86b6b33 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -144,6 +144,19 @@ class ApplicationController < ActionController::Base def accept_key_auth_actions self.class.read_inheritable_attribute('accept_key_auth_actions') || [] end + + # TODO: move to model + def attach_files(obj, files) + attachments = [] + if files && files.is_a?(Array) + files.each do |file| + next unless file.size > 0 + a = Attachment.create(:container => obj, :file => file, :author => User.current) + attachments << a unless a.new_record? + end + end + attachments + end # qvalues http header parser # code taken from webrick diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 94532b65b..104cca10c 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -45,14 +45,8 @@ class DocumentsController < ApplicationController end def add_attachment - # Save the attachments - @attachments = [] - params[:attachments].each { |file| - next unless file.size > 0 - a = Attachment.create(:container => @document, :file => file, :author => User.current) - @attachments << a unless a.new_record? - } if params[:attachments] and params[:attachments].is_a? Array - Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('document_added') + attachments = attach_files(@document, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added') redirect_to :action => 'show', :id => @document end diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 901e1432b..78bcf76a7 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -116,13 +116,8 @@ class IssuesController < ApplicationController def add_note journal = @issue.init_journal(User.current, params[:notes]) - params[:attachments].each { |file| - next unless file.size > 0 - a = Attachment.create(:container => @issue, :file => file, :author => User.current) - journal.details << JournalDetail.new(:property => 'attachment', - :prop_key => a.id, - :value => a.filename) unless a.new_record? - } if params[:attachments] and params[:attachments].is_a? Array + attachments = attach_files(@issue, params[:attachments]) + attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} if journal.save flash[:notice] = l(:notice_successful_update) Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') @@ -140,15 +135,8 @@ class IssuesController < ApplicationController journal = @issue.init_journal(User.current, params[:notes]) @issue.status = @new_status if @issue.update_attributes(params[:issue]) - # Save attachments - params[:attachments].each { |file| - next unless file.size > 0 - a = Attachment.create(:container => @issue, :file => file, :author => User.current) - journal.details << JournalDetail.new(:property => 'attachment', - :prop_key => a.id, - :value => a.filename) unless a.new_record? - } if params[:attachments] and params[:attachments].is_a? Array - + attachments = attach_files(@issue, params[:attachments]) + attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} # Log time if current_role.allowed_to?(:log_time) @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 46c9adadd..8078abf71 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -42,9 +42,7 @@ class MessagesController < ApplicationController @message.sticky = params[:message]['sticky'] end if request.post? && @message.save - params[:attachments].each { |file| - Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0 - } if params[:attachments] and params[:attachments].is_a? Array + attach_files(@message, params[:attachments]) redirect_to :action => 'show', :id => @message end end @@ -56,9 +54,7 @@ class MessagesController < ApplicationController @reply.board = @board @topic.children << @reply if !@reply.new_record? - params[:attachments].each { |file| - Attachment.create(:container => @reply, :file => file, :author => User.current) if file.size > 0 - } if params[:attachments] and params[:attachments].is_a? Array + attach_files(@reply, params[:attachments]) end redirect_to :action => 'show', :id => @topic end @@ -70,9 +66,7 @@ class MessagesController < ApplicationController @message.sticky = params[:message]['sticky'] end if request.post? && @message.update_attributes(params[:message]) - params[:attachments].each { |file| - Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0 - } if params[:attachments] and params[:attachments].is_a? Array + attach_files(@message, params[:attachments]) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'show', :id => @topic end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c4d1b53fc..7b1e4ef3d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -181,10 +181,7 @@ class ProjectsController < ApplicationController def add_document @document = @project.documents.build(params[:document]) if request.post? and @document.save - # Save the attachments - params[:attachments].each { |a| - Attachment.create(:container => @document, :file => a, :author => User.current) unless a.size == 0 - } if params[:attachments] and params[:attachments].is_a? Array + attach_files(@document, params[:attachments]) flash[:notice] = l(:notice_successful_create) Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') redirect_to :action => 'list_documents', :id => @project @@ -237,10 +234,7 @@ class ProjectsController < ApplicationController @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 if @issue.save - if params[:attachments] && params[:attachments].is_a?(Array) - # Save attachments - params[:attachments].each {|a| Attachment.create(:container => @issue, :file => a, :author => User.current) unless a.size == 0} - end + 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 => 'index', :project_id => @project @@ -345,14 +339,8 @@ class ProjectsController < ApplicationController def add_file if request.post? @version = @project.versions.find_by_id(params[:version_id]) - # Save the attachments - @attachments = [] - params[:attachments].each { |file| - next unless file.size > 0 - a = Attachment.create(:container => @version, :file => file, :author => User.current) - @attachments << a unless a.new_record? - } if params[:attachments] and params[:attachments].is_a? Array - Mailer.deliver_attachments_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added') + attachments = attach_files(@issue, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added') redirect_to :controller => 'projects', :action => 'list_files', :id => @project end @versions = @project.versions.sort diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 37a36bf56..2ee22167d 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -154,11 +154,7 @@ class WikiController < ApplicationController def add_attachment @page = @wiki.find_page(params[:page]) - # Save the attachments - params[:attachments].each { |file| - next unless file.size > 0 - a = Attachment.create(:container => @page, :file => file, :author => User.current) - } if params[:attachments] and params[:attachments].is_a? Array + attach_files(@page, params[:attachments]) redirect_to :action => 'index', :page => @page.title end From 47f399104b9c3062ea27d30146c1873df2efa750 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 17:46:45 +0000 Subject: [PATCH 068/710] Added a Mercurial test repository with unit and functional tests. git-svn-id: http://redmine.rubyforge.org/svn/trunk@991 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/RUNNING_TESTS | 6 +- test/fixtures/enabled_modules.yml | 5 + .../repositories/mercurial_repository.tar.gz | Bin 0 -> 7827 bytes .../repositories_mercurial_controller_test.rb | 117 ++++++++++++++++++ test/unit/repository_mercurial_test.rb | 55 ++++++++ 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/repositories/mercurial_repository.tar.gz create mode 100644 test/functional/repositories_mercurial_controller_test.rb create mode 100644 test/unit/repository_mercurial_test.rb diff --git a/doc/RUNNING_TESTS b/doc/RUNNING_TESTS index eb8787d95..fde24413b 100644 --- a/doc/RUNNING_TESTS +++ b/doc/RUNNING_TESTS @@ -14,4 +14,8 @@ gunzip < test/fixtures/repositories/cvs_repository.tar.gz | tar -xv -C tmp/test Bazaar ------ -gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test \ No newline at end of file +gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/test + +Mercurial +--------- +gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test diff --git a/test/fixtures/enabled_modules.yml b/test/fixtures/enabled_modules.yml index 1f05cd9a7..dfc2f0090 100644 --- a/test/fixtures/enabled_modules.yml +++ b/test/fixtures/enabled_modules.yml @@ -31,3 +31,8 @@ enabled_modules_008: name: boards project_id: 1 id: 8 +enabled_modules_009: + name: repository + project_id: 3 + id: 9 + \ No newline at end of file diff --git a/test/fixtures/repositories/mercurial_repository.tar.gz b/test/fixtures/repositories/mercurial_repository.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1d8ad305732f7a0d7bfbcd52bbeec919da88e3f1 GIT binary patch literal 7827 zcmV;E9&F(siwFp_o?=G;3vFd`V|8+AVQgP=WpHnEX>@OLc`kHeascdF2{_bS8=sjF zL$;8V4AHfOnPFzgHe`)b(@l|WFox{2*s?^(UP{WfMTDD_eJk1bN<_(0h$Op^E#GkI zR?ofH?S6f}=X*ZCdEV#Dod5a%&-aQ1dYOOhSIMI7*B z0g;5FP$&ohvJB`qYUpm+ue_XGM^&;Q}bZ|8p` zbUFTS`u~3(j`!aR;xo*K$PSs$nI@`RSiw=Of7H$v0PxDCl(}jqv2>J@2BV^=d{||* znp##RA@T*Kn#s}zi^mbgab}hzF?(D5>UAKUs;mYL#-IBC%6RxOyTI??{~;vc$j$xVMqnNL z|BzkaI`cnV0*3s?{wPZO&G~;r0Hl0=!!3#wtyM3rg4XxSfoIbpPGx4%PkGL>{ABAf z^Z3i#oudWn9)QcBZ0K$|?XVrt{<|68{@&cX^(zdTKHLgP)F=q>x4YXzdfjU;>S9N_ zDFwK$!cXEZ}*-kgRO?XMO-cG)0m>b6;f|bNVzuF%qce6TvUG)uO9#_dw;IuUT?8 zC^D8on5i4nizcZu$-EpErDA8PozfX_ettgQ@rW+Pi*Nd(r+>1W#82El^ThIe892Gr z9GVCg^E~ve1jq4_#ZRaR8u=FV8YNV3EKm2`ZkH2D(|E3ua;(D@2r5 z)C^kX2U->2GB-4RKAxA(?^^ZKvnc-R@JdHym;Kk=1OivM0Y>M+^2WZP9R84}TR+ll zsW*XTM**rv+basR;E~4aMa(fr9N-*x$gFBIyDpBG)Z9p%kWh8b6P-D+-}4pgB(vKC zZ_EfhMD-#j`UAg!&m;&Z@SC%FF#l}9UDxlqK5B3o7kv`i)I7CXD>#}|O)3oS!=L?-R@i80JKR4O%B zs5CA{d()rVDoj%6>fL$&T(?98*8u;~sW~yXC^gPI)5>jy!Tx@9WPXOA84e--`q~%F z;QOlWp~-qnuL5h>4nFj5^>FBiUpb!K##?sDNn7@@QMv|~!sQ{gYkCFRa-fca+6!5i zV@|XQF99w{3d$~b4pLpw!H*)BSCyt(E|(SWnr<2wIHWUwu{gFor$mK=)czv>YsE$D z>z5Vx?M|O(vo=W42Wwrc+6sdPwb9V7@tE7@7UdYs()GDKAx*=bsk>jr_bBU&UB6c65*xs@J8!zp zjmsgnjBt~`RcXJW*C_8}{xcj))AZN*1dp$u_#h-2z}$~MiY0w{yL0rmdTGWoC^P!$Giiq4 zVdqHJfN)8Dz@z?iY^-mn9NaMrBa8FS1zlC?95v@zO+!-i!M-`Fd?lFu*Mqut@*@K@M;kAehtl&Fp5O|`#q_ROwtemQ-K+}3{J zN$ZginQSdqHtqmMAhVy(mfMhOuv&DGOM3d!mV1RrkB@&0RCq9K%^2xXum_QNWVH1W&xc}Y2RZ>b5r@@qm;$~k9Fp2QpJ zlGQP=eua+?@m=R~W;i4k2bGqDO2W+G<`R@|<|YU%7A}p&!ErcK69fv1 zm4KS8qTo-QWsMF)vnPw8c{EEqykD=5j>M_=*j5KNLDt{gH>eLt;jd)Y0^bb}M7`8eM+SW}Dc)!B-^16=~`5LSJ; zV!)MP3^@M<=g(pI`8J=JSv(ey}ctJHQ?x$WH9nFH|kk+*I?lp8A)Upz5>dNZv5oyBW<5_e$OyQeA{=q$Hdt9_0$J@I#AOBx}m(3 zfxoEV-)IZwfIvEnz+;uv0D;I^7cg&RK`KLKK!Gdx7PufF60Ax?(+~Ee(NXo&*>?;4 zlE#mQ#{IDz5bZuB5L=u+^jue0x`ILK7jhoZSBhSiFK8!czAyUvcvEMg0$MSGq-LZ> z;}^2n;mNA2$VfXz=dk#sWNdMMCN@_3Nt^;j{f9*zJU8TNsxCtw8q-|Bqngg`rq|w4 zeJSvC&Vx`l2AbU$P8Bbq6h;s9nh!RMa0dplx@d_deY(EmWl1NWLf-}7)?S}`C=t_% zSNb0jla6P^Zocl=xg!?v$asu2Mbv#8-be*Vx6q*j{IB9W{D19C|GP5*>-ay#^(DWa z|KTu6l*Fe0ZwP+(|ApoEIM!;{{6FrU^{uSuJa+fkF9~4}JZ|lXPh`kI2}8r?U~t*_=?6xg9O^(?@($4xg?JoZjtQe_f6RY;R}{GPLFsv&Ivi^ea^d%I6b zu?DGDoMVefe2_kL+T&ds{?_rwOvtxr=Y_}37l^V1(PZsbT9#s)CJs-eN8APX`n7iaC?jcwDA1eb<@F0R%(&4SuaC`fyB^MuTnZ&@u}n5G?cyRYtU5t?Ah7qqvY zFG|In*Mjyv?E!BuG>bFHpN3CGl0{Ux*w%E{(k`DDBh+l^iH0@od4-QS&a;(DH!M{o zX<@_+7P7~R9uO_sV)i_6e?023l%F|KK6=Qr&1tgF$>>#l+`|M39=W(-HA5p$hv#`= zP6n|-5m(RO^)6bfh&6k~&A#A)?TWAuroM2uW~qC`bM}-E-aMsF^OBbEHMc!+52u-gql5xBcA#$JKY_n9|F0#6 z{%IfoVEs?|vAO==2z-zIKPLvi&J3=*9#}8`kAxz=&i}&^o9};Z6h6oQEjj?eR*tpm zwfG=pY@HrDjKz5xr4fsL1%2w>w8aI?-<1uFC9=dhIEN-op=s?=7&TuOXjS zy~oO4Oj}q2^+ulxJLu5cCsw3iC(yHS7O!#WP%}r3<~dpaj?PGFVt>Q_DNsbuu2edF z&7E0v*uj0-FEFYvp1gsdIb0V;mF=iXWU7W$BWCEg$`)yuGiSTBhjPy{nGT(6#$dUJfgOie%rueqSxW z;FpW92H6Ce3{}y#)FzMnkehxb=Hekk^uETbsc|y_ z?O=%|+cYMH>D-H{q2BTyyECsfUJR@>lA6W`2WWA6CIn}=-$(jRj-YsTBs9rQcK6WL5}#vl$ky!BMPo;7zYX2jXU6|^U$E-y8Ug_^uj>URpM z_Y-C|a;NTGR`~ss<|5Klf?)>?^G=54MGLuhjLwQW@%<_ryi^&EuE;Pd(+;T=&{+T{ zH0c#T4C$Up{>6Uxz4wK>?ddZk2uDp7TPY#>ox5ZuinQ|IR?$edL&%Y3YSVYbdupB< zFYxpY467|gWbKv$8pj4k9cFDlY9x}n9OEY z^EZw;{nK)eGb&T>3(v+)yKue`fBD>yUo*1p>GQ#7&YlvfTpvddr!`-3&EGwYn0I5| zHB;@nAnuSij#o4zb+?`-bn1^i*xwbRWpk=XIC(~qAC6(sUR0DhsTZOw_=ym%$70&z zUwW+NRiO>txa)DU>(UTC-gZGQOPpBuak0yNNx{)&XaJA1&rC^_6SzAx*YdhW(Qj@X z<&b+sZ_OVmI7(TSkF7czXw5SX`qSyqicOPr+Bv@prHc*a*x#i8+AB?sjl0TAN+vD= z8D2DbIRh_Y;{jAEsRBtVE?@zbq*QQ{i*#ybCAdvGpt90cCAE@z)c$n z&?2IM7VCyB^tsfQ7O`S^PouScaec8amG`W#xQF`un$FxLfK;VE`SAPRcmI!@aOcjP zIddj+&Y3%Brun7qo;Rb`UMu@@KI_547wM?$zLxlz3Kq-Wwql5uCFq$#_Kl#t&Gi-Y zg2+ufY6K_c3kNF_9?4H*Q$vGGRb|l!Wn1G*$~K7CjTCR1_d)slso(s)n$<6U_41mK z^u-zPFVqkT2^k9q%+%YBU%&aC9SMD-GjD0^jveoRX=iXyoofT|`P#$3cIM=r^QuOs zix)b@f1XSTqa{&0wuWTI3MH>)tm?C(Us*~~acTL;ii8g~Y^i*~FL8mx)ylT=qhBi6 zrOrP#mhExc1DiK2H7uTPOBl^*FbYMta*bA8=oPEvQ`!DARsORJzc5y`tsYo3$o<2Y zAJ(G%UEzh+Z1~8cWhyYO7EA-PB^kJs;y`PurNn7C<)Owjm{R*{AW1(EQ#v)iz?1zv zqi&zd{^JTMsl=bw{C7M5=L!6}%>VfUK4brT2krL14|KQtp0TCc?EisU(JG+bUh>*1 z$D4NC?qc?GS1EH7o41bhe#*pOjbEshF?@uH>!kn{p)Yy#3orWkb{YNf*22 z$E&b1mz-moAdid3tG2lOI@z4Iq5ns}toJ(PcH323*StmK6cqCFHbwF=(mp`a0~N=eFsz>ydg{GSmbv6;c#UFNLa*AxuXgR4sH;-1ICc7rX!`*vYvqI? z6}KOv8f*JmK8Ch0{aFC7y;i!&S);91$^_xL+`r}>U3n~RYv^0of(s7cUEG;=Zh3Oy z!2yT3F8%Hb_Ak%h+sE{N1n}`VIdW{k$LgHbi+u=zukNSXPq|+dCm#4nJpHnI%i3cT zKXHi&cO6uG;_4#?e0e)Ai;%=@U@`mu zdl`=$|IZrm*9A|t{>MZ6|M~s`CjPTm@TC6l7^KfuM|b+4&qL=w36S#V^SLPg*PqMv zW%Pe<;NAGR2)5Hm-|!GHQ;#kkGbK>kU*ZBz0x=Yp{% zZ2>P|UNqs17B_Qu$fD)XMf%SlPa6L=BmS&)bhrM` zr{n+p{Gk8GhgJaje||h>|9`LG5#!$lUG(_(@EL^Gv^b9${{oPKsj@j14FHEf&o|J| z*P;Pj2SguawQB%7hk-o)s+}7bu-Y|%(+zalh3w*&vi_g}`m&G)s4hsXuP$k7YSLF{ z>g)6LdcA?gGZaV-27|GHWh{?2)@K@x#+F2v7HvpNO=62759*56nvm9-=+@R&Q)+>! zJkQirW73zH^yMafjmc1GGL)DMeC$!yk}%?7jCXg0T+%_cJg20adF>(+T;=-nnV zdC!}h=L}Yo$-xck37y5_OWW4ik5D^DdcL=Kyz{r)GgsakKOpkjODjIRIj-NtJ9qt( zPqQ!f3I6Ksx}tIC;|DBR;D=)q_WKOEAGjmBG2ChVc&9~np?#^#CAL6t0nD zQk=tfilPva91THHxC%Z2@j5Dg2d`b|OSzw!K;^WS>~ zPwf95dIj6v{Lf$D-=Y6~1@!(;9&`S0uV4aH`Lb97s$Urh&jN(QY1I)@zE2c;H} za{4ABE*V7NDqI5%PJEU`K}bOiA;nc%nqv&sjuN%$(DF!hbct}pLz0yT5KcnE^t?6* z;LsMqI&7BB7g4ZQW2Na0NDc77D8#u2kZM%U6SAuV3TWlpi*iuON43AR0fpuaQBEhJ z$f$Hk6E-r3P$+C5j76N&?}t6M+!K3ll|Ap~*4A zL=c~x7(Y8Hi~~Rtj@Y85_;?uuIV7eep&Vs6g%JvEd&^T`*J>eq3K>YpvT@kOQk=*} z_8)o-YTcjgJ_V^tv$zOUCrJhYLJm|U-5rBL=*AyiPlJRaAFH-*dd2FXc64swX3bYSmXt6B_g z*NR{&5UQc{rF3;2>1-!pJuV-(OE4|&juIp|6dNv<6AJX33A7%xELMrjNZuqJ7y?H+y;*_c#^$e>Gx%b{qMWHv6g>S~k6B6d@?c(ia0XI%E(NAd2kuq@ zSy_{D++jPcVt|h+O%@K8bRm_YZ71Xrn5BmOhI@j%d^J==`2c-FcGU{#7mDdh-L2e0 z%GUqHuv0TVY>jJA_FQ7C3eraZ&|hpPT96cO6JlqfTe3fG47WvSRF+0%X<(L0fkOf` z8BT{ZXLwjymx(1hch85r1XgFskJ#y25h1fx6Kqq|(>gk{6YsDxX%=qd2uW1T=knXz lPJcXav%`P^0|pEjFkrxd0RsjM7%=EX{0^uzbfy4!001CPd#eBd literal 0 HcmV?d00001 diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb new file mode 100644 index 000000000..db0029017 --- /dev/null +++ b/test/functional/repositories_mercurial_controller_test.rb @@ -0,0 +1,117 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'repositories_controller' + +# Re-raise errors caught by the controller. +class RepositoriesController; def rescue_action(e) raise e end; end + +class RepositoriesMercurialControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' + + def setup + @controller = RepositoriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH) + end + + def test_show + get :show, :id => 3 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_not_nil assigns(:changesets) + end + + def test_browse_root + get :browse, :id => 3 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal 3, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} + end + + def test_browse_directory + get :browse, :id => 3, :path => ['images'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal 2, assigns(:entries).size + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'images/edit.png', entry.path + end + + def test_changes + get :changes, :id => 3, :path => ['images', 'edit.png'] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'edit.png' + end + + def test_entry_show + get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'] + assert_response :success + assert_template 'entry' + # Line 19 + assert_tag :tag => 'th', + :content => /10/, + :attributes => { :class => /line-num/ }, + :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } + end + + def test_entry_download + get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw' + assert_response :success + # File content + assert @response.body.include?('WITHOUT ANY WARRANTY') + end + + def test_diff + # Full diff of changeset 4 + get :diff, :id => 3, :rev => 4 + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', + :content => /22/, + :sibling => { :tag => 'td', + :attributes => { :class => /diff_out/ }, + :content => /def remove/ } + end + + def test_annotate + get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb'] + assert_response :success + assert_template 'annotate' + # Line 23, revision 4 + assert_tag :tag => 'th', :content => /23/, + :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /4/ } }, + :sibling => { :tag => 'td', :content => /jsmith/ }, + :sibling => { :tag => 'td', :content => /watcher =/ } + end +end diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb new file mode 100644 index 000000000..e6cfdf9b2 --- /dev/null +++ b/test/unit/repository_mercurial_test.rb @@ -0,0 +1,55 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class RepositoryMercurialTest < Test::Unit::TestCase + fixtures :projects + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' + + def setup + @project = Project.find(1) + assert @repository = Repository::Mercurial.create(:project => @project, :url => REPOSITORY_PATH) + end + + if File.directory?(REPOSITORY_PATH) + def test_fetch_changesets_from_scratch + @repository.fetch_changesets + @repository.reload + + assert_equal 6, @repository.changesets.count + assert_equal 11, @repository.changes.count + assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision(0).comments + end + + def test_fetch_changesets_incremental + @repository.fetch_changesets + # Remove changesets with revision > 2 + @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy) + @repository.reload + assert_equal 3, @repository.changesets.count + + @repository.fetch_changesets + assert_equal 6, @repository.changesets.count + end + else + puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!" + def test_fake; assert true end + end +end From e69631f26c46efe50a0faa800ccd47f33535fd8e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 17:47:30 +0000 Subject: [PATCH 069/710] Fixed: missing body closing tag in repository annotate and entry views. git-svn-id: http://redmine.rubyforge.org/svn/trunk@992 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/annotate.rhtml | 2 +- app/views/repositories/entry.rhtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/repositories/annotate.rhtml b/app/views/repositories/annotate.rhtml index b8f481ae5..9a3fc834b 100644 --- a/app/views/repositories/annotate.rhtml +++ b/app/views/repositories/annotate.rhtml @@ -17,7 +17,7 @@ <% line_num += 1 %> <% end %> - + diff --git a/app/views/repositories/entry.rhtml b/app/views/repositories/entry.rhtml index 9927601d7..41565a232 100644 --- a/app/views/repositories/entry.rhtml +++ b/app/views/repositories/entry.rhtml @@ -11,7 +11,7 @@ <% line_num += 1 %> <% end %> - + From 38b185f1dc4d296eb49c94a3ccff7496b1367c84 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 17:48:11 +0000 Subject: [PATCH 070/710] Fixed: empty lines when displaying repository files with Windows style eol. git-svn-id: http://redmine.rubyforge.org/svn/trunk@993 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index dfd5d0a6f..ef332eb37 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -92,6 +92,9 @@ class RepositoriesController < ApplicationController show_error and return unless @content if 'raw' == params[:format] send_data @content, :filename => @path.split('/').last + else + # Prevent empty lines when displaying a file with Windows style eol + @content.gsub!("\r\n", "\n") end end From 58610ec52af2249c4c5eebf35e11cd827a7966ab Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 18:54:55 +0000 Subject: [PATCH 071/710] Search engine: issue custom fields can now be searched. Each issue custom field (excepting numeric, date and boolean fields) can be marked as "Searchable" (default to false). git-svn-id: http://redmine.rubyforge.org/svn/trunk@994 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/custom_field.rb | 2 ++ app/views/custom_fields/_form.rhtml | 17 ++++++++++---- .../086_add_custom_fields_searchable.rb | 9 ++++++++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/fixtures/custom_fields.yml | 5 ++-- test/fixtures/custom_values.yml | 9 +++++++- test/functional/search_controller_test.rb | 15 ++++++++++-- .../lib/acts_as_searchable.rb | 23 +++++++++++++++++-- 27 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 db/migrate/086_add_custom_fields_searchable.rb diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index c3b5f2a9f..e1fd8666d 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -43,6 +43,8 @@ class CustomField < ActiveRecord::Base def before_validation # remove empty values self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact + # make sure these fields are not searchable + self.searchable = false if %w(int float date bool).include?(field_format) end def validate diff --git a/app/views/custom_fields/_form.rhtml b/app/views/custom_fields/_form.rhtml index 013be9b70..915daab32 100644 --- a/app/views/custom_fields/_form.rhtml +++ b/app/views/custom_fields/_form.rhtml @@ -7,21 +7,32 @@ function toggle_custom_field_format() { p_length = $("custom_field_min_length"); p_regexp = $("custom_field_regexp"); p_values = $("custom_field_possible_values"); + p_searchable = $("custom_field_searchable"); switch (format.value) { case "list": Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); + Element.show(p_searchable.parentNode); Element.show(p_values); break; case "date": case "bool": Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); + Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "float": + case "int": + Element.show(p_length.parentNode); + Element.show(p_regexp.parentNode); + Element.hide(p_searchable.parentNode); Element.hide(p_values); break; default: Element.show(p_length.parentNode); Element.show(p_regexp.parentNode); + Element.show(p_searchable.parentNode); Element.hide(p_values); break; } @@ -47,7 +58,6 @@ function deleteValueField(e) { //]]> -

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

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

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

    -<%= javascript_tag "toggle_custom_field_format();" %> -
    <% case @custom_field.type.to_s @@ -78,6 +85,7 @@ when "IssueCustomField" %>

    <%= f.check_box :is_required %>

    <%= f.check_box :is_for_all %>

    <%= f.check_box :is_filter %>

    +

    <%= f.check_box :searchable %>

    <% when "UserCustomField" %>

    <%= f.check_box :is_required %>

    @@ -87,3 +95,4 @@ when "IssueCustomField" %> <% end %>
    +<%= javascript_tag "toggle_custom_field_format();" %> diff --git a/db/migrate/086_add_custom_fields_searchable.rb b/db/migrate/086_add_custom_fields_searchable.rb new file mode 100644 index 000000000..53158d14e --- /dev/null +++ b/db/migrate/086_add_custom_fields_searchable.rb @@ -0,0 +1,9 @@ +class AddCustomFieldsSearchable < ActiveRecord::Migration + def self.up + add_column :custom_fields, :searchable, :boolean, :default => false + end + + def self.down + remove_column :custom_fields, :searchable + end +end diff --git a/lang/bg.yml b/lang/bg.yml index 590952681..d9bcd6c7c 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/cs.yml b/lang/cs.yml index 816f9b92e..fe29defe6 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/de.yml b/lang/de.yml index 046ed9994..c9cdc0c12 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Muss mindestens %d Zeichen lang sein. setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/en.yml b/lang/en.yml index 104e7fe6e..116935f73 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -170,6 +170,7 @@ field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time field_column_names: Columns field_time_zone: Time zone +field_searchable: Searchable setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/lang/es.yml b/lang/es.yml index d806d066e..50c652f2c 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -552,3 +552,4 @@ setting_time_format: Formato de hora setting_bcc_recipients: Blind carbon copy recipients (bcc) button_annotate: Annotate label_issues_by: Issues by %s +field_searchable: Searchable diff --git a/lang/fr.yml b/lang/fr.yml index 36ccc463f..d6ad5da56 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -170,6 +170,7 @@ field_redirect_existing_links: Rediriger les liens existants field_estimated_hours: Temps estimé field_column_names: Colonnes field_time_zone: Fuseau horaire +field_searchable: Utilisé pour les recherches setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/lang/he.yml b/lang/he.yml index 6be1a4c77..0ed57e527 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/it.yml b/lang/it.yml index 8a3e954f4..d266f797e 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/ja.yml b/lang/ja.yml index 1ecfb1ae9..bfe120cfe 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -550,3 +550,4 @@ 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 diff --git a/lang/ko.yml b/lang/ko.yml index ef081e622..76debf345 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/nl.yml b/lang/nl.yml index 24a343eb3..a8b5cc64a 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -550,3 +550,4 @@ 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 diff --git a/lang/pl.yml b/lang/pl.yml index 222c7ea9d..5fa50ec51 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -549,3 +549,4 @@ text_caracters_minimum: Musi być nie krótsze niż %d znaków. setting_bcc_recipients: Odbiorcy kopii tajnej (kt/bcc) button_annotate: Adnotuj label_issues_by: Zagadnienia wprowadzone przez %s +field_searchable: Searchable diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 8c903edd4..ed31915dd 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/pt.yml b/lang/pt.yml index 10de07b5f..72ba4d2b0 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/ro.yml b/lang/ro.yml index f7d3acd56..6f1eb7810 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/ru.yml b/lang/ru.yml index cad357c0c..c7697ae1a 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -549,3 +549,4 @@ 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 diff --git a/lang/sr.yml b/lang/sr.yml index f9008d890..b5e9ecfe3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -550,3 +550,4 @@ 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 diff --git a/lang/sv.yml b/lang/sv.yml index a4f55a17a..d789a2a37 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -550,3 +550,4 @@ 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index af0fd3ed4..4d730c1a9 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -549,3 +549,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) +field_searchable: Searchable diff --git a/lang/zh.yml b/lang/zh.yml index de4ae1ece..6876daa17 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -552,3 +552,4 @@ 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 diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index ce7509fe9..e73e6de96 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -11,16 +11,17 @@ custom_fields_001: is_required: false field_format: list custom_fields_002: - name: Build + name: Searchable field min_length: 1 regexp: "" is_for_all: true type: IssueCustomField - max_length: 10 + max_length: 100 possible_values: "" id: 2 is_required: false field_format: string + searchable: true custom_fields_003: name: Development status min_length: 0 diff --git a/test/fixtures/custom_values.yml b/test/fixtures/custom_values.yml index b71227971..572142889 100644 --- a/test/fixtures/custom_values.yml +++ b/test/fixtures/custom_values.yml @@ -46,4 +46,11 @@ custom_values_008: custom_field_id: 1 customized_id: 3 id: 11 - value: "MySQL" \ No newline at end of file + value: "MySQL" +custom_values_009: + customized_type: Issue + custom_field_id: 2 + customized_id: 3 + id: 12 + value: "this is a stringforcustomfield search" + \ No newline at end of file diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index 5e3673a4e..330cd0de0 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -5,7 +5,7 @@ require 'search_controller' class SearchController; def rescue_action(e) raise e end; end class SearchControllerTest < Test::Unit::TestCase - fixtures :projects, :issues + fixtures :projects, :issues, :custom_fields, :custom_values def setup @controller = SearchController.new @@ -25,7 +25,9 @@ class SearchControllerTest < Test::Unit::TestCase assert assigns(:results).include?(Project.find(1)) end - def test_search_in_project + def test_search_without_searchable_custom_fields + CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" + get :index, :id => 1 assert_response :success assert_template 'index' @@ -36,6 +38,15 @@ class SearchControllerTest < Test::Unit::TestCase assert_template 'index' end + def test_search_with_searchable_custom_fields + get :index, :id => 1, :q => "stringforcustomfield" + assert_response :success + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(3)) + end + def test_quick_jump_to_issue # issue of a public project get :index, :q => "3" diff --git a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb index e2a323abb..1dd88978c 100644 --- a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -49,6 +49,9 @@ module Redmine raise 'No date column defined defined.' end + # Should we search custom fields on this model ? + searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil? + send :include, Redmine::Acts::Searchable::InstanceMethods end end @@ -67,11 +70,27 @@ module Redmine columns = searchable_options[:columns] columns.slice!(1..-1) if options[:titles_only] - sql = ([ '(' + columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}.join(' OR ') + ')' ] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"} + + if !options[:titles_only] && searchable_options[:search_custom_fields] + searchable_custom_field_ids = CustomField.find(:all, + :select => 'id', + :conditions => { :type => "#{self.name}CustomField", + :searchable => true }).collect(&:id) + if searchable_custom_field_ids.any? + custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" + + " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" + + " AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))" + token_clauses << custom_field_sql + end + end + + sql = ([token_clauses.join(' OR ')] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + if options[:offset] sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')" end - find_options[:conditions] = [sql, * (tokens * columns.size).sort] + 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) From d1a3fbea4087bdf5bb06b2bf231a2fe9bda65201 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 21:31:30 +0000 Subject: [PATCH 072/710] Fixed Trac importer error with Rails 2.0: readonly? is defined by ActiveRecord. git-svn-id: http://redmine.rubyforge.org/svn/trunk@995 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 37514df01..828027b87 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -145,6 +145,11 @@ namespace :redmine do class TracWikiPage < ActiveRecord::Base set_table_name :wiki + + def self.columns + # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0) + super.select {|column| column.name.to_s != 'readonly'} + end end class TracPermission < ActiveRecord::Base From 777edc13c1378810371631c3c897c11f314db7c1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 21:47:57 +0000 Subject: [PATCH 073/710] Fixed: can not save numeric, date and boolean custom fields (broken by r994). git-svn-id: http://redmine.rubyforge.org/svn/trunk@996 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/custom_field.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index e1fd8666d..5a134c4ec 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -45,6 +45,7 @@ class CustomField < ActiveRecord::Base self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact # make sure these fields are not searchable self.searchable = false if %w(int float date bool).include?(field_format) + true end def validate From 963b1283c2881b0b9fae4c5b2154f835dd4761c6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 21:49:23 +0000 Subject: [PATCH 074/710] Fixed Bazaar test repository path. git-svn-id: http://redmine.rubyforge.org/svn/trunk@997 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/unit/repository_bazaar_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb index 6b21607bc..15fcc8672 100644 --- a/test/unit/repository_bazaar_test.rb +++ b/test/unit/repository_bazaar_test.rb @@ -22,7 +22,8 @@ class RepositoryBazaarTest < Test::Unit::TestCase # No '..' in the repository path REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository' - + REPOSITORY_PATH.gsub!(/\/+/, '/') + def setup @project = Project.find(1) assert @repository = Repository::Bazaar.create(:project => @project, :url => "file:///#{REPOSITORY_PATH}") From 27d6334afaceb95a4fdad4a3691cf22429a8e64e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 14 Dec 2007 22:00:03 +0000 Subject: [PATCH 075/710] Fixed: Unable to create a wiki (Rails 2.0 compatibility). git-svn-id: http://redmine.rubyforge.org/svn/trunk@998 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/wikis_controller.rb | 2 +- test/fixtures/enabled_modules.yml | 4 +++ test/functional/wikis_controller_test.rb | 43 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/functional/wikis_controller_test.rb diff --git a/app/controllers/wikis_controller.rb b/app/controllers/wikis_controller.rb index 146aaac8a..a222570ef 100644 --- a/app/controllers/wikis_controller.rb +++ b/app/controllers/wikis_controller.rb @@ -23,7 +23,7 @@ class WikisController < ApplicationController def edit @wiki = @project.wiki || Wiki.new(:project => @project) @wiki.attributes = params[:wiki] - @wiki.save if @request.post? + @wiki.save if request.post? render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'} end diff --git a/test/fixtures/enabled_modules.yml b/test/fixtures/enabled_modules.yml index dfc2f0090..8d1565534 100644 --- a/test/fixtures/enabled_modules.yml +++ b/test/fixtures/enabled_modules.yml @@ -35,4 +35,8 @@ enabled_modules_009: name: repository project_id: 3 id: 9 +enabled_modules_010: + name: wiki + project_id: 3 + id: 10 \ No newline at end of file diff --git a/test/functional/wikis_controller_test.rb b/test/functional/wikis_controller_test.rb new file mode 100644 index 000000000..a343fdaaf --- /dev/null +++ b/test/functional/wikis_controller_test.rb @@ -0,0 +1,43 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'wikis_controller' + +# Re-raise errors caught by the controller. +class WikisController; def rescue_action(e) raise e end; end + +class WikisControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis + + def setup + @controller = WikisController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_create_wiki + @request.session[:user_id] = 1 + assert_nil Project.find(3).wiki + post :edit, :id => 3, :wiki => { :start_page => 'Start page' } + assert_response :success + wiki = Project.find(3).wiki + assert_not_nil wiki + assert_equal 'Start page', wiki.start_page + end +end From 361c50c1f410ce575631bfb59cfdcdf5353d331b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 15 Dec 2007 11:57:06 +0000 Subject: [PATCH 076/710] Added some functional tests (wiki). git-svn-id: http://redmine.rubyforge.org/svn/trunk@999 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/fixtures/roles.yml | 2 +- test/fixtures/wiki_content_versions.yml | 14 ++- test/fixtures/wiki_contents.yml | 12 ++ test/fixtures/wiki_pages.yml | 6 + test/functional/wiki_controller_test.rb | 145 +++++++++++++++++++++++ test/functional/wikis_controller_test.rb | 15 ++- 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 test/functional/wiki_controller_test.rb diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index 70490286f..a089a98f9 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -39,7 +39,6 @@ roles_005: - :view_time_entries - :view_documents - :view_wiki_pages - - :edit_wiki_pages - :view_files - :browse_repository - :view_changesets @@ -75,6 +74,7 @@ roles_001: - :view_wiki_pages - :edit_wiki_pages - :delete_wiki_pages + - :rename_wiki_pages - :add_messages - :edit_messages - :delete_messages diff --git a/test/fixtures/wiki_content_versions.yml b/test/fixtures/wiki_content_versions.yml index c433fc5dd..547030ccf 100644 --- a/test/fixtures/wiki_content_versions.yml +++ b/test/fixtures/wiki_content_versions.yml @@ -37,4 +37,16 @@ wiki_content_versions_003: data: |- h1. CookBook documentation Some updated [[documentation]] here... - +wiki_content_versions_004: + data: |- + h1. Another page + + This is a link to a ticket: #2 + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 2 + wiki_content_id: 2 + id: 4 + version: 1 + author_id: 1 + comments: + diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml index 1f4ffc36d..a230b9c08 100644 --- a/test/fixtures/wiki_contents.yml +++ b/test/fixtures/wiki_contents.yml @@ -10,3 +10,15 @@ wiki_contents_001: version: 3 author_id: 1 comments: Gzip compression activated +wiki_contents_002: + text: |- + h1. Another page + + This is a link to a ticket: #2 + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 2 + id: 2 + version: 1 + author_id: 1 + comments: + \ No newline at end of file diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml index d488add26..ca9d6f5dc 100644 --- a/test/fixtures/wiki_pages.yml +++ b/test/fixtures/wiki_pages.yml @@ -4,3 +4,9 @@ wiki_pages_001: title: CookBook_documentation id: 1 wiki_id: 1 +wiki_pages_002: + created_on: 2007-03-08 00:18:07 +01:00 + title: Another_page + id: 2 + wiki_id: 1 + \ No newline at end of file diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb new file mode 100644 index 000000000..6ee5ab276 --- /dev/null +++ b/test/functional/wiki_controller_test.rb @@ -0,0 +1,145 @@ +# redMine - project management software +# Copyright (C) 2006-2007 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'wiki_controller' + +# Re-raise errors caught by the controller. +class WikiController; def rescue_action(e) raise e end; end + +class WikiControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions + + def setup + @controller = WikiController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show_start_page + get :index, :id => 1 + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /CookBook documentation/ + end + + def test_show_page_with_name + get :index, :id => 1, :page => 'Another_page' + assert_response :success + assert_template 'show' + assert_tag :tag => 'h1', :content => /Another page/ + end + + def test_show_unexistent_page_without_edit_right + get :index, :id => 1, :page => 'Unexistent page' + assert_response 404 + end + + def test_show_unexistent_page_with_edit_right + @request.session[:user_id] = 2 + get :index, :id => 1, :page => 'Unexistent page' + assert_response :success + assert_template 'edit' + end + + def test_create_page + @request.session[:user_id] = 2 + post :edit, :id => 1, + :page => 'New page', + :content => {:comments => 'Created the page', + :text => "h1. New page\n\nThis is a new page", + :version => 0} + assert_redirected_to 'wiki/1/New_page' + page = Project.find(1).wiki.find_page('New page') + assert !page.new_record? + assert_not_nil page.content + assert_equal 'Created the page', page.content.comments + end + + def test_preview + @request.session[:user_id] = 2 + xhr :post, :preview, :id => 1, :page => 'CookBook_documentation', + :content => { :comments => '', + :text => 'this is a *previewed text*', + :version => 3 } + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'strong', :content => /previewed text/ + end + + def test_history + get :history, :id => 1, :page => 'CookBook_documentation' + assert_response :success + assert_template 'history' + assert_not_nil assigns(:versions) + assert_equal 3, assigns(:versions).size + end + + def test_diff + get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1 + assert_response :success + assert_template 'diff' + assert_tag :tag => 'span', :attributes => { :class => 'diff_in'}, + :content => /updated/ + end + + def test_rename_with_redirect + @request.session[:user_id] = 2 + post :rename, :id => 1, :page => 'Another_page', + :wiki_page => { :title => 'Another renamed page', + :redirect_existing_links => 1 } + assert_redirected_to 'wiki/1/Another_renamed_page' + wiki = Project.find(1).wiki + # Check redirects + assert_not_nil wiki.find_page('Another page') + assert_nil wiki.find_page('Another page', :with_redirect => false) + end + + def test_rename_without_redirect + @request.session[:user_id] = 2 + post :rename, :id => 1, :page => 'Another_page', + :wiki_page => { :title => 'Another renamed page', + :redirect_existing_links => "0" } + assert_redirected_to 'wiki/1/Another_renamed_page' + wiki = Project.find(1).wiki + # Check that there's no redirects + assert_nil wiki.find_page('Another page') + end + + def test_destroy + @request.session[:user_id] = 2 + post :destroy, :id => 1, :page => 'CookBook_documentation' + assert_redirected_to 'wiki/1/Page_index/special' + end + + def test_page_index + get :special, :id => 1, :page => 'Page_index' + assert_response :success + assert_template 'special_page_index' + pages = assigns(:pages) + assert_not_nil pages + assert_equal 2, pages.size + assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation' }, + :content => /CookBook documentation/ + end + + def test_not_found + get :index, :id => 999 + assert_response 404 + end +end diff --git a/test/functional/wikis_controller_test.rb b/test/functional/wikis_controller_test.rb index a343fdaaf..93ad7e32d 100644 --- a/test/functional/wikis_controller_test.rb +++ b/test/functional/wikis_controller_test.rb @@ -31,7 +31,7 @@ class WikisControllerTest < Test::Unit::TestCase User.current = nil end - def test_create_wiki + def test_create @request.session[:user_id] = 1 assert_nil Project.find(3).wiki post :edit, :id => 3, :wiki => { :start_page => 'Start page' } @@ -40,4 +40,17 @@ class WikisControllerTest < Test::Unit::TestCase assert_not_nil wiki assert_equal 'Start page', wiki.start_page end + + def test_destroy + @request.session[:user_id] = 1 + post :destroy, :id => 1, :confirm => 1 + assert_redirected_to 'projects/settings/1' + assert_nil Project.find(1).wiki + end + + def test_not_found + @request.session[:user_id] = 1 + post :destroy, :id => 999, :confirm => 1 + assert_response 404 + end end From 124ef65c1fd2c7174452ef80ebb52d4cdc99e80a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 15 Dec 2007 11:57:48 +0000 Subject: [PATCH 077/710] Fixed warning: toplevel constant User referenced by WikiContent::User git-svn-id: http://redmine.rubyforge.org/svn/trunk@1000 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/wiki_content.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index 4b60a4373..d0a48467b 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -25,8 +25,8 @@ class WikiContent < ActiveRecord::Base acts_as_versioned class Version - belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id' - belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id' + belongs_to :author, :class_name => '::User', :foreign_key => 'author_id' attr_protected :data acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"}, From ea35fff5bf4dde17fe67320118a9b73da31a81c2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 15 Dec 2007 12:14:40 +0000 Subject: [PATCH 078/710] SCM adapters: moved Errno::ENOENT exception rescuing to the abstract adapter. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1001 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/abstract_adapter.rb | 12 +++++++++--- lib/redmine/scm/adapters/bazaar_adapter.rb | 14 ++------------ lib/redmine/scm/adapters/cvs_adapter.rb | 10 ---------- lib/redmine/scm/adapters/darcs_adapter.rb | 8 +------- lib/redmine/scm/adapters/mercurial_adapter.rb | 13 +------------ lib/redmine/scm/adapters/subversion_adapter.rb | 12 +----------- 6 files changed, 14 insertions(+), 55 deletions(-) diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 720a4e9d9..c93fc6350 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -112,9 +112,15 @@ module Redmine def shellout(cmd, &block) logger.debug "Shelling out: #{cmd}" if logger && logger.debug? - IO.popen(cmd, "r+") do |io| - io.close_write - block.call(io) if block_given? + begin + IO.popen(cmd, "r+") do |io| + io.close_write + block.call(io) if block_given? + end + rescue Errno::ENOENT => e + # The command failed, log it and re-raise + log.error("SCM command failed: #{cmd}\n with: #{e.message}") + raise CommandFailed end end end diff --git a/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb index 4d7afeacc..11a44b7cf 100644 --- a/lib/redmine/scm/adapters/bazaar_adapter.rb +++ b/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -40,7 +40,7 @@ module Redmine end return nil if $? && $?.exitstatus != 0 info - rescue Errno::ENOENT => e + rescue CommandFailed return nil end @@ -81,8 +81,6 @@ module Redmine return nil if $? && $?.exitstatus != 0 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? entries.sort_by_name - rescue Errno::ENOENT => e - raise CommandFailed end def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) @@ -144,8 +142,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 revisions - rescue Errno::ENOENT => e - raise CommandFailed end def diff(path, identifier_from, identifier_to=nil, type="inline") @@ -163,9 +159,7 @@ module Redmine end end #return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type - rescue Errno::ENOENT => e - raise CommandFailed + DiffTableList.new diff, type end def cat(path, identifier=nil) @@ -179,8 +173,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 cat - rescue Errno::ENOENT => e - raise CommandFailed end def annotate(path, identifier=nil) @@ -198,8 +190,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 blame - rescue Errno::ENOENT => e - raise CommandFailed end end end diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index 5c6c1775b..73dc9b6c4 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -103,8 +103,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 entries.sort_by_name - rescue Errno::ENOENT => e - raise CommandFailed end STARTLOG="----------------------------" @@ -234,8 +232,6 @@ module Redmine end end end - rescue Errno::ENOENT => e - raise CommandFailed end def diff(path, identifier_from, identifier_to=nil, type="inline") @@ -250,8 +246,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 DiffTableList.new diff, type - rescue Errno::ENOENT => e - raise CommandFailed end def cat(path, identifier=nil) @@ -265,8 +259,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 cat - rescue Errno::ENOENT => e - raise CommandFailed end def annotate(path, identifier=nil) @@ -283,8 +275,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 blame - rescue Errno::ENOENT => e - raise CommandFailed end private diff --git a/lib/redmine/scm/adapters/darcs_adapter.rb b/lib/redmine/scm/adapters/darcs_adapter.rb index 34b36202b..2955b26dc 100644 --- a/lib/redmine/scm/adapters/darcs_adapter.rb +++ b/lib/redmine/scm/adapters/darcs_adapter.rb @@ -70,8 +70,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 entries.sort_by_name - rescue Errno::ENOENT => e - raise CommandFailed end def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) @@ -99,8 +97,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 revisions - rescue Errno::ENOENT => e - raise CommandFailed end def diff(path, identifier_from, identifier_to=nil, type="inline") @@ -117,8 +113,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 DiffTableList.new diff, type - rescue Errno::ENOENT => e - raise CommandFailed end private @@ -154,7 +148,7 @@ module Redmine end end paths - rescue Errno::ENOENT => e + rescue CommandFailed paths end end diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 3cbf01f91..1b9cab47c 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -36,7 +36,7 @@ module Redmine :lastrev => revisions(nil,nil,nil,{:limit => 1}).last }) info - rescue Errno::ENOENT => e + rescue CommandFailed return nil end @@ -58,8 +58,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 entries.sort_by_name - rescue Errno::ENOENT => e - raise CommandFailed end def entry(path=nil, identifier=nil) @@ -119,8 +117,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 revisions - rescue Errno::ENOENT => e - raise CommandFailed end def diff(path, identifier_from, identifier_to=nil, type="inline") @@ -140,9 +136,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 DiffTableList.new diff, type - - rescue Errno::ENOENT => e - raise CommandFailed end def cat(path, identifier=nil) @@ -154,8 +147,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 cat - rescue Errno::ENOENT => e - raise CommandFailed end def annotate(path, identifier=nil) @@ -173,8 +164,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 blame - rescue Errno::ENOENT => e - raise CommandFailed end end end diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index d55b8712e..f698f4a62 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -47,7 +47,7 @@ module Redmine end return nil if $? && $?.exitstatus != 0 info - rescue Errno::ENOENT => e + rescue CommandFailed return nil end @@ -91,8 +91,6 @@ module Redmine return nil if $? && $?.exitstatus != 0 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug? entries.sort_by_name - rescue Errno::ENOENT => e - raise CommandFailed end def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) @@ -130,8 +128,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 revisions - rescue Errno::ENOENT => e - raise CommandFailed end def diff(path, identifier_from, identifier_to=nil, type="inline") @@ -154,8 +150,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 DiffTableList.new diff, type - rescue Errno::ENOENT => e - raise CommandFailed end def cat(path, identifier=nil) @@ -169,8 +163,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 cat - rescue Errno::ENOENT => e - raise CommandFailed end def annotate(path, identifier=nil) @@ -186,8 +178,6 @@ module Redmine end return nil if $? && $?.exitstatus != 0 blame - rescue Errno::ENOENT => e - raise CommandFailed end private From 1af9c47a274386beb6f8d2d663cc09c3e798a4d7 Mon Sep 17 00:00:00 2001 From: Nicolas Chuche Date: Sat, 15 Dec 2007 12:23:39 +0000 Subject: [PATCH 079/710] bug when using apache authentication method git-svn-id: http://redmine.rubyforge.org/svn/trunk@1002 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/reposman.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extra/svn/reposman.rb b/extra/svn/reposman.rb index 729970406..0b476cdc4 100755 --- a/extra/svn/reposman.rb +++ b/extra/svn/reposman.rb @@ -78,6 +78,7 @@ $quiet = false $redmine_host = '' $repos_base = '' $svn_owner = 'root' +$use_groupid = true $svn_url = false $test = false @@ -92,7 +93,7 @@ begin case opt when '--svn-dir'; $repos_base = arg.dup when '--redmine-host'; $redmine_host = arg.dup - when '--owner'; $svn_owner = arg.dup + when '--owner'; $svn_owner = arg.dup; $use_groupid = false; when '--url'; $svn_url = arg.dup when '--verbose'; $verbose += 1 when '--test'; $test = true @@ -144,7 +145,7 @@ def set_owner_and_rights(project, repos_path, &block) if RUBY_PLATFORM =~ /mswin/ yield if block_given? else - uid, gid = Etc.getpwnam($svn_owner).uid, Etc.getgrnam(project.identifier).gid + uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : 0) right = project.is_public ? 0775 : 0770 yield if block_given? Find.find(repos_path) do |f| From 2bcd448dda6aee13e71350a886942888fcd05a4f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 16 Dec 2007 13:33:15 +0000 Subject: [PATCH 080/710] Updated Japanese translation (Satoru Kurashiki). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1003 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/ja.yml | 80 ++++++++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/lang/ja.yml b/lang/ja.yml index bfe120cfe..4206f3db6 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -72,13 +72,13 @@ notice_locking_conflict: 別のユーザがデータを更新しています。 notice_scm_error: リポジトリに、エントリ/リビジョンが存在しません。 notice_not_authorized: このページにアクセスするには認証が必要です。 notice_email_sent: %s宛にメールを送信しました。 -notice_email_error: メール送信中にエラーが発生しました (%s) +notice_email_error: メール送信中にエラーが発生しました(%s) notice_feeds_access_key_reseted: RSSアクセスキーを初期化しました。 -mail_subject_lost_password: redMineパスワード +mail_subject_lost_password: Redmineパスワード mail_body_lost_password: 'パスワードを変更するには、以下のリンクをたどってください:' -mail_subject_register: redMineアカウントが有効になりました -mail_body_register: 'Redmine アカウントをアクティブにするには、以下のリンクをたどってください:' +mail_subject_register: Redmineアカウントが有効になりました +mail_body_register: 'Redmineアカウントをアクティブにするには、以下のリンクをたどってください:' gui_validation_error: 1件のエラー gui_validation_error_plural: %d件のエラー @@ -159,8 +159,8 @@ field_identifier: 識別子 field_is_filter: フィルタとして使う field_issue_to_id: 関連する問題 field_delay: 遅延 -field_assignable: Issues can be assigned to this role -field_redirect_existing_links: Redirect existing links +field_assignable: 問題はこのロールに割り当てることができます +field_redirect_existing_links: 既存のリンクをリダイレクトする field_estimated_hours: 予定工数 setting_app_title: アプリケーションのタイトル @@ -410,18 +410,18 @@ label_message_last: 最新のメッセージ label_message_new: 新しいメッセージ label_reply_plural: 返答 label_send_information: アカウント情報をユーザに送信 -label_year: Year -label_month: Month -label_week: Week -label_date_from: From -label_date_to: To +label_year: 年 +label_month: 月 +label_week: 週 +label_date_from: から +label_date_to: まで label_language_based: 既定の言語の設定に従う -label_sort_by: Sort by %s +label_sort_by: %sで並び替え label_send_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_feeds_access_key_created_on: RSSアクセスキーは%s前に作成されました +label_module_plural: モジュール +label_added_time_by: %sが%s前に追加しました +label_updated_time: %s前に更新されました label_jump_to_a_project: プロジェクトへ移動... button_login: ログイン @@ -454,8 +454,8 @@ button_unwatch: ウォッチをやめる button_reply: 返答 button_archive: 書庫に保存 button_unarchive: 書庫から戻す -button_reset: Reset -button_rename: Rename +button_reset: リセット +button_rename: 名前変更 status_active: 有効 status_registered: 登録 @@ -482,10 +482,10 @@ text_comma_separated: (カンマで区切った)複数の値が使えます text_issues_ref_in_commit_messages: コミットメッセージ内で問題の参照/修正 text_issue_added: 問題 %s が報告されました。 text_issue_updated: 問題 %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_wiki_destroy_confirmation: 本当にこのwikiとその内容の全てを削除しますか? +text_issue_category_destroy_question: このカテゴリに割り当て済みの問題(%d)があります。何をしようとしていますか? +text_issue_category_destroy_assignments: カテゴリの割り当てを削除する +text_issue_category_reassign_to: 問題をこのカテゴリに再割り当てする default_role_manager: 管理者 default_role_developper: 開発者 @@ -512,8 +512,8 @@ default_activity_development: 開発作業 enumeration_issue_priorities: 問題の優先度 enumeration_doc_categories: 文書カテゴリ enumeration_activities: 作業分類 (時間トラッキング) -label_file_plural: Files -label_changeset_plural: Changesets +label_file_plural: ファイル +label_changeset_plural: チェンジセット field_column_names: 項目 label_default_columns: 既定の項目 setting_issue_list_default_columns: 問題の一覧で表示する項目 @@ -534,20 +534,20 @@ label_user_mail_option_none: "ウォッチまたは関係している問題の setting_emails_footer: メールのフッタ label_float: 小数 button_copy: コピー -mail_body_account_information_external: You can use your "%s" account to log into Redmine. -mail_body_account_information: Your Redmine 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: Redmine 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 +mail_body_account_information_external: 「%s」アカウントを使ってRedmineにログインできます。 +mail_body_account_information: Redmineアカウント情報 +setting_protocol: プロトコル +label_user_mail_no_self_notified: 自分自身による変更の通知は不要です +setting_time_format: 時刻の形式 +label_registration_activation_by_email: メールでアカウントを有効化 +mail_subject_account_activation_request: Redminアカウントの有効化要求 +mail_body_account_activation_request: 新しいユーザ(%s)が登録しています。このアカウントはあなたの承認待ちです: +label_registration_automatic_activation: 自動でアカウントを有効化 +label_registration_manual_activation: 手動でアカウントを有効化 +notice_account_pending: アカウントは作成済みで、管理者の承認待ちです。 +field_time_zone: タイムゾーン +text_caracters_minimum: 最低%d文字の長さが必要です +setting_bcc_recipients: ブラインドカーボンコピーで受信(bcc) +button_annotate: 注釈 +label_issues_by: %s別の問題 field_searchable: Searchable From b4eafd9ea8132534f75e3065c3d36345610e907a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 16 Dec 2007 14:03:26 +0000 Subject: [PATCH 081/710] Changes for 0.6.2 release. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1004 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/CHANGELOG | 22 ++++++++++++++++++++-- lib/redmine/version.rb | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/doc/CHANGELOG b/doc/CHANGELOG index f5614f6f9..051105a04 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -5,6 +5,24 @@ Copyright (C) 2006-2007 Jean-Philippe Lang http://www.redmine.org/ +== 2007-12-16 v0.6.2 + +* Search engine: issue custom fields can now be searched +* News comments are now textilized +* Updated Japanese translation (Satoru Kurashiki) +* Updated Chinese translation (Shortie Lo) +* Fixed Rails 2.0 compatibility bugs: + * Unable to create a wiki + * Gantt and calendar error + * Trac importer error (readonly? is defined by ActiveRecord) +* Fixed: 'assigned to me' filter broken +* Fixed: crash when validation fails on issue edition with no custom fields +* Fixed: reposman "can't find group" error +* Fixed: 'LDAP account password is too long' error when leaving the field empty on creation +* Fixed: empty lines when displaying repository files with Windows style eol +* Fixed: missing body closing tag in repository annotate and entry views + + == 2007-12-10 v0.6.1 * Rails 2.0 compatibility @@ -26,7 +44,7 @@ http://www.redmine.org/ * Diff style (inline or side by side) automatically saved as a user preference * Added issues status changes on the activity view (by Cyril Mougel) * Added forums topics on the activity view (disabled by default) -* Added an option on 'My account' for users who dont want to be notified of changes that they make +* Added an option on 'My account' for users who don't want to be notified of changes that they make * Trac importer now supports mysql and postgresql databases * Trac importer improvements (by Mat Trudel) * 'fixed version' field can now be displayed on the issue list @@ -61,7 +79,7 @@ http://www.redmine.org/ * Fixed: admin should be able to move issues to any project * Fixed: adding an attachment is not possible when changing the status of an issue * Fixed: No mime-types in documents/files downloading -* Fixed: error when sorting the messages if theres only one board for the project +* Fixed: error when sorting the messages if there's only one board for the project * Fixed: 'me' doesn't appear in the drop down filters on a project issue list. == 2007-11-04 v0.6.0 diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb index dd0f4f82c..b109d098e 100644 --- a/lib/redmine/version.rb +++ b/lib/redmine/version.rb @@ -4,7 +4,7 @@ module Redmine module VERSION #:nodoc: MAJOR = 0 MINOR = 6 - TINY = 1 + TINY = 2 def self.revision revision = nil From 524cd689cf8743c389aab6a767a09383891dc5c6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 17 Dec 2007 21:00:56 +0000 Subject: [PATCH 082/710] Project identifier is now used in URLs (instead of project id). URLs with a project id will still be recognized. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1007 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 14 ++++++++++++++ test/fixtures/projects.yml | 2 +- test/functional/issues_controller_test.rb | 2 +- test/functional/projects_controller_test.rb | 18 +++++++++++++----- .../functional/repositories_controller_test.rb | 4 ++-- test/functional/versions_controller_test.rb | 4 ++-- test/functional/wiki_controller_test.rb | 12 ++++++------ test/functional/wikis_controller_test.rb | 2 +- test/integration/issues_test.rb | 2 +- 9 files changed, 41 insertions(+), 19 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 84eeefee4..60a7a84a4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -111,6 +111,20 @@ class Project < ActiveRecord::Base end end + def self.find(*args) + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + project = find_by_identifier(*args) + raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil? + project + else + super + end + end + + def to_param + identifier + end + def active? self.status == STATUS_ACTIVE end diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index d3758c9e3..ad5cf4aa2 100644 --- a/test/fixtures/projects.yml +++ b/test/fixtures/projects.yml @@ -41,5 +41,5 @@ projects_004: description: eCookbook Subproject 2 homepage: "" is_public: true - identifier: subproject1 + identifier: subproject2 parent_id: 1 diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 638362dbe..ebcf78f24 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -144,7 +144,7 @@ class IssuesControllerTest < Test::Unit::TestCase def test_destroy @request.session[:user_id] = 2 post :destroy, :id => 1 - assert_redirected_to 'projects/1/issues' + assert_redirected_to 'projects/ecookbook/issues' assert_nil Issue.find_by_id(1) end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index b6ac59141..556d5d407 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -47,12 +47,20 @@ class ProjectsControllerTest < Test::Unit::TestCase assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) end - def test_show + def test_show_by_id get :show, :id => 1 assert_response :success assert_template 'show' assert_not_nil assigns(:project) end + + def test_show_by_identifier + get :show, :id => 'ecookbook' + assert_response :success + assert_template 'show' + assert_not_nil assigns(:project) + assert_equal Project.find_by_identifier('ecookbook'), assigns(:project) + end def test_settings @request.session[:user_id] = 2 # manager @@ -64,7 +72,7 @@ class ProjectsControllerTest < Test::Unit::TestCase def test_edit @request.session[:user_id] = 2 # manager post :edit, :id => 1, :project => {:name => 'Test changed name'} - assert_redirected_to 'projects/settings/1' + assert_redirected_to 'projects/settings/ecookbook' project = Project.find(1) assert_equal 'Test changed name', project.name end @@ -104,7 +112,7 @@ class ProjectsControllerTest < Test::Unit::TestCase def test_move_issues_to_another_project @request.session[:user_id] = 1 post :move_issues, :id => 1, :issue_ids => [1, 2], :new_project_id => 2 - assert_redirected_to 'projects/1/issues' + assert_redirected_to 'projects/ecookbook/issues' assert_equal 2, Issue.find(1).project_id assert_equal 2, Issue.find(2).project_id end @@ -112,7 +120,7 @@ class ProjectsControllerTest < Test::Unit::TestCase def test_move_issues_to_another_tracker @request.session[:user_id] = 1 post :move_issues, :id => 1, :issue_ids => [1, 2], :new_tracker_id => 3 - assert_redirected_to 'projects/1/issues' + assert_redirected_to 'projects/ecookbook/issues' assert_equal 3, Issue.find(1).tracker_id assert_equal 3, Issue.find(2).tracker_id end @@ -242,7 +250,7 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'add_issue' post :add_issue, :id => 1, :issue => {:tracker_id => 1, :subject => 'This is the test_add_issue issue', :description => 'This is the description', :priority_id => 5} - assert_redirected_to 'projects/1/issues' + assert_redirected_to 'projects/ecookbook/issues' assert Issue.find_by_subject('This is the test_add_issue issue') end diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb index 2f0459505..47455dc55 100644 --- a/test/functional/repositories_controller_test.rb +++ b/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/1?rev=0'} + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=0'} } assert_tag :tag => "div", :attributes => { :class => "contextual" }, - :child => { :tag => "a", :attributes => { :href => '/repositories/revision/1?rev=2'} + :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=2'} } end diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb index 17ebd3518..3477c5edd 100644 --- a/test/functional/versions_controller_test.rb +++ b/test/functional/versions_controller_test.rb @@ -52,7 +52,7 @@ class VersionsControllerTest < Test::Unit::TestCase post :edit, :id => 2, :version => { :name => 'New version name', :effective_date => Date.today.strftime("%Y-%m-%d")} - assert_redirected_to 'projects/settings/1' + assert_redirected_to 'projects/settings/ecookbook' version = Version.find(2) assert_equal 'New version name', version.name assert_equal Date.today, version.effective_date @@ -61,7 +61,7 @@ class VersionsControllerTest < Test::Unit::TestCase def test_destroy @request.session[:user_id] = 2 post :destroy, :id => 2 - assert_redirected_to 'projects/settings/1' + assert_redirected_to 'projects/settings/ecookbook' assert_nil Version.find_by_id(2) end diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index 6ee5ab276..043b0e805 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -64,7 +64,7 @@ class WikiControllerTest < Test::Unit::TestCase :content => {:comments => 'Created the page', :text => "h1. New page\n\nThis is a new page", :version => 0} - assert_redirected_to 'wiki/1/New_page' + assert_redirected_to 'wiki/ecookbook/New_page' page = Project.find(1).wiki.find_page('New page') assert !page.new_record? assert_not_nil page.content @@ -103,7 +103,7 @@ class WikiControllerTest < Test::Unit::TestCase post :rename, :id => 1, :page => 'Another_page', :wiki_page => { :title => 'Another renamed page', :redirect_existing_links => 1 } - assert_redirected_to 'wiki/1/Another_renamed_page' + assert_redirected_to 'wiki/ecookbook/Another_renamed_page' wiki = Project.find(1).wiki # Check redirects assert_not_nil wiki.find_page('Another page') @@ -115,7 +115,7 @@ class WikiControllerTest < Test::Unit::TestCase post :rename, :id => 1, :page => 'Another_page', :wiki_page => { :title => 'Another renamed page', :redirect_existing_links => "0" } - assert_redirected_to 'wiki/1/Another_renamed_page' + assert_redirected_to 'wiki/ecookbook/Another_renamed_page' wiki = Project.find(1).wiki # Check that there's no redirects assert_nil wiki.find_page('Another page') @@ -124,17 +124,17 @@ class WikiControllerTest < Test::Unit::TestCase def test_destroy @request.session[:user_id] = 2 post :destroy, :id => 1, :page => 'CookBook_documentation' - assert_redirected_to 'wiki/1/Page_index/special' + assert_redirected_to 'wiki/ecookbook/Page_index/special' end def test_page_index - get :special, :id => 1, :page => 'Page_index' + get :special, :id => 'ecookbook', :page => 'Page_index' assert_response :success assert_template 'special_page_index' pages = assigns(:pages) assert_not_nil pages assert_equal 2, pages.size - assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation' }, + assert_tag :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, :content => /CookBook documentation/ end diff --git a/test/functional/wikis_controller_test.rb b/test/functional/wikis_controller_test.rb index 93ad7e32d..3e51314a5 100644 --- a/test/functional/wikis_controller_test.rb +++ b/test/functional/wikis_controller_test.rb @@ -44,7 +44,7 @@ class WikisControllerTest < Test::Unit::TestCase def test_destroy @request.session[:user_id] = 1 post :destroy, :id => 1, :confirm => 1 - assert_redirected_to 'projects/settings/1' + assert_redirected_to 'projects/settings/ecookbook' assert_nil Project.find(1).wiki end diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index eac407b1b..702fbc026 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -24,7 +24,7 @@ class IssuesTest < ActionController::IntegrationTest assert_kind_of Issue, issue # check redirection - assert_redirected_to "projects/1/issues" + assert_redirected_to "projects/ecookbook/issues" follow_redirect! assert assigns(:issues).include?(issue) From 8e00f57a886ae529ae308a17bfc9a363056754bf Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 18 Dec 2007 18:50:41 +0000 Subject: [PATCH 083/710] Moved ProjectsController#list_documents and add_document to DocumentsController#index and new. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1011 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/documents_controller.rb | 42 ++++++++++-- app/controllers/projects_controller.rb | 28 -------- .../index.rhtml} | 4 +- .../new.rhtml} | 2 +- config/routes.rb | 1 + lib/redmine.rb | 6 +- test/functional/documents_controller_test.rb | 64 +++++++++++++++++++ test/functional/projects_controller_test.rb | 7 -- 8 files changed, 109 insertions(+), 45 deletions(-) rename app/views/{projects/list_documents.rhtml => documents/index.rhtml} (87%) rename app/views/{projects/add_document.rhtml => documents/new.rhtml} (61%) create mode 100644 test/functional/documents_controller_test.rb diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 104cca10c..b37a8371a 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -17,12 +17,40 @@ class DocumentsController < ApplicationController layout 'base' - before_filter :find_project, :authorize - + before_filter :find_project, :only => [:index, :new] + before_filter :find_document, :except => [:index, :new] + before_filter :authorize + + def index + @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category' + documents = @project.documents.find :all, :include => [:attachments, :category] + case @sort_by + when 'date' + @grouped = documents.group_by {|d| d.created_on.to_date } + when 'title' + @grouped = documents.group_by {|d| d.title.first.upcase} + when 'author' + @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author} + else + @grouped = documents.group_by(&:category) + end + render :layout => false if request.xhr? + end + def show @attachments = @document.attachments.find(:all, :order => "created_on DESC") end + def new + @document = @project.documents.build(params[:document]) + if request.post? and @document.save + attach_files(@document, params[:attachments]) + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') + redirect_to :action => 'index', :project_id => @project + end + end + def edit @categories = Enumeration::get_values('DCAT') if request.post? and @document.update_attributes(params[:document]) @@ -33,7 +61,7 @@ class DocumentsController < ApplicationController def destroy @document.destroy - redirect_to :controller => 'projects', :action => 'list_documents', :id => @project + redirect_to :controller => 'documents', :action => 'index', :project_id => @project end def download @@ -57,9 +85,15 @@ class DocumentsController < ApplicationController private def find_project + @project = Project.find(params[:project_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_document @document = Document.find(params[:id]) @project = @document.project rescue ActiveRecord::RecordNotFound render_404 - end + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7b1e4ef3d..84203a34d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -177,34 +177,6 @@ class ProjectsController < ApplicationController end end - # Add a new document to @project - def add_document - @document = @project.documents.build(params[:document]) - if request.post? and @document.save - attach_files(@document, params[:attachments]) - flash[:notice] = l(:notice_successful_create) - Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added') - redirect_to :action => 'list_documents', :id => @project - end - end - - # Show documents list of @project - def list_documents - @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category' - documents = @project.documents.find :all, :include => [:attachments, :category] - case @sort_by - when 'date' - @grouped = documents.group_by {|d| d.created_on.to_date } - when 'title' - @grouped = documents.group_by {|d| d.title.first.upcase} - when 'author' - @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author} - else - @grouped = documents.group_by(&:category) - end - render :layout => false if request.xhr? - end - # Add a new issue to @project # The new issue will be created from an existing one if copy_from parameter is given def add_issue diff --git a/app/views/projects/list_documents.rhtml b/app/views/documents/index.rhtml similarity index 87% rename from app/views/projects/list_documents.rhtml rename to app/views/documents/index.rhtml index 6829b9bf5..a7cefb733 100644 --- a/app/views/projects/list_documents.rhtml +++ b/app/views/documents/index.rhtml @@ -1,13 +1,13 @@
    <%= link_to_if_authorized l(:label_document_new), - {:controller => 'projects', :action => 'add_document', :id => @project}, + {:controller => 'documents', :action => 'new', :project_id => @project}, :class => 'icon icon-add', :onclick => 'Element.show("add-document"); return false;' %>
  • -
  • <%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue}, +
  • <%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon-copy', :disabled => !@can[:copy] %>
  • <%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon-move', :disabled => !@can[:move] %> diff --git a/app/views/projects/add_issue.rhtml b/app/views/issues/new.rhtml similarity index 88% rename from app/views/projects/add_issue.rhtml rename to app/views/issues/new.rhtml index a68922906..4fedc6895 100644 --- a/app/views/projects/add_issue.rhtml +++ b/app/views/issues/new.rhtml @@ -1,9 +1,7 @@

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

    <% labelled_tabular_form_for :issue, @issue, - :url => {:action => 'add_issue'}, :html => {:multipart => true, :id => 'issue-form'} do |f| %> - <%= f.hidden_field :tracker_id %> <%= render :partial => 'issues/form', :locals => {:f => f} %> <%= submit_tag l(:button_create) %> <%= link_to_remote l(:label_preview), diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 8cd44424c..7208e37be 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -3,7 +3,7 @@ <%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :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 => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> +<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> <%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index 2b930e01e..ecaa9750a 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -56,7 +56,7 @@ <% content_for :sidebar do %> - <% if authorize_for('projects', 'add_issue') && @project.trackers.any? %> + <% if authorize_for('issues', 'new') && @project.trackers.any? %>

    <%= l(:label_issue_new) %>

    <%= l(:label_tracker) %>: <%= new_issue_selector %> <% end %> diff --git a/lib/redmine.rb b/lib/redmine.rb index d0e152467..ea05d6e80 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -30,7 +30,7 @@ Redmine::AccessControl.map do |map| :versions => [:show, :status_by], :queries => :index, :reports => :issue_report}, :public => true - map.permission :add_issues, {:projects => :add_issue} + map.permission :add_issues, {:issues => :new} map.permission :edit_issues, {:projects => :bulk_edit_issues, :issues => [:edit, :update, :destroy_attachment]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index d60e32200..05bfc96e3 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -29,6 +29,7 @@ class IssuesControllerTest < Test::Unit::TestCase :issues, :issue_statuses, :trackers, + :projects_trackers, :issue_categories, :enabled_modules, :enumerations, @@ -126,6 +127,55 @@ class IssuesControllerTest < Test::Unit::TestCase :content => /Notes/ } } end + def test_get_new + @request.session[:user_id] = 2 + get :new, :project_id => 1, :tracker_id => 1 + assert_response :success + assert_template 'new' + end + + def test_get_new_without_tracker_id + @request.session[:user_id] = 2 + get :new, :project_id => 1 + assert_response :success + assert_template 'new' + + issue = assigns(:issue) + assert_not_nil issue + assert_equal Project.find(1).trackers.first, issue.tracker + end + + def test_update_new_form + @request.session[:user_id] = 2 + xhr :post, :new, :project_id => 1, + :issue => {:tracker_id => 2, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_response :success + assert_template 'new' + end + + def test_post_new + @request.session[:user_id] = 2 + post :new, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_redirected_to 'projects/ecookbook/issues' + assert Issue.find_by_subject('This is the test_new issue') + end + + def test_copy_issue + @request.session[:user_id] = 2 + get :new, :project_id => 1, :copy_from => 1 + assert_template 'new' + assert_not_nil assigns(:issue) + orig = Issue.find(1) + assert_equal orig.subject, assigns(:issue).subject + end + def test_get_edit @request.session[:user_id] = 2 get :edit, :id => 1 diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index f19aa7748..61047079e 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -236,23 +236,4 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_redirected_to 'admin/projects' assert Project.find(1).active? end - - def test_add_issue - @request.session[:user_id] = 2 - get :add_issue, :id => 1, :tracker_id => 1 - assert_response :success - assert_template 'add_issue' - post :add_issue, :id => 1, :issue => {:tracker_id => 1, :subject => 'This is the test_add_issue issue', :description => 'This is the description', :priority_id => 5} - assert_redirected_to 'projects/ecookbook/issues' - assert Issue.find_by_subject('This is the test_add_issue issue') - end - - def test_copy_issue - @request.session[:user_id] = 2 - get :add_issue, :id => 1, :copy_from => 1 - assert_template 'add_issue' - assert_not_nil assigns(:issue) - orig = Issue.find(1) - assert_equal orig.subject, assigns(:issue).subject - end end diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index a9d3f9c74..81d27c30f 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -6,11 +6,11 @@ class IssuesTest < ActionController::IntegrationTest # create an issue def test_add_issue log_user('jsmith', 'jsmith') - get "projects/add_issue/1", :tracker_id => "1" + get 'projects/1/issues/new', :tracker_id => '1' assert_response :success - assert_template "projects/add_issue" + assert_template 'issues/new' - post "projects/add_issue/1", :tracker_id => "1", + post 'projects/1/issues/new', :tracker_id => "1", :issue => { :start_date => "2006-12-26", :priority_id => "3", :subject => "new test issue", From 16e9ffce0d0e803f02e3660450120602bca8aff5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 13:07:19 +0000 Subject: [PATCH 142/710] Added a 'New issue' link in the main menu (accesskey 7). The drop-down lists to add an issue on the project overview and the issue list are removed. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1081 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 ++ app/helpers/application_helper.rb | 8 +------ app/helpers/projects_helper.rb | 8 ------- app/views/issues/_sidebar.rhtml | 5 ----- app/views/projects/show.rhtml | 5 ----- lib/redmine.rb | 2 ++ lib/redmine/access_keys.rb | 31 ++++++++++++++++++++++++++++ lib/redmine/menu_manager.rb | 10 +++++++-- public/stylesheets/application.css | 2 +- 9 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 lib/redmine/access_keys.rb diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 7a48282c9..643a4e0ef 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -17,6 +17,8 @@ class IssuesController < ApplicationController layout 'base' + menu_item :new_issue, :only => :new + before_filter :find_issue, :except => [:index, :changes, :preview, :new, :update_form] before_filter :find_project, :only => [:new, :update_form] before_filter :authorize, :except => [:index, :changes, :preview, :update_form] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f0455f3e4..1dfb57ff8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -162,15 +162,9 @@ module ApplicationHelper @html_title += args end end - - ACCESSKEYS = {:edit => 'e', - :preview => 'r', - :quick_search => 'f', - :search => '4', - }.freeze unless const_defined?(:ACCESSKEYS) def accesskey(s) - ACCESSKEYS[s] + Redmine::AccessKeys.key_for s end # Formats text according to system settings. diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ee61d88dd..883be0ead 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -188,12 +188,4 @@ module ProjectsHelper gc.draw(imgl) imgl end if Object.const_defined?(:Magick) - - def new_issue_selector - trackers = @project.trackers - # can't use form tag inside helper - content_tag('form', - select_tag('tracker_id', '' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"), - :action => url_for(:controller => 'issues', :action => 'new', :project_id => @project), :method => 'get') - end end diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml index 1e1fc87d0..4a1b7e9bc 100644 --- a/app/views/issues/_sidebar.rhtml +++ b/app/views/issues/_sidebar.rhtml @@ -1,8 +1,3 @@ -<% if authorize_for('issues', 'new') && @project.trackers.any? %> -

    <%= l(:label_issue_new) %>

    -<%= l(:label_tracker) %>: <%= new_issue_selector %> -<% end %> -

    <%= l(:label_issue_plural) %>

    <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %>
    <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %>
    diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index ecaa9750a..eb80bf9fa 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -56,11 +56,6 @@ <% content_for :sidebar do %> - <% if authorize_for('issues', 'new') && @project.trackers.any? %> -

    <%= l(:label_issue_new) %>

    - <%= l(:label_tracker) %>: <%= new_issue_selector %> - <% end %> - <% planning_links = [] planning_links << link_to_if_authorized(l(:label_calendar), :action => 'calendar', :id => @project) planning_links << link_to_if_authorized(l(:label_gantt), :action => 'gantt', :id => @project) diff --git a/lib/redmine.rb b/lib/redmine.rb index ea05d6e80..1aee1767c 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -95,6 +95,8 @@ Redmine::MenuManager.map :project_menu do |menu| menu.push :activity, { :controller => 'projects', :action => 'activity' }, :caption => :label_activity menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, :caption => :label_roadmap menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural + menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new, + :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) } menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil }, diff --git a/lib/redmine/access_keys.rb b/lib/redmine/access_keys.rb new file mode 100644 index 000000000..96029a6fc --- /dev/null +++ b/lib/redmine/access_keys.rb @@ -0,0 +1,31 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module AccessKeys + ACCESSKEYS = {:edit => 'e', + :preview => 'r', + :quick_search => 'f', + :search => '4', + :new_issue => '7' + }.freeze unless const_defined?(:ACCESSKEYS) + + def self.key_for(action) + ACCESSKEYS[action] + end + end +end diff --git a/lib/redmine/menu_manager.rb b/lib/redmine/menu_manager.rb index c801cb383..379028ebc 100644 --- a/lib/redmine/menu_manager.rb +++ b/lib/redmine/menu_manager.rb @@ -68,7 +68,7 @@ module Redmine unless item.condition && !item.condition.call(project) links << content_tag('li', link_to(l(item.caption), {item.param => project}.merge(item.url), - :class => (current_menu_item == item.name ? 'selected' : nil))) + (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options))) end end if project && !project.new_record? links.empty? ? nil : content_tag('ul', links.join("\n")) @@ -94,6 +94,11 @@ module Redmine end class Mapper + # Adds an item at the end of the menu. Available options: + # * param: the parameter name that is used for the project id (default is :id) + # * condition: a proc that is called before rendering the item, the item is displayed only if it returns true + # * caption: the localized string key that is used as the item label + # * html_options: a hash of html options that are passed to link_to def push(name, url, options={}) @items ||= [] @items << MenuItem.new(name, url, options) @@ -105,7 +110,7 @@ module Redmine end class MenuItem - attr_reader :name, :url, :param, :condition, :caption + attr_reader :name, :url, :param, :condition, :caption, :html_options def initialize(name, url, options) @name = name @@ -113,6 +118,7 @@ module Redmine @condition = options[:if] @param = options[:param] || :id @caption = options[:caption] || name.to_s.humanize + @html_options = options[:html] || {} end end end diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index d370f1b42..5bada6e06 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -17,7 +17,7 @@ h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; bord #header a {color:#f8f8f8;} #quick-search {float:right;} -#main-menu {position: absolute; bottom: 0px; left:6px;} +#main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} #main-menu ul {margin: 0; padding: 0;} #main-menu li { float:left; From 889bf388be0325d4c38b984a4eaa23db6102938a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 13:53:45 +0000 Subject: [PATCH 143/710] Fixed: error when removing a project member (postgresql and sqlite only). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1082 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/member.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/member.rb b/app/models/member.rb index 39703147d..0d0540690 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -33,6 +33,6 @@ class Member < ActiveRecord::Base def before_destroy # remove category based auto assignments for this member - project.issue_categories.update_all "assigned_to_id = NULL", ["assigned_to_id = ?", self.user.id] + IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] end end From d79c20c4f28f9b43f1bc105cb590d244c1b322f2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 14:01:02 +0000 Subject: [PATCH 144/710] Fixed: custom field selection is not saved when unchecking them all on project settings git-svn-id: http://redmine.rubyforge.org/svn/trunk@1083 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 1 - app/views/projects/_form.rhtml | 3 ++- test/functional/projects_controller_test.rb | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2342e8074..30e7ef85f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -113,7 +113,6 @@ class ProjectsController < ApplicationController # Edit @project def edit if request.post? - @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] if params[:custom_fields] @custom_values = ProjectCustomField.find(:all, :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 diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index e29777af4..e63c929cc 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -35,10 +35,11 @@
    <%=l(:label_custom_field_plural)%> <% for custom_field in @custom_fields %> <% end %> +<%= hidden_field_tag 'project[custom_field_ids][]', '' %>
    <% end %> diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 61047079e..16eb2906e 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -71,7 +71,8 @@ class ProjectsControllerTest < Test::Unit::TestCase def test_edit @request.session[:user_id] = 2 # manager - post :edit, :id => 1, :project => {:name => 'Test changed name'} + post :edit, :id => 1, :project => {:name => 'Test changed name', + :custom_field_ids => ['']} assert_redirected_to 'projects/settings/ecookbook' project = Project.find(1) assert_equal 'Test changed name', project.name From 5e2a01656df1b98f82a0565271b6d527d8fefbf4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 14:24:19 +0000 Subject: [PATCH 145/710] Test environments cleanup. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1084 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- config/database.yml.example | 17 +++++------------ config/environments/test_oracle.rb | 16 ---------------- .../{test_sqlserver.rb => test_sqlite3.rb} | 0 vendor/plugins/actionwebservice/init.rb | 2 +- 4 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 config/environments/test_oracle.rb rename config/environments/{test_sqlserver.rb => test_sqlite3.rb} (100%) diff --git a/config/database.yml.example b/config/database.yml.example index 2f38d81ef..f72844a07 100644 --- a/config/database.yml.example +++ b/config/database.yml.example @@ -29,23 +29,16 @@ test: test_pgsql: adapter: postgresql - database: redmine + database: redmine_test host: localhost username: postgres password: "postgres" -test_oracle: - adapter: oci - host: 192.168.0.14 - username: rails_test - password: "rails" - -test_sqlserver: - adapter: sqlserver - host: localhost,1157 - database: redmine_test +test_sqlite3: + adapter: sqlite3 + dbfile: db/test.db demo: adapter: sqlite3 - dbfile: db/redmine_demo.db + dbfile: db/demo.db diff --git a/config/environments/test_oracle.rb b/config/environments/test_oracle.rb deleted file mode 100644 index 0eb1cd6c1..000000000 --- a/config/environments/test_oracle.rb +++ /dev/null @@ -1,16 +0,0 @@ -# Settings specified here will take precedence over those in config/environment.rb - -# The test environment is used exclusively to run your application's -# test suite. You never need to work with it otherwise. Remember that -# your test database is "scratch space" for the test suite and is wiped -# and recreated between test runs. Don't rely on the data there! -config.cache_classes = false - -# Log error messages when you accidentally call methods on nil. -config.whiny_nils = true - -# Show full error reports and disable caching -config.action_controller.consider_all_requests_local = true -config.action_controller.perform_caching = false - -config.action_mailer.delivery_method = :test \ No newline at end of file diff --git a/config/environments/test_sqlserver.rb b/config/environments/test_sqlite3.rb similarity index 100% rename from config/environments/test_sqlserver.rb rename to config/environments/test_sqlite3.rb diff --git a/vendor/plugins/actionwebservice/init.rb b/vendor/plugins/actionwebservice/init.rb index 582f73717..ade118c0f 100644 --- a/vendor/plugins/actionwebservice/init.rb +++ b/vendor/plugins/actionwebservice/init.rb @@ -4,4 +4,4 @@ require 'action_web_service' Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"] # AWS Test helpers -require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] == 'test' +require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] =~ /^test/ From b6549d763ae7a4dc9ad2f00c7b9d6becc1e39012 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 15:38:11 +0000 Subject: [PATCH 146/710] Added related changesets messages on issue details view. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1085 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_changesets.rhtml | 8 ++++++++ app/views/issues/_pdf.rfpdf | 20 ++++++++++++++++++-- app/views/issues/show.rhtml | 13 +++++++------ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + public/stylesheets/application.css | 5 +++++ 26 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 app/views/issues/_changesets.rhtml diff --git a/app/views/issues/_changesets.rhtml b/app/views/issues/_changesets.rhtml new file mode 100644 index 000000000..1a4c1a5bd --- /dev/null +++ b/app/views/issues/_changesets.rhtml @@ -0,0 +1,8 @@ +
      +<% changesets.each do |changeset| %> +
    • <%= link_to("#{l(:label_revision)} #{changeset.revision}", + :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %>
      + <%= changeset.committer %>, <%= format_time(changeset.committed_on) %> + <%= textilizable(changeset, :comments) %>
    • +<% end %> +
    diff --git a/app/views/issues/_pdf.rfpdf b/app/views/issues/_pdf.rfpdf index 558399abb..6830506f6 100644 --- a/app/views/issues/_pdf.rfpdf +++ b/app/views/issues/_pdf.rfpdf @@ -66,8 +66,24 @@ pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY) pdf.Ln - - pdf.SetFontStyle('B',9) + + 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") diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 7208e37be..45423a2e0 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -57,12 +57,6 @@ end %>
    -<% if @issue.changesets.any? %> -
    - <%= l(:label_revision_plural) %>: <%= @issue.changesets.collect{|changeset| link_to(changeset.revision, :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision)}.join(", ") %> -
    -<% end %> -

    <%=l(:field_description)%>

    <%= textilizable @issue, :description, :attachments => @issue.attachments %> @@ -81,6 +75,13 @@ end %>
    +<% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %> +
    +

    <%=l(:label_associated_revisions)%>

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

    <%=l(:label_history)%>

    diff --git a/lang/bg.yml b/lang/bg.yml index ae5630ecf..7f994acd4 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/cs.yml b/lang/cs.yml index 813dade3f..6e904c106 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/de.yml b/lang/de.yml index 660289768..27ca40a3d 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/en.yml b/lang/en.yml index 7d3912760..6580d398a 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -359,6 +359,7 @@ label_modification: %d change label_modification_plural: %d changes label_revision: Revision label_revision_plural: Revisions +label_associated_revisions: Associated revisions label_added: added label_modified: modified label_deleted: deleted diff --git a/lang/es.yml b/lang/es.yml index 37821e129..1c6776555 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -564,3 +564,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/fi.yml b/lang/fi.yml index 4aee95bb9..7eba540a7 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -566,3 +566,4 @@ default_activity_development: Kehitys enumeration_issue_priorities: Tapahtuman prioriteetit enumeration_doc_categories: Dokumentin luokat enumeration_activities: Aktiviteetit (ajan seuranta) +label_associated_revisions: Associated revisions diff --git a/lang/fr.yml b/lang/fr.yml index 841f132d4..ce3817d3c 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -359,6 +359,7 @@ label_modification: %d modification label_modification_plural: %d modifications label_revision: Révision label_revision_plural: Révisions +label_associated_revisions: Révisions associées label_added: ajouté label_modified: modifié label_deleted: supprimé diff --git a/lang/he.yml b/lang/he.yml index 666675e75..928c68baa 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/it.yml b/lang/it.yml index 24e39955a..a16228149 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/ja.yml b/lang/ja.yml index d68013296..56f846947 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -562,3 +562,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/ko.yml b/lang/ko.yml index 75a4bef4c..7a509478e 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -561,3 +561,4 @@ button_update: 변경사항기록 label_change_properties: 속성 변경 label_general: 일반 label_repository_plural: 저장소들 +label_associated_revisions: Associated revisions diff --git a/lang/lt.yml b/lang/lt.yml index 8cf573d10..8455260f1 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -562,3 +562,4 @@ text_load_default_configuration: Load the default configuration text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." label_repository_plural: Repositories error_can_t_load_default_data: "Default configuration could not be loaded: %s" +label_associated_revisions: Associated revisions diff --git a/lang/nl.yml b/lang/nl.yml index 44633e4b4..92e5a5971 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -562,3 +562,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/pl.yml b/lang/pl.yml index 3353333b8..548917a36 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -561,3 +561,4 @@ button_update: Uaktualnij label_change_properties: Zmień właściwości label_general: Ogólne label_repository_plural: Repozytoria +label_associated_revisions: Associated revisions diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 684b51daa..45e66cd52 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/pt.yml b/lang/pt.yml index 0ce2d3471..54b74c47c 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/ro.yml b/lang/ro.yml index 95e0ea318..c3427d261 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -561,3 +561,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/ru.yml b/lang/ru.yml index 131b5c77c..7bf47c8fc 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -560,3 +560,4 @@ button_update: Обновить label_change_properties: Изменить свойства label_general: Общее label_repository_plural: Репозитории +label_associated_revisions: Associated revisions diff --git a/lang/sr.yml b/lang/sr.yml index ece6cf6e2..6c486a83f 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -562,3 +562,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/sv.yml b/lang/sv.yml index cf1e760f9..0cb9a2d67 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -562,3 +562,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index b0c258bcf..465855b58 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -562,3 +562,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) +label_associated_revisions: Associated revisions diff --git a/lang/zh.yml b/lang/zh.yml index 2d537b250..c42c60fac 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -564,3 +564,4 @@ button_update: Update label_change_properties: Change properties label_general: General label_repository_plural: Repositories +label_associated_revisions: Associated revisions diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 5bada6e06..ec3ae159d 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -135,6 +135,11 @@ hr { width: 100%; height: 1px; background: #ccc; border: 0;} textarea.wiki-edit { width: 99%; } li p {margin-top: 0;} div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} + +div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em;} +div#issue-changesets ul {list-style-position: outside; list-style-type:none; margin: 0; padding: 0;} +div#issue-changesets li { padding: 4px; } + .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;} #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; } From 0b4a02f607a1eefb89bc37b734375fb7d70afc6c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 17:20:34 +0000 Subject: [PATCH 147/710] Display custom fields in two columns on the issue form. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1086 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/custom_fields_helper.rb | 2 +- app/views/issues/_form.rhtml | 5 ++--- app/views/issues/_form_custom_fields.rhtml | 9 +++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 app/views/issues/_form_custom_fields.rhtml diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index 25389ca63..8792c8c6e 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -28,7 +28,7 @@ module CustomFieldsHelper text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) + calendar_for(field_id) when "text" - text_area 'custom_value', 'value', :name => field_name, :id => field_id, :cols => 60, :rows => 3 + text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%' when "bool" check_box 'custom_value', 'value', :name => field_name, :id => field_id when "list" diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index 779846f4d..d91effedf 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -38,9 +38,8 @@ :accesskey => accesskey(:edit), :class => 'wiki-edit' %>

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

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

    <%= custom_field_tag_with_label @custom_value %>

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

    +<% values.each_with_index do |value, i| %> +

    <%= custom_field_tag_with_label value %>

    + <% if i >= values.size / 2 - 1 %> +
    + <% end %> +<% end %> +
    +
    From d5b9dedca2298e8b95bbb8ac309ddfe8454a8c76 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 17:36:01 +0000 Subject: [PATCH 148/710] Added tabindex property on wiki toolbar buttons and 'new category' link. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1087 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_form.rhtml | 2 +- public/javascripts/jstoolbar/jstoolbar.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index d91effedf..0f5e10de9 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -21,7 +21,7 @@ <%= prompt_to_remote(l(:label_issue_category_new), l(:label_issue_category_new), 'category[name]', {:controller => 'projects', :action => 'add_issue_category', :id => @project}, - :class => 'small') if authorize_for('projects', 'add_issue_category') %>

    + :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %>

    diff --git a/public/javascripts/jstoolbar/jstoolbar.js b/public/javascripts/jstoolbar/jstoolbar.js index 65f25bac1..7941bd135 100644 --- a/public/javascripts/jstoolbar/jstoolbar.js +++ b/public/javascripts/jstoolbar/jstoolbar.js @@ -81,6 +81,7 @@ jsButton.prototype.draw = function() { var button = document.createElement('button'); button.setAttribute('type','button'); + button.tabIndex = 200; if (this.className) button.className = this.className; button.title = this.title; var span = document.createElement('span'); From df99d8f30801792560fc955ac36c2801e6aa5ff5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 18:37:51 +0000 Subject: [PATCH 149/710] Unlimited and optional project description. The project list will show truncated descriptions only (the first fews lines). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1088 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 12 ++++++++++-- app/views/admin/projects.rhtml | 2 +- app/views/projects/_form.rhtml | 7 +++++-- app/views/projects/list.rhtml | 2 +- app/views/welcome/index.rhtml | 2 +- .../087_change_projects_description_to_text.rb | 8 ++++++++ 6 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 db/migrate/087_change_projects_description_to_text.rb diff --git a/app/models/project.rb b/app/models/project.rb index 0e7f8284c..42f2ddfd9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -52,12 +52,11 @@ class Project < ActiveRecord::Base attr_protected :status, :enabled_module_names - validates_presence_of :name, :description, :identifier + 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 :description, :maximum => 255 validates_length_of :homepage, :maximum => 60 validates_length_of :identifier, :in => 3..20 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ @@ -184,6 +183,15 @@ class Project < ActiveRecord::Base name.downcase <=> project.name.downcase end + def to_s + name + end + + # Returns a short description of the projects (first lines) + def short_description(length = 255) + description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description + end + def allows_to?(action) if action.is_a? Hash allowed_actions.include? "#{action[:controller]}/#{action[:action]}" diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index 3a759ecaa..d35c484b5 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -27,7 +27,7 @@ <% for project in @projects %> "> <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> - <%= textilizable project.description, :project => project %> + <%= textilizable project.short_description, :project => project %> <%= image_tag 'true.png' if project.is_public? %> <%= project.children.size %> <%= format_date(project.created_on) %> diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index e63c929cc..a810369d4 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -8,8 +8,11 @@

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

    <% end %> -

    <%= f.text_area :description, :required => true, :cols => 60, :rows => 5 %><%= l(:text_caracters_maximum, 255) %>

    -

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

    +

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

    +

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

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

    <%= f.check_box :is_public %>

    <%= wikitoolbar_for 'project_description' %> diff --git a/app/views/projects/list.rhtml b/app/views/projects/list.rhtml index a18f63e43..15ea06483 100644 --- a/app/views/projects/list.rhtml +++ b/app/views/projects/list.rhtml @@ -2,7 +2,7 @@ <% @project_tree.keys.sort.each do |project| %>

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

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

    <%= l(:label_subproject_plural) %>: diff --git a/app/views/welcome/index.rhtml b/app/views/welcome/index.rhtml index 2e7ec2c06..d618fa6e1 100644 --- a/app/views/welcome/index.rhtml +++ b/app/views/welcome/index.rhtml @@ -18,7 +18,7 @@ <% for project in @projects %>

  • <%= link_to project.name, :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>) - <%= textilizable project.description, :project => project %> + <%= textilizable project.short_description, :project => project %>
  • <% end %> diff --git a/db/migrate/087_change_projects_description_to_text.rb b/db/migrate/087_change_projects_description_to_text.rb new file mode 100644 index 000000000..d215840aa --- /dev/null +++ b/db/migrate/087_change_projects_description_to_text.rb @@ -0,0 +1,8 @@ +class ChangeProjectsDescriptionToText < ActiveRecord::Migration + def self.up + change_column :projects, :description, :text, :default => '' + end + + def self.down + end +end From 59c5d995c339f726f7c6249d543a2b2a2a7bc061 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 19:45:08 +0000 Subject: [PATCH 150/710] Fixed: custom fields empty on issue/edit (broken by r1086). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1089 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_form_custom_fields.rhtml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/views/issues/_form_custom_fields.rhtml b/app/views/issues/_form_custom_fields.rhtml index 291b5d4de..e5a98e30e 100644 --- a/app/views/issues/_form_custom_fields.rhtml +++ b/app/views/issues/_form_custom_fields.rhtml @@ -1,9 +1,11 @@
    -<% values.each_with_index do |value, i| %> -

    <%= custom_field_tag_with_label value %>

    - <% if i >= values.size / 2 - 1 %> +<% i = 1 %> +<% for @custom_value in values %> +

    <%= custom_field_tag_with_label @custom_value %>

    + <% if i >= values.size / 2 %>
    <% end %> + <% i += 1 %> <% end %>
    From d6bfb7fa4da4068c3e64f3fce16574de56fd72e9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 21:29:51 +0000 Subject: [PATCH 151/710] Added default value for custom fields. Fixed javascript on custom field form for project and user custom fields. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1090 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/custom_field.rb | 5 +++++ app/models/custom_value.rb | 6 ++++++ app/views/custom_fields/_form.rhtml | 22 +++++++++++++++++----- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 3 ++- lang/nl.yml | 3 ++- lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/fixtures/custom_fields.yml | 5 +++++ test/functional/issues_controller_test.rb | 19 ++++++++++++++++--- test/integration/issues_test.rb | 14 ++++++++++++-- test/unit/custom_value_test.rb | 11 +++++++++++ 29 files changed, 96 insertions(+), 12 deletions(-) diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index 5a134c4ec..6be081b0b 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -53,6 +53,11 @@ class CustomField < ActiveRecord::Base errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty? errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array end + + # validate default value + v = CustomValue.new(:custom_field => self.dup, :value => default_value, :customized => nil) + v.custom_field.is_required = false + errors.add(:default_value, :activerecord_error_invalid) unless v.valid? end def <=>(field) diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index c3d6b7bb9..94b797bcc 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -19,6 +19,12 @@ class CustomValue < ActiveRecord::Base belongs_to :custom_field belongs_to :customized, :polymorphic => true + def after_initialize + if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?)) + self.value ||= custom_field.default_value + end + end + protected def validate errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank? diff --git a/app/views/custom_fields/_form.rhtml b/app/views/custom_fields/_form.rhtml index 915daab32..5e4eadf21 100644 --- a/app/views/custom_fields/_form.rhtml +++ b/app/views/custom_fields/_form.rhtml @@ -8,31 +8,42 @@ function toggle_custom_field_format() { p_regexp = $("custom_field_regexp"); p_values = $("custom_field_possible_values"); p_searchable = $("custom_field_searchable"); + p_default = $("custom_field_default_value"); + + p_default.setAttribute('type','text'); + Element.show(p_default.parentNode); + switch (format.value) { case "list": Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); - Element.show(p_searchable.parentNode); + if (p_searchable) Element.show(p_searchable.parentNode); Element.show(p_values); break; - case "date": case "bool": + p_default.setAttribute('type','checkbox'); Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); - Element.hide(p_searchable.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "date": + Element.hide(p_length.parentNode); + Element.hide(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); Element.hide(p_values); break; case "float": case "int": Element.show(p_length.parentNode); Element.show(p_regexp.parentNode); - Element.hide(p_searchable.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); Element.hide(p_values); break; default: Element.show(p_length.parentNode); Element.show(p_regexp.parentNode); - Element.show(p_searchable.parentNode); + if (p_searchable) Element.show(p_searchable.parentNode); Element.hide(p_values); break; } @@ -70,6 +81,7 @@ function deleteValueField(e) { <%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %>
    <% end %>

    +

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

    diff --git a/lang/bg.yml b/lang/bg.yml index 7f994acd4..2c83b966f 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -161,6 +161,7 @@ field_delay: Отместване field_assignable: Възможно е възлагане на задачи за тази роля field_redirect_existing_links: Пренасочване на съществуващи линкове field_estimated_hours: Изчислено време +field_default_value: Статус по подразбиране setting_app_title: Заглавие setting_app_subtitle: Описание diff --git a/lang/cs.yml b/lang/cs.yml index 6e904c106..cde58fe17 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -159,6 +159,7 @@ field_is_filter: Used as a filter field_issue_to_id: Vztažený požadavek field_delay: Zpoždění field_assignable: Požadavky mohou být přiřazeny této roli +field_default_value: Výchozí stav setting_app_title: Titulek aplikace setting_app_subtitle: Podtitulek aplikace diff --git a/lang/de.yml b/lang/de.yml index 27ca40a3d..a23e9c533 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -161,6 +161,7 @@ field_delay: Pufferzeit field_assignable: Tickets können dieser Rolle zugewiesen werden field_redirect_existing_links: Existierende Links umleiten field_estimated_hours: Geschätzter Aufwand +field_default_value: Default setting_app_title: Applikations-Titel setting_app_subtitle: Applikations-Untertitel diff --git a/lang/en.yml b/lang/en.yml index 6580d398a..dc7b04dd2 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -174,6 +174,7 @@ field_estimated_hours: Estimated time field_column_names: Columns field_time_zone: Time zone field_searchable: Searchable +field_default_value: Default value setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/lang/es.yml b/lang/es.yml index 1c6776555..8e9ffaa2f 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -155,6 +155,7 @@ field_identifier: Identificador field_is_filter: Usado como filtro field_issue_to_id: Petición Relacionada field_delay: Retraso +field_default_value: Estado por defecto setting_app_title: Título de la aplicación setting_app_subtitle: Subtítulo de la aplicación diff --git a/lang/fi.yml b/lang/fi.yml index 7eba540a7..497647ad9 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -174,6 +174,7 @@ field_estimated_hours: Arvioitu aika field_column_names: Saraketta field_time_zone: Aikavyöhyke field_searchable: Haettava +field_default_value: Vakio arvo setting_app_title: Ohjelman otsikko setting_app_subtitle: Ohjelman alaotsikko diff --git a/lang/fr.yml b/lang/fr.yml index ce3817d3c..854ed5859 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -174,6 +174,7 @@ field_estimated_hours: Temps estimé field_column_names: Colonnes field_time_zone: Fuseau horaire field_searchable: Utilisé pour les recherches +field_default_value: Valeur par défaut setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/lang/he.yml b/lang/he.yml index 928c68baa..3e8ad4160 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -164,6 +164,7 @@ field_assignable: ניתן להקצות נושאים לתפקיד זה field_redirect_existing_links: העבר קישורים קיימים field_estimated_hours: זמן משוער field_column_names: עמודות +field_default_value: ערך ברירת מחדל setting_app_title: כותרת ישום setting_app_subtitle: תת-כותרת ישום diff --git a/lang/it.yml b/lang/it.yml index a16228149..ca4b3391a 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -161,6 +161,7 @@ field_delay: Delay field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_default_value: Stato predefinito setting_app_title: Titolo applicazione setting_app_subtitle: Sottotitolo applicazione diff --git a/lang/ja.yml b/lang/ja.yml index 56f846947..713af6b46 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -162,6 +162,7 @@ field_delay: 遅延 field_assignable: 問題はこのロールに割り当てることができます field_redirect_existing_links: 既存のリンクをリダイレクトする field_estimated_hours: 予定工数 +field_default_value: デフォルトのステータス setting_app_title: アプリケーションのタイトル setting_app_subtitle: アプリケーションのサブタイトル diff --git a/lang/ko.yml b/lang/ko.yml index 7a509478e..564e67253 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -164,6 +164,7 @@ field_assignable: 이 역할에 할당될수 있는 티켓 field_redirect_existing_links: Redirect existing links field_estimated_hours: 추정시간 field_column_names: 컬럼 +field_default_value: 기본값 setting_app_title: 레드마인 제목 setting_app_subtitle: 레드마인 부제목 diff --git a/lang/lt.yml b/lang/lt.yml index 8455260f1..d95da7469 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -171,7 +171,8 @@ field_estimated_hours: Apskaičiuotas laikas field_column_names: Skiltys field_time_zone: Laiko juosta field_searchable: Randamas - +field_default_value: Numatytoji vertė + setting_app_title: Programos pavadinimas setting_app_subtitle: Programos paantraštė setting_welcome_text: Pasveikinimas diff --git a/lang/nl.yml b/lang/nl.yml index 92e5a5971..8d29ab902 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -109,7 +109,7 @@ field_issue: Issue field_status: Status field_notes: Notities field_is_closed: Issue gesloten -field_is_default: Default status +field_is_default: Default field_tracker: Tracker field_subject: Onderwerp field_due_date: Verwachte datum gereed @@ -161,6 +161,7 @@ field_delay: Vertraging field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_default_value: Default value setting_app_title: Applicatie titel setting_app_subtitle: Applicatie ondertitel diff --git a/lang/pl.yml b/lang/pl.yml index 548917a36..69bf5306a 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -155,6 +155,7 @@ field_identifier: Identifikator field_is_filter: Atrybut filtrowania field_issue_to_id: Powiązania zagadnienia field_delay: Opóźnienie +field_default_value: Domyślny setting_app_title: Tytuł aplikacji setting_app_subtitle: Podtytuł aplikacji diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 45e66cd52..926c913a0 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -161,6 +161,7 @@ field_delay: Delay field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_default_value: Padrao setting_app_title: Titulo da aplicacao setting_app_subtitle: Sub-titulo da aplicacao diff --git a/lang/pt.yml b/lang/pt.yml index 54b74c47c..88fd9cf74 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -161,6 +161,7 @@ field_delay: Atraso field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_default_value: Padrão setting_app_title: Título da aplicação setting_app_subtitle: Sub-título da aplicação diff --git a/lang/ro.yml b/lang/ro.yml index c3427d261..2eb17609c 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -161,6 +161,7 @@ field_delay: Intarziere field_assignable: La acest rol se poate atribui tichete field_redirect_existing_links: Redirectare linkuri existente field_estimated_hours: Timpul estimat +field_default_value: Default value setting_app_title: Titlul aplicatiei setting_app_subtitle: Subtitlul aplicatiei diff --git a/lang/ru.yml b/lang/ru.yml index 7bf47c8fc..62799bdf8 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -166,6 +166,7 @@ field_assignable: Задача может быть назначена этой field_redirect_existing_links: Перенаправить существующие ссылки field_estimated_hours: Оцененное время field_column_names: Колонки +field_default_value: Default value setting_app_title: Название приложения setting_app_subtitle: Подзаголовок приложения diff --git a/lang/sr.yml b/lang/sr.yml index 6c486a83f..6d88d34ff 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -166,6 +166,7 @@ field_assignable: Kartice mogu biti dodeljene ovoj ulozi field_redirect_existing_links: Redirekcija postojećih linkova field_estimated_hours: Procenjeno vreme field_column_names: Kolone +field_default_value: Default value setting_app_title: Naziv aplikacije setting_app_subtitle: Podnaslov aplikacije diff --git a/lang/sv.yml b/lang/sv.yml index 0cb9a2d67..5f4a01126 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -161,6 +161,7 @@ field_delay: Delay field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_default_value: Default value setting_app_title: Applikationstitel setting_app_subtitle: Applicationsunderrubrik diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 465855b58..2a431266e 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -174,6 +174,7 @@ field_estimated_hours: 預估工時 field_column_names: Columns field_time_zone: 時區 field_searchable: 可用做搜尋條件 +field_default_value: Default value setting_app_title: 標題 setting_app_subtitle: 副標題 diff --git a/lang/zh.yml b/lang/zh.yml index c42c60fac..b87a1515d 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -164,6 +164,7 @@ field_delay: Delay field_assignable: Issues can be assigned to this role field_redirect_existing_links: Redirect existing links field_estimated_hours: Estimated time +field_default_value: Default value setting_app_title: 应用程序标题 setting_app_subtitle: 应用程序子标题 diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index e73e6de96..e58d8e3dc 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -10,6 +10,7 @@ custom_fields_001: id: 1 is_required: false field_format: list + default_value: "" custom_fields_002: name: Searchable field min_length: 1 @@ -22,6 +23,7 @@ custom_fields_002: is_required: false field_format: string searchable: true + default_value: "Default string" custom_fields_003: name: Development status min_length: 0 @@ -33,6 +35,7 @@ custom_fields_003: id: 3 is_required: true field_format: list + default_value: "" custom_fields_004: name: Phone number min_length: 0 @@ -44,6 +47,7 @@ custom_fields_004: id: 4 is_required: false field_format: string + default_value: "" custom_fields_005: name: Money min_length: 0 @@ -55,4 +59,5 @@ custom_fields_005: id: 5 is_required: false field_format: float + default_value: "" \ No newline at end of file diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 05bfc96e3..f4c99f1ed 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -34,7 +34,10 @@ class IssuesControllerTest < Test::Unit::TestCase :enabled_modules, :enumerations, :attachments, - :workflows + :workflows, + :custom_fields, + :custom_values, + :custom_fields_trackers def setup @controller = IssuesController.new @@ -132,6 +135,9 @@ class IssuesControllerTest < Test::Unit::TestCase get :new, :project_id => 1, :tracker_id => 1 assert_response :success assert_template 'new' + + assert_tag :tag => 'input', :attributes => { :name => 'custom_fields[2]', + :value => 'Default string' } end def test_get_new_without_tracker_id @@ -162,9 +168,16 @@ class IssuesControllerTest < Test::Unit::TestCase :issue => {:tracker_id => 1, :subject => 'This is the test_new issue', :description => 'This is the description', - :priority_id => 5} + :priority_id => 5}, + :custom_fields => {'2' => 'Value for field 2'} assert_redirected_to 'projects/ecookbook/issues' - assert Issue.find_by_subject('This is the test_new issue') + + issue = Issue.find_by_subject('This is the test_new issue') + assert_not_nil issue + assert_equal 2, issue.author_id + v = issue.custom_values.find_by_custom_field_id(2) + assert_not_nil v + assert_equal 'Value for field 2', v.value end def test_copy_issue diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index 81d27c30f..7249ed3da 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -1,7 +1,16 @@ require "#{File.dirname(__FILE__)}/../test_helper" class IssuesTest < ActionController::IntegrationTest - fixtures :projects, :users, :trackers, :issue_statuses, :issues, :enumerations + fixtures :projects, + :users, + :trackers, + :projects_trackers, + :issue_statuses, + :issues, + :enumerations, + :custom_fields, + :custom_values, + :custom_fields_trackers # create an issue def test_add_issue @@ -18,7 +27,8 @@ class IssuesTest < ActionController::IntegrationTest :description => "new issue", :done_ratio => "0", :due_date => "", - :assigned_to_id => "" } + :assigned_to_id => "" }, + :custom_fields => {'2' => 'Value for field 2'} # find created issue issue = Issue.find_by_subject("new test issue") assert_kind_of Issue, issue diff --git a/test/unit/custom_value_test.rb b/test/unit/custom_value_test.rb index 24d09fe49..4d488bae2 100644 --- a/test/unit/custom_value_test.rb +++ b/test/unit/custom_value_test.rb @@ -31,4 +31,15 @@ class CustomValueTest < Test::Unit::TestCase v.value = '6a' assert !v.save end + + def test_default_value + field = CustomField.find_by_default_value('Default string') + assert_not_nil field + + v = CustomValue.new(:custom_field => field) + assert_equal 'Default string', v.value + + v = CustomValue.new(:custom_field => field, :value => 'Not empty') + assert_equal 'Not empty', v.value + end end From c65ab7c0f2722cee919ee2d516bf75f8a85daa21 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 20 Jan 2008 23:38:55 +0000 Subject: [PATCH 152/710] Missing migration for r1090. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1091 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- db/migrate/088_add_custom_fields_default_value.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 db/migrate/088_add_custom_fields_default_value.rb diff --git a/db/migrate/088_add_custom_fields_default_value.rb b/db/migrate/088_add_custom_fields_default_value.rb new file mode 100644 index 000000000..33a39ec6e --- /dev/null +++ b/db/migrate/088_add_custom_fields_default_value.rb @@ -0,0 +1,9 @@ +class AddCustomFieldsDefaultValue < ActiveRecord::Migration + def self.up + add_column :custom_fields, :default_value, :text + end + + def self.down + remove_column :custom_fields, :default_value + end +end From dad5f6d403dcf70e11065da199f05be49ca2bc48 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 21 Jan 2008 18:52:45 +0000 Subject: [PATCH 153/710] Fixed search with all words (broken in r994). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1092 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/fixtures/issues.yml | 2 +- test/functional/search_controller_test.rb | 33 ++++++++++++++++++- .../lib/acts_as_searchable.rb | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml index fc5b48dee..b3c662039 100644 --- a/test/fixtures/issues.yml +++ b/test/fixtures/issues.yml @@ -22,7 +22,7 @@ issues_002: id: 2 fixed_version_id: category_id: - description: Ingredients should be classified by categories + description: Ingredients of the recipe should be classified by categories tracker_id: 2 assigned_to_id: 3 author_id: 2 diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index 330cd0de0..63f1097d6 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -5,7 +5,7 @@ require 'search_controller' class SearchController; def rescue_action(e) raise e end; end class SearchControllerTest < Test::Unit::TestCase - fixtures :projects, :issues, :custom_fields, :custom_values + fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values def setup @controller = SearchController.new @@ -47,6 +47,37 @@ class SearchControllerTest < Test::Unit::TestCase assert results.include?(Issue.find(3)) end + def test_search_all_words + # 'all words' is on by default + get :index, :id => 1, :q => 'recipe updating saving' + results = assigns(:results) + assert_not_nil results + assert_equal 1, results.size + assert results.include?(Issue.find(3)) + end + + def test_search_one_of_the_words + get :index, :id => 1, :q => 'recipe updating saving', :submit => 'Search' + results = assigns(:results) + assert_not_nil results + assert_equal 3, results.size + assert results.include?(Issue.find(3)) + end + + def test_search_titles_only_without_result + get :index, :id => 1, :q => 'recipe updating saving', :all_words => '1', :titles_only => '1', :submit => 'Search' + results = assigns(:results) + assert_not_nil results + assert_equal 0, results.size + end + + def test_search_titles_only + get :index, :id => 1, :q => 'recipe', :titles_only => '1', :submit => 'Search' + results = assigns(:results) + assert_not_nil results + assert_equal 2, results.size + end + def test_quick_jump_to_issue # issue of a public project get :index, :q => "3" diff --git a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb index 1dd88978c..dff76b913 100644 --- a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -85,7 +85,7 @@ module Redmine end end - sql = ([token_clauses.join(' OR ')] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') + 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])}')" From 97e7432ce6da80dea633cd99e934943925a774b5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 22 Jan 2008 17:17:52 +0000 Subject: [PATCH 154/710] Fixed migration 87 (mysql: TEXT column can't have a default value). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1093 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- db/migrate/087_change_projects_description_to_text.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/087_change_projects_description_to_text.rb b/db/migrate/087_change_projects_description_to_text.rb index d215840aa..2829655d1 100644 --- a/db/migrate/087_change_projects_description_to_text.rb +++ b/db/migrate/087_change_projects_description_to_text.rb @@ -1,6 +1,6 @@ class ChangeProjectsDescriptionToText < ActiveRecord::Migration def self.up - change_column :projects, :description, :text, :default => '' + change_column :projects, :description, :text end def self.down From 91dc13f4b22c45bee5cfd1d783d6544ae79afa04 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 17:25:11 +0000 Subject: [PATCH 155/710] Show explicit error message when the scm command failed (eg. when svn binary is not available). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1094 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/application.rb | 5 ++++ app/controllers/repositories_controller.rb | 23 ++++++++++++++++--- lang/bg.yml | 4 +++- lang/cs.yml | 4 +++- lang/de.yml | 4 +++- lang/en.yml | 3 ++- lang/es.yml | 4 +++- lang/fi.yml | 3 ++- lang/fr.yml | 2 ++ lang/he.yml | 4 +++- lang/it.yml | 4 +++- lang/ja.yml | 4 +++- lang/ko.yml | 4 +++- lang/lt.yml | 6 +++-- lang/nl.yml | 4 +++- lang/pl.yml | 4 +++- lang/pt-br.yml | 4 +++- lang/pt.yml | 4 +++- lang/ro.yml | 4 +++- lang/ru.yml | 4 +++- lang/sr.yml | 4 +++- lang/sv.yml | 4 +++- lang/zh-tw.yml | 4 +++- lang/zh.yml | 4 +++- lib/redmine/scm/adapters/abstract_adapter.rb | 2 +- .../scm/adapters/subversion_adapter.rb | 1 - 26 files changed, 90 insertions(+), 27 deletions(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 66cec7d2a..f4883cf52 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -137,6 +137,11 @@ class ApplicationController < ActionController::Base return false end + def render_error(msg) + flash.now[:error] = msg + render :nothing => true, :layout => !request.xhr?, :status => 500 + end + def render_feed(items, options={}) @items = items || [] @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 6c2b088cc..542eb93ea 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -56,6 +56,8 @@ class RepositoriesController < ApplicationController # latest changesets @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") show_error and return unless @entries || @changesets.any? + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def browse @@ -65,12 +67,16 @@ class RepositoriesController < ApplicationController else show_error unless @entries end + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def changes @entry = @repository.scm.entry(@path, @rev) show_error and return unless @entry @changesets = @repository.changesets_for_path(@path) + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def revisions @@ -97,11 +103,15 @@ class RepositoriesController < ApplicationController # Prevent empty lines when displaying a file with Windows style eol @content.gsub!("\r\n", "\n") end + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def annotate @annotate = @repository.scm.annotate(@path, @rev) show_error and return if @annotate.nil? || @annotate.empty? + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def revision @@ -119,6 +129,8 @@ class RepositoriesController < ApplicationController end rescue ChangesetNotFound show_error + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def diff @@ -137,6 +149,8 @@ class RepositoriesController < ApplicationController @diff = @repository.diff(@path, @rev, @rev_to, @diff_type) show_error and return unless @diff end + rescue Redmine::Scm::Adapters::CommandFailed => e + show_error_command_failed(e.message) end def stats @@ -176,9 +190,12 @@ private render_404 end - def show_error - flash.now[:error] = l(:notice_scm_error) - render :nothing => true, :layout => true + def show_error_not_found + render_error l(:error_scm_not_found) + end + + def show_error_command_failed(msg) + render_error l(:error_scm_command_failed, msg) end def graph_commits_per_month(repository) diff --git a/lang/bg.yml b/lang/bg.yml index 2c83b966f..00b20308b 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -68,12 +68,14 @@ notice_successful_delete: Успешно изтриване. notice_successful_connection: Успешно свързване. notice_file_not_found: Несъществуваща или преместена страница. notice_locking_conflict: Друг потребител променя тези данни в момента. -notice_scm_error: Несъществуващ обект в склада. notice_not_authorized: Нямате право на достъп до тази страница. notice_email_sent: Изпратен e-mail на %s notice_email_error: Грешка при изпращане на e-mail (%s) notice_feeds_access_key_reseted: Вашия ключ за RSS достъп беше променен. +error_scm_not_found: Несъществуващ обект в склада. +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Вашата парола mail_body_lost_password: 'За да смените паролата си, използвайте следния линк:' mail_subject_register: Активация на акаунт diff --git a/lang/cs.yml b/lang/cs.yml index cde58fe17..ab159c2c4 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -68,12 +68,14 @@ notice_successful_delete: Úspěšné smazání. notice_successful_connection: Úspěšné připojení. notice_file_not_found: Stránka na kterou se snažíte zobrazit neexistuje nebo byla smazána. notice_locking_conflict: Údaje byly změněny jiným uživatelem. -notice_scm_error: Entry and/or revision doesn't exist in the repository. notice_not_authorized: Nemáte dostatečná práva pro zobrazení této stránky. notice_email_sent: Na adresu %s byl odeslán email notice_email_error: Při odesílání emailu nastala chyba (%s) notice_feeds_access_key_reseted: Váš klíč pro přístup k RSS byl resetován. +error_scm_not_found: "Entry and/or revision doesn't exist in the repository." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Vaše heslo mail_body_lost_password: 'To change your Redmine password, click on the following link:' mail_subject_register: aktivace účtu diff --git a/lang/de.yml b/lang/de.yml index a23e9c533..0c1ba3575 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -68,12 +68,14 @@ notice_successful_delete: Erfolgreich gelöscht. notice_successful_connection: Verbindung erfolgreich. notice_file_not_found: Anhang besteht nicht oder ist gelöscht worden. notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert. -notice_scm_error: Eintrag und/oder Revision besteht nicht im Projektarchiv. notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen. notice_email_sent: Eine E-Mail wurde an %s gesendet. notice_email_error: Beim Senden einer E-Mail ist ein Fehler aufgetreten (%s). notice_feeds_access_key_reseted: Ihr RSS-Zugriffsschlüssel wurde zurückgesetzt. +error_scm_not_found: "Eintrag und/oder Revision besteht nicht im Projektarchiv." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Ihr Redmine-Kennwort mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:' mail_subject_register: Redmine Kontoaktivierung diff --git a/lang/en.yml b/lang/en.yml index dc7b04dd2..32fd9926f 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -68,7 +68,6 @@ notice_successful_delete: Successful deletion. notice_successful_connection: Successful connection. notice_file_not_found: The page you were trying to access doesn't exist or has been removed. notice_locking_conflict: Data have been updated by another user. -notice_scm_error: Entry and/or revision doesn't exist in the repository. 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) @@ -79,6 +78,8 @@ notice_account_pending: "Your account was created and is now pending administrat notice_default_data_loaded: Default configuration successfully loaded. error_can_t_load_default_data: "Default configuration could not be loaded: %s" +error_scm_not_found: "Entry and/or revision doesn't exist in the repository." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" mail_subject_lost_password: Your Redmine password mail_body_lost_password: 'To change your Redmine password, click on the following link:' diff --git a/lang/es.yml b/lang/es.yml index 8e9ffaa2f..ac3b34c19 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -68,9 +68,11 @@ notice_successful_delete: Borrado correcto. notice_successful_connection: Conexión correcta. notice_file_not_found: La página a la que intentas acceder no existe. notice_locking_conflict: Los datos han sido modificados por otro usuario. -notice_scm_error: La entrada y/o la revisión no existe en el repositorio. notice_not_authorized: No tiene autorización para acceder a esta página. +error_scm_not_found: "La entrada y/o la revisión no existe en el repositorio." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Tu contraseña del CIYAT - Gestor de Solicitudes mail_body_lost_password: 'Para cambiar su contraseña de Redmine, haga click en el siguiente enlace:' mail_subject_register: Activación de la cuenta del CIYAT - Gestor de Solicitudes diff --git a/lang/fi.yml b/lang/fi.yml index 497647ad9..eeba20c35 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -68,7 +68,6 @@ notice_successful_delete: Poisto onnistui. notice_successful_connection: Yhteyden muodostus onnistui. notice_file_not_found: Hakemaasi sivua ei löytynyt tai se on poistettu. notice_locking_conflict: Toinen käyttäjä on päivittänyt tiedot. -notice_scm_error: Syötettä ja/tai versiota ei löydy säiliöstä. 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) @@ -79,6 +78,8 @@ notice_account_pending: "Tilisi on luotu ja odottaa ylläpitäjän hyväksyntä notice_default_data_loaded: Vakio asetusten palautus onnistui. error_can_t_load_default_data: "Vakio asetuksia ei voitu ladata: %s" +error_scm_not_found: "Syötettä ja/tai versiota ei löydy säiliöstä." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" mail_subject_lost_password: Sinun Redmine salasanasi mail_body_lost_password: 'Vaihtaaksesi Redmine salasanasi, paina seuraavaa linkkiä:' diff --git a/lang/fr.yml b/lang/fr.yml index 854ed5859..be93da9a1 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -79,6 +79,8 @@ notice_account_pending: "Votre compte a été créé et attend l'approbation de notice_default_data_loaded: Paramétrage par défaut chargé avec succès. error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage: %s" +error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt." +error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt: %s" mail_subject_lost_password: Votre mot de passe redMine mail_body_lost_password: 'Pour changer votre mot de passe Redmine, cliquez sur le lien suivant:' diff --git a/lang/he.yml b/lang/he.yml index 3e8ad4160..ad3331624 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -68,7 +68,6 @@ notice_successful_delete: מחיקה מוצלחת. notice_successful_connection: חיבור מוצלח. notice_file_not_found: הדף שאת\ה מנסה לגשת אליו אינו קיים או שהוסר. notice_locking_conflict: המידע עודכן על ידי משתמש אחר. -notice_scm_error: כניסה ו\או גירסא אינם קיימים במאגר. notice_not_authorized: אינך מורשה לראות דף זה. notice_email_sent: דוא"ל נשלח לכתובת %s notice_email_error: ארעה שגיאה בעט שליחת הדוא"ל (%s) @@ -76,6 +75,9 @@ notice_feeds_access_key_reseted: מפתח ה-RSS שלך אופס. notice_failed_to_save_issues: "נכשרת בשמירת %d נושא\ים ב %d נבחרו: %s." notice_no_issue_selected: "לא נבחר אף נושא! בחר בבקשה את הנושאים שברצונך לערוך." +error_scm_not_found: כניסה ו\או גירסא אינם קיימים במאגר. +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: סיסמת ה-Redmine שלך mail_body_lost_password: 'לשינו סיסמת ה-Redmine שלך,לחץ על הקישור הבא:' mail_subject_register: הפעלת חשבון Redmine diff --git a/lang/it.yml b/lang/it.yml index ca4b3391a..3b3131211 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -68,12 +68,14 @@ 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_scm_error: La risorsa e/o la versione non esistono nel repository. notice_not_authorized: You are not authorized to access this page. notice_email_sent: An email was sent to %s notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. +error_scm_not_found: "La risorsa e/o la versione non esistono nel repository." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Password redMine mail_body_lost_password: 'Per cambiare la password, usate il seguente collegamento:' mail_subject_register: Attivazione utenza redMine diff --git a/lang/ja.yml b/lang/ja.yml index 713af6b46..08423c824 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -69,12 +69,14 @@ notice_successful_delete: 削除しました。 notice_successful_connection: 接続しました。 notice_file_not_found: アクセスしようとしたページは存在しないか削除されています。 notice_locking_conflict: 別のユーザがデータを更新しています。 -notice_scm_error: リポジトリに、エントリ/リビジョンが存在しません。 notice_not_authorized: このページにアクセスするには認証が必要です。 notice_email_sent: %s宛にメールを送信しました。 notice_email_error: メール送信中にエラーが発生しました(%s) notice_feeds_access_key_reseted: RSSアクセスキーを初期化しました。 +error_scm_not_found: リポジトリに、エントリ/リビジョンが存在しません。 +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Redmineパスワード mail_body_lost_password: 'パスワードを変更するには、以下のリンクをたどってください:' mail_subject_register: Redmineアカウントが有効になりました diff --git a/lang/ko.yml b/lang/ko.yml index 564e67253..0b556840d 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -68,7 +68,6 @@ notice_successful_delete: 삭제 성공. notice_successful_connection: 연결 성공. notice_file_not_found: 요청하신 페이지는 삭제되었거나 옮겨졌습니다. notice_locking_conflict: 다른 사용자에 의해서 데이터가 변경되었습니다. -notice_scm_error: 소스 저장소에 해당 내용이 존재하지 않습니다. notice_not_authorized: 이 페이지에 접근할 권한이 없습니다. notice_email_sent: %s 님에게 Email이 발송되었습니다. notice_email_error: 메일을 전송하는 과정에 오류가 발생했습니다. (%s) @@ -76,6 +75,9 @@ notice_feeds_access_key_reseted: RSS에 접근가능한 열쇠(key)가 생성되 notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." notice_no_issue_selected: "티켓이 선택되지 않았습니다. 수정하기 원하는 티켓을 선택하세요" +error_scm_not_found: 소스 저장소에 해당 내용이 존재하지 않습니다. +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: 당신의 비밀번호 mail_body_lost_password: '비밀번호를 변경하기 위해서 링크를 이용하세요' mail_subject_register: 당신의 계정 활성화 diff --git a/lang/lt.yml b/lang/lt.yml index d95da7469..5e8273b91 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -68,7 +68,6 @@ notice_successful_delete: Sėkmingas panaikinimas. notice_successful_connection: Sėkmingas susijungimas. notice_file_not_found: Puslapis, į kurį ketinate įeiti, neegzistuoja arba pašalintas. notice_locking_conflict: Duomenys atnaujinti kito vartotojo. -notice_scm_error: Duomenys ir/ar pakeitimai saugykloje(repozitorojoje) neegzistuoja. notice_not_authorized: Jūs neturite teisių gauti prieigą prie šio puslapio. notice_email_sent: Laiškas išsiųstas %s notice_email_error: Laiško siųntimo metu įvyko klaida (%s) @@ -76,7 +75,10 @@ notice_feeds_access_key_reseted: Jūsų RSS raktas buvo atnaujintas. notice_failed_to_save_issues: "Nepavyko išsaugoti %d problemos(ų) iš %d pasirinkto: %s." notice_no_issue_selected: "Nepasirinkta nė viena problema! Prašom pažymėti problemą, kurią norite redaguoti." notice_account_pending: "Jūsų paskyra buvo sukūrta ir dabar laukiama administratoriaus patvirtinimo." - + +error_scm_not_found: "Duomenys ir/ar pakeitimai saugykloje(repozitorojoje) neegzistuoja." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Jūsų Redmine slaptažodis mail_body_lost_password: 'Norėdami pakeisti Redmine slaptažodį, spauskite nuorodą:' mail_subject_register: 'Redmine paskyros aktyvavymas' diff --git a/lang/nl.yml b/lang/nl.yml index 8d29ab902..f81247623 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -68,12 +68,14 @@ notice_successful_delete: Verwijderen succesvol. notice_successful_connection: Verbinding succesvol. notice_file_not_found: De pagina die U probeerde te benaderen bestaat niet of is verwijderd. notice_locking_conflict: De gegevens zijn gewijzigd door een andere gebruiker. -notice_scm_error: Deze ingang of revisie bestaat niet in de repository. notice_not_authorized: Het is U niet toegestaan om deze pagina te raadplegen. notice_email_sent: An email was sent to %s notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. +error_scm_not_found: "Deze ingang of revisie bestaat niet in de repository." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Uw redMine wachtwoord mail_body_lost_password: 'Gebruik de volgende link om Uw wachtwoord te wijzigen:' mail_subject_register: redMine account activatie diff --git a/lang/pl.yml b/lang/pl.yml index 69bf5306a..ec7eec222 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -68,9 +68,11 @@ notice_successful_delete: Udane usunięcie. notice_successful_connection: Udane nawiązanie połączenia. notice_file_not_found: Strona do której próbujesz się dostać nie istnieje lub została usunięta. notice_locking_conflict: Dane poprawione przez innego użytkownika. -notice_scm_error: Wejście i/lub zmiana nie istnieje w repozytorium. notice_not_authorized: Nie jesteś autoryzowany by zobaczyć stronę. +error_scm_not_found: "Wejście i/lub zmiana nie istnieje w repozytorium." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Twoje hasło do redMine mail_body_lost_password: 'W celu zmiany swojego hasła użyj poniższego odnośnika:' mail_subject_register: Aktywacja konta w redMine diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 926c913a0..629ca2c38 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -68,12 +68,14 @@ notice_successful_delete: Apagado com sucesso. notice_successful_connection: Conectado com sucesso. notice_file_not_found: A pagina que voce esta tentando acessar nao existe ou foi excluida. notice_locking_conflict: Os dados foram atualizados por um outro usuario. -notice_scm_error: A entrada e/ou a revisao nao existem no repositorio. notice_not_authorized: You are not authorized to access this page. notice_email_sent: An email was sent to %s notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. +error_scm_not_found: "A entrada e/ou a revisao nao existem no repositorio." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Sua senha do redMine. mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:' mail_subject_register: Ativacao de conta do redMine. diff --git a/lang/pt.yml b/lang/pt.yml index 88fd9cf74..8add9b6eb 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -68,12 +68,14 @@ notice_successful_delete: Apagado com sucesso. notice_successful_connection: Conectado com sucesso. notice_file_not_found: A página que você está tentando acessar não existe ou foi excluída. notice_locking_conflict: Os dados foram atualizados por um outro usuário. -notice_scm_error: A entrada e/ou a revisão não existem no repositório. notice_not_authorized: Você não está autorizado a acessar esta página. notice_email_sent: An email was sent to %s notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. +error_scm_not_found: "A entrada e/ou a revisão não existem no repositório." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Sua senha do redMine. mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:' mail_subject_register: Ativação de conta do redMine. diff --git a/lang/ro.yml b/lang/ro.yml index 2eb17609c..b5413fdd4 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -68,12 +68,14 @@ notice_successful_delete: Stergere cu succes. notice_successful_connection: Conectare cu succes. notice_file_not_found: Pagina dorita nu exista sau nu mai este valabila. notice_locking_conflict: Informatiile au fost modificate de un alt utilizator. -notice_scm_error: Articolul sau reviziunea nu exista in stoc (Repository). notice_not_authorized: Nu aveti autorizatia sa accesati aceasta pagina. notice_email_sent: Un e-mail a fost trimis la adresa %s notice_email_error: Eroare in trimiterea e-mailului (%s) notice_feeds_access_key_reseted: Parola de acces RSS a fost resetat. +error_scm_not_found: "Articolul sau reviziunea nu exista in stoc (Repository)." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Your Redmine password mail_body_lost_password: 'To change your Redmine password, click on the following link:' mail_subject_register: Redmine account activation diff --git a/lang/ru.yml b/lang/ru.yml index 62799bdf8..020a888b7 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -68,7 +68,6 @@ notice_successful_delete: Удаление успешно завершено. notice_successful_connection: Подключение успешно установлено. notice_file_not_found: Страница, на которую вы пытаетесь зайти, не существует или удалена. notice_locking_conflict: Информация обновлена другим пользователем. -notice_scm_error: Записи и/или исправления нет в репозитории. notice_not_authorized: У вас нет прав для посещения данной страницы. notice_email_sent: Отправлено письмо %s notice_email_error: Во время отправки письма произошла ошибка (%s) @@ -76,6 +75,9 @@ notice_feeds_access_key_reseted: Ваш ключ доступа RSS был пе notice_failed_to_save_issues: "Не удалось сохранить %d пункт(ов)из %d выбранных: %s." notice_no_issue_selected: "Не выбрано ни одной задачи! Пожалуйста, отметьте задачи, которые вы хотите отредактировать." +error_scm_not_found: Записи и/или исправления нет в репозитории. +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Ваш Redmine пароль mail_body_lost_password: 'Для изменения Redmine пароля, зайдите по следующей ссылке:' mail_subject_register: Активация учетной записи Redmine diff --git a/lang/sr.yml b/lang/sr.yml index 6d88d34ff..0c64ee128 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -68,7 +68,6 @@ notice_successful_delete: Uspešno brisanje. notice_successful_connection: Uspešna konekcija. notice_file_not_found: Stranica kojoj pokušavate da pristupite ne postoji ili je uklonjena. notice_locking_conflict: Podaci su izmenjeni od strane drugog korisnika. -notice_scm_error: Unos i/ili revizija ne postoji u spremištu. notice_not_authorized: Niste ovlašćeni da pristupite ovoj stranici. notice_email_sent: Email je poslat %s notice_email_error: Došlo je do greške pri slanju maila (%s) @@ -76,6 +75,9 @@ notice_feeds_access_key_reseted: Vaš RSS pristup je resetovan. notice_failed_to_save_issues: "Neuspešno snimanje %d kartica na %d izabrano: %s." notice_no_issue_selected: "Nijedna kartica nije izabrana! Molim, izaberite kartice koje želite za editujete." +error_scm_not_found: "Unos i/ili revizija ne postoji u spremištu." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Vaša redMine lozinka mail_body_lost_password: 'Da biste izmenili vašu Redmine lozinku, kliknite na sledeći link:' mail_subject_register: aktivacija redMine naloga diff --git a/lang/sv.yml b/lang/sv.yml index 5f4a01126..ff199aba4 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -68,12 +68,14 @@ notice_successful_delete: Lyckad borttagning. notice_successful_connection: Lyckad uppkoppling. notice_file_not_found: Sidan du försökte komma åt existerar inte eller har blivit borttagen. notice_locking_conflict: Data har uppdaterats av en annan användare. -notice_scm_error: Inlägg och/eller revision finns inte i repositoriet. notice_not_authorized: You are not authorized to access this page. notice_email_sent: An email was sent to %s notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. +error_scm_not_found: "Inlägg och/eller revision finns inte i repositoriet." +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: Ditt redMine lösenord mail_body_lost_password: 'För att ändra lösenord, följ denna länk:' mail_subject_register: redMine kontoaktivering diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 2a431266e..21d1e9ead 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -68,7 +68,6 @@ notice_successful_delete: 刪除成功 notice_successful_connection: Successful connection. notice_file_not_found: The page you were trying to access doesn't exist or has been removed. notice_locking_conflict: Data have been updated by another user. -notice_scm_error: SCM 儲存庫中找不到這個專案或版本。 notice_not_authorized: 你未被授權存取此頁面。 notice_email_sent: 郵件已經成功寄送至以下收件者: %s notice_email_error: 寄送郵件的過程中發生錯誤 (%s) @@ -78,6 +77,9 @@ notice_no_issue_selected: "No issue is selected! Please, check the issues you wa notice_account_pending: "您的帳號已經建立,正在等待管理員的審核。" notice_default_data_loaded: Default configuration successfully loaded. +error_scm_not_found: SCM 儲存庫中找不到這個專案或版本。 +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + error_can_t_load_default_data: "Default configuration could not be loaded: %s" mail_subject_lost_password: 您的 Redmine 網站密碼 diff --git a/lang/zh.yml b/lang/zh.yml index b87a1515d..de7ec093e 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -71,12 +71,14 @@ notice_successful_delete: 删除成功 notice_successful_connection: 连接成功 notice_file_not_found: 您访问的页面不存在或已被删除。 notice_locking_conflict: 数据已被另一个用户更新 -notice_scm_error: 在版本库中不存在该条目或修订 notice_not_authorized: You are not authorized to access this page. notice_email_sent: An email was sent to %s notice_email_error: An error occurred while sending mail (%s) notice_feeds_access_key_reseted: Your RSS access key was reseted. +error_scm_not_found: 在版本库中不存在该条目或修订 +error_scm_command_failed: "An error occurred when trying to access the repository: %s" + mail_subject_lost_password: 您的redMine口令 mail_body_lost_password: 'To change your Redmine password, click on the following link:' mail_subject_register: redMine帐户激活 diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 633637ab4..5db4964e9 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -120,7 +120,7 @@ module Redmine rescue Errno::ENOENT => e # The command failed, log it and re-raise logger.error("SCM command failed: #{cmd}\n with: #{e.message}") - raise CommandFailed + raise CommandFailed.new(e.message) end end end diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index f698f4a62..5b6bfa97c 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -66,7 +66,6 @@ module Redmine entries = Entries.new cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}" cmd << credentials_string - cmd << " 2>&1" shellout(cmd) do |io| output = io.read begin From d54ba39e7aced22dd352273ec4af4b345a827363 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 17:48:48 +0000 Subject: [PATCH 156/710] Fixed: can not lock a topic when creating it. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1095 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/message.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/message.rb b/app/models/message.rb index 038665cce..12b1cd990 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -36,7 +36,7 @@ class Message < ActiveRecord::Base def validate_on_create # Can not reply to a locked topic - errors.add_to_base 'Topic is locked' if root.locked? + errors.add_to_base 'Topic is locked' if root.locked? && self != root end def after_create From 2d9e669e8541c591b488283416776378b8f69da9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 18:21:42 +0000 Subject: [PATCH 157/710] Added preview for issue notes. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1096 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 +- app/views/issues/_update.rhtml | 14 +++++++++++++- app/views/issues/show.rhtml | 1 - 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 643a4e0ef..fdece9e1a 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -242,7 +242,7 @@ class IssuesController < ApplicationController def preview issue = Issue.find_by_id(params[:id]) @attachements = issue.attachments if issue - @text = params[:issue][:description] + @text = (params[:issue] ? params[:issue][:description] : nil) || params[:notes] render :partial => 'common/preview' end diff --git a/app/views/issues/_update.rhtml b/app/views/issues/_update.rhtml index 3cf593806..49d1473d9 100644 --- a/app/views/issues/_update.rhtml +++ b/app/views/issues/_update.rhtml @@ -1,4 +1,6 @@ -<% labelled_tabular_form_for(:issue, @issue, :url => {:action => 'update', :id => @issue}, :html => {:multipart => true}) do |f| %> +<% labelled_tabular_form_for(:issue, @issue, :url => {:action => 'update', :id => @issue}, + :html => {:multipart => true, + :id => 'issue-form'}) do |f| %>
    <% unless @status_options.empty? %> @@ -39,4 +41,14 @@
    <%= submit_tag l(:button_submit) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('issue-form')", + :complete => "window.location.hash='preview'" + }, :accesskey => accesskey(:preview) %> | +<%= toggle_link l(:button_cancel), 'update' %> <% end %> + +
    diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 45423a2e0..d29b1b88f 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -94,7 +94,6 @@ end %> <% end %> From d8ac6d2b79397ef8414b82164849f23cae2df5f1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 19:58:11 +0000 Subject: [PATCH 158/710] Issue properties below the description textarea. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1097 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_form.rhtml | 19 +++++++++++-------- app/views/issues/_form_custom_fields.rhtml | 2 +- app/views/issues/new.rhtml | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index 0f5e10de9..d11cea84c 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -8,6 +8,13 @@ :with => "Form.serialize('issue-form')" %> <% end %> +

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

    +

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

    +
    <% if @issue.new_record? %>

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

    @@ -22,6 +29,9 @@ l(:label_issue_category_new), 'category[name]', {:controller => 'projects', :action => 'add_issue_category', :id => @project}, :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %>

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

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

    -

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

    -

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

    -

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

    - +
    <%= render :partial => 'form_custom_fields', :locals => {:values => @custom_values} %> <% if @issue.new_record? %> diff --git a/app/views/issues/_form_custom_fields.rhtml b/app/views/issues/_form_custom_fields.rhtml index e5a98e30e..1268bb1f9 100644 --- a/app/views/issues/_form_custom_fields.rhtml +++ b/app/views/issues/_form_custom_fields.rhtml @@ -2,7 +2,7 @@ <% i = 1 %> <% for @custom_value in values %>

    <%= custom_field_tag_with_label @custom_value %>

    - <% if i >= values.size / 2 %> + <% if i == values.size / 2 %>
    <% end %> <% i += 1 %> diff --git a/app/views/issues/new.rhtml b/app/views/issues/new.rhtml index 4fedc6895..8ff07f226 100644 --- a/app/views/issues/new.rhtml +++ b/app/views/issues/new.rhtml @@ -1,4 +1,4 @@ -

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

    +

    <%=l(:label_issue_new)%>

    <% labelled_tabular_form_for :issue, @issue, :html => {:multipart => true, :id => 'issue-form'} do |f| %> From 2247700f24ae6858ace3de11ea88ec45f43d5dd7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 20:07:17 +0000 Subject: [PATCH 159/710] Fixed: Incorrect filtering for unset values when using 'is not' filter. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1098 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/query.rb b/app/models/query.rb index 3a1b5a19e..adfd5f42a 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -300,7 +300,7 @@ class Query < ActiveRecord::Base when "=" sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" when "!" - sql = sql + "#{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + 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" when "*" From b0f3de5c3b75c6e69ff5df31eac9f12c6cce064e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 22:19:24 +0000 Subject: [PATCH 160/710] Fixed: PostgreSQL issues_seq_id not updated when using Trac importer. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1099 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 828027b87..63a719b35 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -372,6 +372,17 @@ namespace :redmine do migrated_custom_values += 1 end end + + # update issue id sequence if needed + begin + case ActiveRecord::Base.connection.adapter_name.downcase + when 'mysql' + # nothing to do + when 'postgresql' + sql = "SELECT setval('#{Issue.table_name}_id_seq', (SELECT MAX(id) FROM #{Issue.table_name}))" + ActiveRecord::Base.connection.execute(sql) + end + end puts # Wiki @@ -478,7 +489,7 @@ namespace :redmine do if !project # create the target project project = Project.new :name => identifier.humanize, - :description => identifier.humanize + :description => '' project.identifier = identifier puts "Unable to create a project with identifier '#{identifier}'!" unless project.save # enable issues and wiki for the created project From 7a1e928b8db0cc0a584011973042522cd7b38d4d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 23 Jan 2008 22:31:26 +0000 Subject: [PATCH 161/710] Mantis importer: * do not truncate projects descriptions * encode attachment filenames to utf8 git-svn-id: http://redmine.rubyforge.org/svn/trunk@1100 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_mantis.rake | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/tasks/migrate_from_mantis.rake b/lib/tasks/migrate_from_mantis.rake index 6d8d55e7c..36e7e1514 100644 --- a/lib/tasks/migrate_from_mantis.rake +++ b/lib/tasks/migrate_from_mantis.rake @@ -118,10 +118,6 @@ task :migrate_from_mantis => :environment do read_attribute(:name)[0..29] end - def description - read_attribute(:description).blank? ? read_attribute(:name) : read_attribute(:description)[0..254] - end - def identifier read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-') end @@ -186,7 +182,7 @@ task :migrate_from_mantis => :environment do end def original_filename - filename + MantisMigrate.encode(filename) end def content_type @@ -445,7 +441,6 @@ task :migrate_from_mantis => :environment do end end - private def self.encode(text) @ic.iconv text rescue From e13f49d91951a82d9989c2a0f0c4981832f2664f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 24 Jan 2008 18:15:38 +0000 Subject: [PATCH 162/710] Prevent unexpected nil in custom value validation. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1101 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/custom_value.rb | 31 ++++++++------ test/unit/custom_value_test.rb | 74 +++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 14 deletions(-) diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index 94b797bcc..98ce6b168 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -27,19 +27,24 @@ class CustomValue < ActiveRecord::Base protected def validate - errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank? - errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) - errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0 - errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length - case custom_field.field_format - when 'int' - errors.add(:value, :activerecord_error_not_a_number) unless value.blank? || value =~ /^[+-]?\d+$/ - when 'float' - begin; !value.blank? && Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end - when 'date' - errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.blank? - when 'list' - errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) or value.blank? + if value.blank? + errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank? + else + errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp) + errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length + errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length + + # Format specific validations + case custom_field.field_format + when 'int' + errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/ + when 'float' + begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end + when 'date' + errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ + when 'list' + errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value) + end end end end diff --git a/test/unit/custom_value_test.rb b/test/unit/custom_value_test.rb index 4d488bae2..11578ae6b 100644 --- a/test/unit/custom_value_test.rb +++ b/test/unit/custom_value_test.rb @@ -20,7 +20,79 @@ require File.dirname(__FILE__) + '/../test_helper' class CustomValueTest < Test::Unit::TestCase fixtures :custom_fields - def test_float_field + def test_string_field_validation_with_blank_value + f = CustomField.new(:field_format => 'string') + v = CustomValue.new(:custom_field => f) + + v.value = nil + assert v.valid? + v.value = '' + assert v.valid? + + f.is_required = true + v.value = nil + assert !v.valid? + v.value = '' + assert !v.valid? + end + + def test_string_field_validation_with_min_and_max_lengths + f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5) + v = CustomValue.new(:custom_field => f, :value => '') + assert v.valid? + v.value = 'a' + assert !v.valid? + v.value = 'a' * 2 + assert v.valid? + v.value = 'a' * 6 + assert !v.valid? + end + + def test_string_field_validation_with_regexp + f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$') + v = CustomValue.new(:custom_field => f, :value => '') + assert v.valid? + v.value = 'abc' + assert !v.valid? + v.value = 'ABC' + assert v.valid? + end + + def test_date_field_validation + f = CustomField.new(:field_format => 'date') + v = CustomValue.new(:custom_field => f, :value => '') + assert v.valid? + v.value = 'abc' + assert !v.valid? + v.value = '1975-07-14' + assert v.valid? + end + + def test_list_field_validation + f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2']) + v = CustomValue.new(:custom_field => f, :value => '') + assert v.valid? + v.value = 'abc' + assert !v.valid? + v.value = 'value2' + assert v.valid? + end + + def test_int_field_validation + f = CustomField.new(:field_format => 'int') + v = CustomValue.new(:custom_field => f, :value => '') + assert v.valid? + v.value = 'abc' + assert !v.valid? + v.value = '123' + assert v.valid? + v.value = '+123' + assert v.valid? + v.value = '-123' + assert v.valid? + end + + def test_float_field_validation v = CustomValue.new(:customized => User.find(:first), :custom_field => UserCustomField.find_by_name('Money')) v.value = '11.2' assert v.save From 3d5d1a38e3d48e1fda7eee751563cbf8c3b0f00b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 24 Jan 2008 18:31:02 +0000 Subject: [PATCH 163/710] =?UTF-8?q?Translations=20updates:=20*=20Russian?= =?UTF-8?q?=20(Michael=20Pirogov)=20*=20Finnish=20(Antti=20Perki=C3=B6m?= =?UTF-8?q?=C3=A4ki)=20*=20Traditional=20Chinese=20wiki=20toolbar=20(Short?= =?UTF-8?q?ie=20Lo)=20*=20German=20wiki=20toolbar=20(Thomas=20L=C3=B6ber)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1102 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/fi.yml | 2 +- lang/ru.yml | 97 ++++++++++--------- .../jstoolbar/lang/jstoolbar-de.js | 28 +++--- .../jstoolbar/lang/jstoolbar-zh-tw.js | 28 +++--- 4 files changed, 79 insertions(+), 76 deletions(-) diff --git a/lang/fi.yml b/lang/fi.yml index eeba20c35..d70cf396a 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -568,4 +568,4 @@ default_activity_development: Kehitys enumeration_issue_priorities: Tapahtuman prioriteetit enumeration_doc_categories: Dokumentin luokat enumeration_activities: Aktiviteetit (ajan seuranta) -label_associated_revisions: Associated revisions +label_associated_revisions: Liittyvät versiot diff --git a/lang/ru.yml b/lang/ru.yml index 020a888b7..6636ed56f 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -74,8 +74,10 @@ notice_email_error: Во время отправки письма произош notice_feeds_access_key_reseted: Ваш ключ доступа RSS был перезапущен. notice_failed_to_save_issues: "Не удалось сохранить %d пункт(ов)из %d выбранных: %s." notice_no_issue_selected: "Не выбрано ни одной задачи! Пожалуйста, отметьте задачи, которые вы хотите отредактировать." +notice_account_pending: "Ваша учетная запись уже создан и ожидает подтверждения администратора." +notice_default_data_loaded: Была загружена конфигурация по-умолчанию. -error_scm_not_found: Записи и/или исправления нет в репозитории. +error_scm_not_found: Хранилилище не содержит записи и/или исправления. error_scm_command_failed: "An error occurred when trying to access the repository: %s" mail_subject_lost_password: Ваш Redmine пароль @@ -84,6 +86,9 @@ mail_subject_register: Активация учетной записи Redmine mail_body_register: 'Для активации учетной записи Redmine, зайдите по следующей ссылке:' mail_body_account_information_external: Вы можете использовать вашу "%s" учетную запись для входа в Redmine. mail_body_account_information: Информация по Вашей учетной записи Redmine +mail_subject_account_activation_request: Запрос на активацию пользователя в системе Redmine +mail_body_account_activation_request: 'Новый пользователь (%s) зарегистрирован. Учетная запись ожидает вашего утверждения:' + gui_validation_error: 1 ошибка gui_validation_error_plural: %d ошибки(ок) @@ -126,7 +131,7 @@ field_user: Пользователь field_role: Роль field_homepage: Стартовая страница field_is_public: Публичный -field_parent: Подпроект +field_parent: Родительский проект field_is_in_chlog: Задачи, отображаемые в журнале изменений field_is_in_roadmap: Задачи, отображаемые в оперативном плане field_login: Вход @@ -168,7 +173,9 @@ field_assignable: Задача может быть назначена этой field_redirect_existing_links: Перенаправить существующие ссылки field_estimated_hours: Оцененное время field_column_names: Колонки -field_default_value: Default value +field_default_value: Значение по умолчанию +field_time_zone: Часовой пояс +field_searchable: Доступно для поиска setting_app_title: Название приложения setting_app_subtitle: Подзаголовок приложения @@ -182,9 +189,9 @@ setting_mail_from: email адрес для передачи информации setting_host_name: Имя компьютера setting_text_formatting: Форматирование текста setting_wiki_compression: Сжатие истории Wiki -setting_feeds_limit: Ограничения вводимого содержания -setting_autofetch_changesets: Автоматически следить за коммитами -setting_sys_api_enabled: Разрешить WS для управления репозиторием +setting_feeds_limit: Ограничение кол-ва заголовков для RSS потока +setting_autofetch_changesets: Автоматически следить за изменениями хранилища +setting_sys_api_enabled: Разрешить WS для управления хранилищем setting_commit_ref_keywords: Ключевые слова для поиска setting_commit_fix_keywords: Назначение ключевых слов setting_autologin: Автоматический вход @@ -192,9 +199,11 @@ setting_date_format: Формат даты setting_time_format: Формат времени setting_cross_project_issue_relations: Разрешить пересечение задач по проектам setting_issue_list_default_columns: Колонки, отображаемые в списке задач по умолчанию -setting_repositories_encodings: Кодировки репозитория +setting_repositories_encodings: Кодировки хранилища setting_emails_footer: Подстрочные примечания Emailов setting_protocol: Протокол +setting_bcc_recipients: Использовать скрытые списки (bcc) +setting_per_page_options: Кол-во строк на страницу label_user: Пользователь label_user_plural: Пользователи @@ -246,7 +255,7 @@ label_administration: Администрирование label_login: Войти label_logout: Выйти label_help: Помощь -label_reported_issues: Созданые задачи +label_reported_issues: Созданные задачи label_assigned_to_me_issues: Мои задачи label_last_login: Последнее подключение label_last_updates: Последнее обновление @@ -261,7 +270,7 @@ label_auth_source: Режим аутентификации label_auth_source_new: Новый режим аутентификации label_auth_source_plural: Режимы аутентификации label_subproject_plural: Подпроекты -label_min_max_length: Min - Максимальная длина +label_min_max_length: Минимальная - Максимальная длина label_list: Список label_date: Дата label_integer: Целый @@ -333,7 +342,7 @@ label_filter_add: Добавить фильтр label_filter_plural: Фильтры label_equals: есть label_not_equals: нет -label_in_less_than: менее чем +label_in_less_than: Менее чем label_in_more_than: более чем label_in: в label_today: сегодня @@ -344,18 +353,18 @@ label_ago: дней(я) назад label_contains: содержит label_not_contains: не содержит label_day_plural: дней(я) -label_repository: Репозиторий +label_repository: Хранилище label_browse: Искать label_modification: %d изменение label_modification_plural: %d изменений -label_revision: Версия -label_revision_plural: Версии +label_revision: Редакция +label_revision_plural: Редакции label_added: добавлено label_modified: изменено label_deleted: удалено -label_latest_revision: Последняя версия -label_latest_revision_plural: Последние версии -label_view_revisions: Просмотреть версии +label_latest_revision: Последняя редакция +label_latest_revision_plural: Последние редакции +label_view_revisions: Просмотреть редакции label_max_size: Максимальный размер label_on: 'из' label_sort_highest: В начало @@ -385,12 +394,12 @@ label_spent_time: Затраченное время label_f_hour: %.2f час label_f_hour_plural: %.2f часов(а) label_time_tracking: Учет времени -label_change_plural: Изменения +label_change_plural: Правки label_statistics: Статистика -label_commits_per_month: Коммиты на месяц -label_commits_per_author: Коммиты на пользователя +label_commits_per_month: Изменений в месяц +label_commits_per_author: Изменений на пользователя label_view_diff: Просмотреть отличия -label_diff_inline: подключенный +label_diff_inline: вставкой label_diff_side_by_side: рядом label_options: Опции label_copy_workflow_from: Скопировать последовательность действий из @@ -402,7 +411,7 @@ label_loading: Загрузка... label_relation_new: Новое отношение label_relation_delete: Удалить связь label_relates_to: связана с -label_duplicates: дублицирует +label_duplicates: дублирует label_blocks: блокирует label_blocked_by: заблокировано label_precedes: предшествует @@ -449,6 +458,16 @@ label_user_mail_option_all: "Для всех событий во всех мои label_user_mail_option_selected: "Для всех событий только в выбранном проекте..." label_user_mail_option_none: "Только для того, что я просматриваю или в чем я участвую" label_user_mail_no_self_notified: "Не извещать об изменениях которые я сделал сам" +label_registration_activation_by_email: активация учетных записей по email +label_registration_automatic_activation: автоматическая активация учтеных записей +label_registration_manual_activation: активировать учетные записи вручную +label_age: Возраст +label_change_properties: Изменить свойства +label_general: Общее +label_repository_plural: Хранилища +label_associated_revisions: Связанные редакции +label_issues_by: Сортировать по %s +label_display_per_page: 'На страницу: %s' button_login: Вход button_submit: Принять @@ -484,13 +503,15 @@ button_reset: Перезапустить button_rename: Переименовать button_change_password: Изменить пароль button_copy: Копировать +button_annotate: Авторство +button_update: Обновить status_active: Активен status_registered: Зарегистрирован status_locked: Закрыт text_select_mail_notifications: Выберите действия, на которые будет отсылаться уведомление на электронную почту. -text_regexp_info: eg. ^[A-Z0-9]+$ +text_regexp_info: напр. ^[A-Z0-9]+$ text_min_max_length_info: 0 означает отсутствие запретов text_project_destroy_confirmation: Вы настаиваете на удалении данного проекта и всей относящейся к нему информации? text_workflow_edit: Выберите роль и трекер для редактирования последовательности состояний @@ -514,12 +535,15 @@ text_wiki_destroy_confirmation: Вы уверены, что хотите уда text_issue_category_destroy_question: Несколько задач (%d) назначено в данную категорию. Что вы хотите предпринять? text_issue_category_destroy_assignments: Удалить назначения категории text_issue_category_reassign_to: Переназначить задачи для данной категории -text_user_mail_option: "Для невыбранных проектов, вы будете получать уведомления только о том что просматриваете или в чем участвуете (например, вопросы автором которых вы являетесь или которые вам назначенАы)." +text_user_mail_option: "Для невыбранных проектов, вы будете получать уведомления только о том что просматриваете или в чем участвуете (например, вопросы автором которых вы являетесь или которые вам назначены)." +text_caracters_minimum: Должно быть не менее %d знаков. +text_load_default_configuration: Загрузить конфигурацию по-умолчанию +text_no_configuration_data: "Роли, трекеры, статусы задач и оперативный план не были сконфигурированы.\nНастоятельно рекомендуется загрузить конфигурацию по-умолчанию. Вы сможете её изменить потом." default_role_manager: Менеджер default_role_developper: Разработчик default_role_reporter: Генератор отчетов -default_tracker_bug: Bug Ошибка +default_tracker_bug: Ошибка default_tracker_feature: Характеристика default_tracker_support: Поддержка default_issue_status_new: Новый @@ -537,30 +561,9 @@ default_priority_urgent: Срочный default_priority_immediate: Немедленный default_activity_design: Проектирование default_activity_development: Разработка + enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) -label_registration_activation_by_email: активация аккаунтов по email -mail_subject_account_activation_request: Запрос на активацию пользователя в системе Redmine -mail_body_account_activation_request: 'Новый пользователь (%s) зарегистирован. Аккаунт ожидает вашего утверждения:' -label_registration_automatic_activation: автоматическая активация аккаунтов -label_registration_manual_activation: активировать аккаунты вручную -notice_account_pending: "Ваш аккаунт уже создан и ожидает подтверждения администратора." -field_time_zone: Часовой пояс -text_caracters_minimum: Должно быть не менее %d знаков. -setting_bcc_recipients: Использовать скрытые списки (bcc) -button_annotate: Авторство -label_issues_by: Сортировать по %s -field_searchable: Доступно для поиска -label_display_per_page: 'На страницу: %s' -setting_per_page_options: Кол-во строк на страницу -label_age: Возраст -notice_default_data_loaded: Была загружена конфигурация по-умолчанию. -text_load_default_configuration: Загрузить конфигурацию по-умолчанию -text_no_configuration_data: "Роли, трекеры, статусы задач и оперативный план не были сконфигурированны.\nНастоятельно рекомендуется загрузить конфигурацию по-умолчанию. Вы сможете её изменить потом." + error_can_t_load_default_data: "Конфигурация по умолчанию не была загружена: %s" -button_update: Обновить -label_change_properties: Изменить свойства -label_general: Общее -label_repository_plural: Репозитории -label_associated_revisions: Associated revisions diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-de.js b/public/javascripts/jstoolbar/lang/jstoolbar-de.js index 72bab0be3..0c7a8799e 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-de.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-de.js @@ -1,15 +1,15 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = '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'] = 'Fett'; +jsToolBar.strings['Italic'] = 'Kursiv'; +jsToolBar.strings['Underline'] = 'Unterstrichen'; +jsToolBar.strings['Deleted'] = 'Durchgestrichen'; +jsToolBar.strings['Inline quote'] = 'Inline-Zitat'; +jsToolBar.strings['Code'] = 'Quelltext'; +jsToolBar.strings['Heading 1'] = 'Überschrift 1. Ordnung'; +jsToolBar.strings['Heading 2'] = 'Überschrift 2. Ordnung'; +jsToolBar.strings['Heading 3'] = 'Überschrift 3. Ordnung'; +jsToolBar.strings['Unordered list'] = 'Aufzählungsliste'; +jsToolBar.strings['Ordered list'] = 'Nummerierte Liste'; +jsToolBar.strings['Preformatted text'] = 'Präformatierter Text'; +jsToolBar.strings['Wiki link'] = 'Verweis (Link) zu einer Wiki-Seite'; +jsToolBar.strings['Image'] = 'Grafik'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js index 72bab0be3..7e795ec6d 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js @@ -1,15 +1,15 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = '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['Inline quote'] = '內嵌引文'; +jsToolBar.strings['Code'] = '程式碼'; +jsToolBar.strings['Heading 1'] = '標題 1'; +jsToolBar.strings['Heading 2'] = '標題 2'; +jsToolBar.strings['Heading 3'] = '標題 3'; +jsToolBar.strings['Unordered list'] = '項目清單'; +jsToolBar.strings['Ordered list'] = '編號清單'; +jsToolBar.strings['Preformatted text'] = '格式化文字'; +jsToolBar.strings['Wiki link'] = '連結至 Wiki 頁面'; +jsToolBar.strings['Image'] = '圖片'; From 79f92a675af528ecbe5abcc15b9c98805c9c7315 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 25 Jan 2008 10:31:06 +0000 Subject: [PATCH 164/710] User display format is now configurable in administration settings. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1103 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/settings_controller.rb | 3 +++ app/models/member.rb | 4 ++++ app/models/setting.rb | 3 ++- app/models/user.rb | 21 ++++++++++++++++++--- app/views/projects/settings/_members.rhtml | 4 ++-- app/views/settings/_general.rhtml | 3 +++ config/settings.yml | 3 +++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/unit/user_test.rb | 8 ++++++++ 30 files changed, 65 insertions(+), 6 deletions(-) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index c5fae2a16..c7c8751dd 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -35,7 +35,10 @@ class SettingsController < ApplicationController end flash[:notice] = l(:notice_successful_update) redirect_to :action => 'edit', :tab => params[:tab] + return end + @options = {} + @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } end def plugin diff --git a/app/models/member.rb b/app/models/member.rb index 0d0540690..b4617c229 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -31,6 +31,10 @@ class Member < ActiveRecord::Base self.user.name end + def <=>(member) + role == member.role ? (user <=> member.user) : (role <=> member.role) + end + def before_destroy # remove category based auto assignments for this member IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] diff --git a/app/models/setting.rb b/app/models/setting.rb index 46b6d76ce..185991d9b 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -53,12 +53,13 @@ class Setting < ActiveRecord::Base v = read_attribute(:value) # Unserialize serialized settings v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String) + v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank? v end def value=(v) v = v.to_yaml if v && @@available_settings[name]['serialized'] - write_attribute(:value, v) + write_attribute(:value, v.to_s) end # Returns the value of the setting named name diff --git a/app/models/user.rb b/app/models/user.rb index 906420ed4..22d66539d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,6 +23,14 @@ class User < ActiveRecord::Base STATUS_ACTIVE = 1 STATUS_REGISTERED = 2 STATUS_LOCKED = 3 + + USER_FORMATS = { + :firstname_lastname => '#{firstname} #{lastname}', + :firstname => '#{firstname}', + :lastname_firstname => '#{lastname} #{firstname}', + :lastname_coma_firstname => '#{lastname}, #{firstname}', + :username => '#{login}' + } has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all has_many :projects, :through => :memberships @@ -107,8 +115,9 @@ class User < ActiveRecord::Base end # Return user's full name for display - def name - "#{firstname} #{lastname}" + def name(formatter = nil) + f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] + eval '"' + f + '"' end def active? @@ -164,7 +173,13 @@ class User < ActiveRecord::Base end def <=>(user) - user.nil? ? -1 : (lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname) + if user.nil? + -1 + elsif lastname.to_s.downcase == user.lastname.to_s.downcase + firstname.to_s.downcase <=> user.firstname.to_s.downcase + else + lastname.to_s.downcase <=> user.lastname.to_s.downcase + end end def to_s diff --git a/app/views/projects/settings/_members.rhtml b/app/views/projects/settings/_members.rhtml index 55dfde1ac..ab22ac602 100644 --- a/app/views/projects/settings/_members.rhtml +++ b/app/views/projects/settings/_members.rhtml @@ -1,8 +1,8 @@ <%= error_messages_for 'member' %> <% roles = Role.find_all_givable %> -<% users = User.find_active(:all) - @project.users %> +<% users = User.find_active(:all).sort - @project.users %> <% # members sorted by role position - members = @project.members.find(:all, :include => [:role, :user]).sort{|x,y| x.role.position <=> y.role.position} %> + members = @project.members.find(:all, :include => [:role, :user]).sort %> <% if members.any? %> diff --git a/app/views/settings/_general.rhtml b/app/views/settings/_general.rhtml index 514e62b59..548ebbfac 100644 --- a/app/views/settings/_general.rhtml +++ b/app/views/settings/_general.rhtml @@ -20,6 +20,9 @@

    <%= select_tag 'settings[time_format]', options_for_select( [[l(:label_language_based), '']] + Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, Setting.time_format) %>

    +

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

    +

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

    diff --git a/config/settings.yml b/config/settings.yml index 1360b5501..a45ae17ce 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -79,6 +79,9 @@ date_format: default: '' time_format: default: '' +user_format: + default: :firstname_lastname + format: symbol cross_project_issue_relations: default: 0 notified_events: diff --git a/lang/bg.yml b/lang/bg.yml index 00b20308b..7aac092cb 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/cs.yml b/lang/cs.yml index ab159c2c4..d6307babe 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/de.yml b/lang/de.yml index 0c1ba3575..0cdf989bf 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/en.yml b/lang/en.yml index 32fd9926f..534a4834e 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -204,6 +204,7 @@ setting_repositories_encodings: Repositories encodings setting_emails_footer: Emails footer setting_protocol: Protocol setting_per_page_options: Objects per page options +setting_user_format: Users display format label_user: User label_user_plural: Users diff --git a/lang/es.yml b/lang/es.yml index ac3b34c19..9023f2b6a 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -568,3 +568,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/fi.yml b/lang/fi.yml index d70cf396a..5dbe5e8a5 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -569,3 +569,4 @@ enumeration_issue_priorities: Tapahtuman prioriteetit enumeration_doc_categories: Dokumentin luokat enumeration_activities: Aktiviteetit (ajan seuranta) label_associated_revisions: Liittyvät versiot +setting_user_format: Users display format diff --git a/lang/fr.yml b/lang/fr.yml index be93da9a1..1c1e7b5c2 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -205,6 +205,7 @@ setting_repositories_encodings: Encodages des dépôts setting_emails_footer: Pied-de-page des emails setting_protocol: Protocole setting_per_page_options: Options d'objets affichés par page +setting_user_format: Format d'affichage des utilisateurs label_user: Utilisateur label_user_plural: Utilisateurs diff --git a/lang/he.yml b/lang/he.yml index ad3331624..9b274c58d 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/it.yml b/lang/it.yml index 3b3131211..5f07a2f24 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/ja.yml b/lang/ja.yml index 08423c824..3266256c2 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -566,3 +566,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/ko.yml b/lang/ko.yml index 0b556840d..7f93fe94a 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -565,3 +565,4 @@ label_change_properties: 속성 변경 label_general: 일반 label_repository_plural: 저장소들 label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/lt.yml b/lang/lt.yml index 5e8273b91..51686f9ed 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -566,3 +566,4 @@ text_no_configuration_data: "Roles, trackers, issue statuses and workflow have n label_repository_plural: Repositories error_can_t_load_default_data: "Default configuration could not be loaded: %s" label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/nl.yml b/lang/nl.yml index f81247623..dc413b058 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -566,3 +566,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/pl.yml b/lang/pl.yml index ec7eec222..10b5cd029 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -565,3 +565,4 @@ label_change_properties: Zmień właściwości label_general: Ogólne label_repository_plural: Repozytoria label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 629ca2c38..b246cfd30 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/pt.yml b/lang/pt.yml index 8add9b6eb..aa98d15a8 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/ro.yml b/lang/ro.yml index b5413fdd4..d9011c21e 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -565,3 +565,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/ru.yml b/lang/ru.yml index 6636ed56f..61ce06759 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -567,3 +567,4 @@ enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) error_can_t_load_default_data: "Конфигурация по умолчанию не была загружена: %s" +setting_user_format: Users display format diff --git a/lang/sr.yml b/lang/sr.yml index 0c64ee128..71503be01 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -566,3 +566,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/sv.yml b/lang/sv.yml index ff199aba4..604590aff 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -566,3 +566,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 21d1e9ead..08b9057fa 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -566,3 +566,4 @@ enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/lang/zh.yml b/lang/zh.yml index de7ec093e..469bfb0c5 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -568,3 +568,4 @@ label_change_properties: Change properties label_general: General label_repository_plural: Repositories label_associated_revisions: Associated revisions +setting_user_format: Users display format diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 9f58d278f..845a34afd 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -76,6 +76,14 @@ class UserTest < Test::Unit::TestCase assert_equal User.hash_password("hello"), user.hashed_password end + def test_name_format + assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname) + Setting.user_format = :firstname_lastname + assert_equal 'John Smith', @jsmith.name + Setting.user_format = :username + assert_equal 'jsmith', @jsmith.name + end + def test_lock user = User.try_to_login("jsmith", "jsmith") assert_equal @jsmith, user From 4abb82fd7bcdd2cdffdd8778a5d9e2fc6a3857dd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 25 Jan 2008 10:55:16 +0000 Subject: [PATCH 165/710] Fixed RepositoriesController: undefined local variable or method `show_error' (broken by r1094). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1104 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 14 +++++++------- .../repositories_subversion_controller_test.rb | 6 ++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 542eb93ea..13d3eaa32 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -55,7 +55,7 @@ class RepositoriesController < ApplicationController @entries = @repository.entries('') # latest changesets @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") - show_error and return unless @entries || @changesets.any? + show_error_not_found unless @entries || @changesets.any? rescue Redmine::Scm::Adapters::CommandFailed => e show_error_command_failed(e.message) end @@ -65,7 +65,7 @@ class RepositoriesController < ApplicationController if request.xhr? @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) else - show_error unless @entries + show_error_not_found unless @entries end rescue Redmine::Scm::Adapters::CommandFailed => e show_error_command_failed(e.message) @@ -73,7 +73,7 @@ class RepositoriesController < ApplicationController def changes @entry = @repository.scm.entry(@path, @rev) - show_error and return unless @entry + 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) @@ -96,7 +96,7 @@ class RepositoriesController < ApplicationController def entry @content = @repository.scm.cat(@path, @rev) - show_error and return unless @content + show_error_not_found and return unless @content if 'raw' == params[:format] send_data @content, :filename => @path.split('/').last else @@ -109,7 +109,7 @@ class RepositoriesController < ApplicationController def annotate @annotate = @repository.scm.annotate(@path, @rev) - show_error and return if @annotate.nil? || @annotate.empty? + show_error_not_found and return if @annotate.nil? || @annotate.empty? rescue Redmine::Scm::Adapters::CommandFailed => e show_error_command_failed(e.message) end @@ -128,7 +128,7 @@ class RepositoriesController < ApplicationController format.js {render :layout => false} end rescue ChangesetNotFound - show_error + show_error_not_found rescue Redmine::Scm::Adapters::CommandFailed => e show_error_command_failed(e.message) end @@ -147,7 +147,7 @@ class RepositoriesController < ApplicationController @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 and return unless @diff + show_error_not_found unless @diff end rescue Redmine::Scm::Adapters::CommandFailed => e show_error_command_failed(e.message) diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index d7ce45640..ab5ec14ca 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -67,6 +67,12 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_response :success assert_template 'entry' end + + def test_entry_not_found + get :entry, :id => 1, :path => ['subversion_test', 'zzz.c'] + assert_tag :tag => 'div', :attributes => { :class => /error/ }, + :content => /Entry and\/or revision doesn't exist/ + end def test_entry_download get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'], :format => 'raw' From bea49ae24532284ca4ef9ce72733b78f77e480d5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 2 Feb 2008 10:50:31 +0000 Subject: [PATCH 166/710] Administrators can edit issue notes. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1105 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 1 + app/controllers/journals_controller.rb | 40 +++++++++++++++ app/helpers/journals_helper.rb | 37 ++++++++++++++ app/models/journal.rb | 4 ++ app/views/issues/_history.rhtml | 2 +- app/views/journals/_notes_form.rhtml | 7 +++ app/views/journals/edit.rjs | 3 ++ app/views/journals/update.rjs | 3 ++ public/images/delete.png | Bin 265 -> 1022 bytes public/images/edit.png | Bin 1340 -> 1343 bytes public/stylesheets/application.css | 3 +- test/fixtures/journals.yml | 8 +++ test/functional/journals_controller_test.rb | 51 ++++++++++++++++++++ 13 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 app/controllers/journals_controller.rb create mode 100644 app/helpers/journals_helper.rb create mode 100644 app/views/journals/_notes_form.rhtml create mode 100644 app/views/journals/edit.rjs create mode 100644 app/views/journals/update.rjs create mode 100644 test/functional/journals_controller_test.rb diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index fdece9e1a..dfee53841 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -27,6 +27,7 @@ class IssuesController < ApplicationController cache_sweeper :issue_sweeper, :only => [ :new, :edit, :update, :destroy ] + helper :journals helper :projects include ProjectsHelper helper :custom_fields diff --git a/app/controllers/journals_controller.rb b/app/controllers/journals_controller.rb new file mode 100644 index 000000000..b867b4ae6 --- /dev/null +++ b/app/controllers/journals_controller.rb @@ -0,0 +1,40 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class JournalsController < ApplicationController + layout 'base' + before_filter :find_journal + + def edit + if request.post? + @journal.update_attributes(:notes => params[:notes]) if params[:notes] + respond_to do |format| + format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id } + format.js { render :action => 'update' } + end + return + end + end + +private + def find_journal + @journal = Journal.find(params[:id]) + render_403 and return false unless @journal.editable_by?(User.current) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb new file mode 100644 index 000000000..c97446448 --- /dev/null +++ b/app/helpers/journals_helper.rb @@ -0,0 +1,37 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module JournalsHelper + def render_notes(journal, options={}) + content = '' + editable = journal.editable_by?(User.current) + if editable + links = [] + links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", + { :controller => 'journals', :action => 'edit', :id => journal }, + :title => l(:button_edit)) + content << content_tag('div', links.join(' '), :class => 'contextual') + end + content << textilizable(journal, :notes) + content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'editable' : nil)) + end + + def link_to_in_place_notes_editor(text, field_id, url, options={}) + onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;" + link_to text, '#', options.merge(:onclick => onclick) + end +end diff --git a/app/models/journal.rb b/app/models/journal.rb index d02067a9d..df7308435 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -49,4 +49,8 @@ class Journal < ActiveRecord::Base c = details.detect {|detail| detail.prop_key == prop} c ? c.value : nil end + + def editable_by?(usr) + usr && usr.admin? + end end diff --git a/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml index bab37b4fd..edfb9b94d 100644 --- a/app/views/issues/_history.rhtml +++ b/app/views/issues/_history.rhtml @@ -8,6 +8,6 @@
  • <%= show_detail(detail) %>
  • <% end %> - <%= textilizable(journal.notes) unless journal.notes.blank? %> + <%= render_notes(journal) unless journal.notes.blank? %> <% note_id += 1 %> <% end %> diff --git a/app/views/journals/_notes_form.rhtml b/app/views/journals/_notes_form.rhtml new file mode 100644 index 000000000..9baec03fa --- /dev/null +++ b/app/views/journals/_notes_form.rhtml @@ -0,0 +1,7 @@ +<% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %> + <%= text_area_tag :notes, @journal.notes, :class => 'wiki-edit', + :rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %> +

    <%= submit_tag l(:button_save) %> + <%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " + + "Element.show('journal-#{@journal.id}-notes'); return false;" %>

    +<% end %> diff --git a/app/views/journals/edit.rjs b/app/views/journals/edit.rjs new file mode 100644 index 000000000..798cb0f04 --- /dev/null +++ b/app/views/journals/edit.rjs @@ -0,0 +1,3 @@ +page.hide "journal-#{@journal.id}-notes" +page.insert_html :after, "journal-#{@journal.id}-notes", + :partial => 'notes_form' diff --git a/app/views/journals/update.rjs b/app/views/journals/update.rjs new file mode 100644 index 000000000..9da0ebeae --- /dev/null +++ b/app/views/journals/update.rjs @@ -0,0 +1,3 @@ +page.replace "journal-#{@journal.id}-notes", render_notes(@journal) +page.show "journal-#{@journal.id}-notes" +page.remove "journal-#{@journal.id}-form" diff --git a/public/images/delete.png b/public/images/delete.png index 137baa68eac092124487651cd173a02c67f5a940..a1af31d8334a2d5ee776711876b58dd6c9f27581 100644 GIT binary patch literal 1022 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!PKj$oiE~kEVo7FxoQosX}gIrh<`yfw_XAft8V=m5Hf>wt=C6L7#I^08k%$iKnkC`wb>W5f!x){P`er zISV`@iy0WWK7cTz(;AZ*KtYytM_)$E)e-c?47?>FXd_r9RGcbJr{=KJX&hzKb z|NjT_M!{$ZjGz!;Z+)@@=v~H=AirP+hi5m^fE*i77srr_TfKdrd<+I0Opd?*n_pj& zG~M}eK~htvgZS>Q0Qp1n8r7~Gi7=O}VO4HlP`RWfeOttoggVD9GbXHO6g+ox>0MA5 Ndb;|#taD0e0suNuR676w delta 210 zcmeyz-pQoc8Q|y6%O%Cdz`(%k>ERLtr1^oEhl3eNcFhsLHc`>B{yHlwhnN9Jv+g#a zAZLL`WHAE+w-5+3Ub#+poSnjXK8<8-9v0*k^1GPO9WIV*mnAS3j3^P69AcK75{ wEGmq;li#!GDKF*e@v-63@C!fiPCSN*fp_gzXWmeWJO&`}boFyt=akR{0Oai+&;S4c diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index ec3ae159d..eb4e855b4 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -122,7 +122,7 @@ div.square { width: .6em; height: .6em; } -.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;} +.contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} .contextual input {font-size:0.9em;} .splitcontentleft{float:left; width:49%;} @@ -227,7 +227,6 @@ text-align:center; padding:0.6em; z-index:100; filter:alpha(opacity=50); --moz-opacity:0.5; opacity: 0.5; -khtml-opacity: 0.5; } diff --git a/test/fixtures/journals.yml b/test/fixtures/journals.yml index 0de938168..169713fc5 100644 --- a/test/fixtures/journals.yml +++ b/test/fixtures/journals.yml @@ -6,3 +6,11 @@ journals_001: journalized_type: Issue user_id: 1 journalized_id: 1 +journals_002: + created_on: <%= 1.days.ago.to_date.to_s(:db) %> + notes: "Some notes" + id: 2 + journalized_type: Issue + user_id: 2 + journalized_id: 1 + \ No newline at end of file diff --git a/test/functional/journals_controller_test.rb b/test/functional/journals_controller_test.rb new file mode 100644 index 000000000..f231d10ee --- /dev/null +++ b/test/functional/journals_controller_test.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 File.dirname(__FILE__) + '/../test_helper' +require 'journals_controller' + +# Re-raise errors caught by the controller. +class JournalsController; def rescue_action(e) raise e end; end + +class JournalsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :roles, :issues, :journals, :journal_details, :enabled_modules + + def setup + @controller = JournalsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_get_edit + @request.session[:user_id] = 1 + xhr :get, :edit, :id => 2 + assert_response :success + assert_select_rjs :insert, :after, 'journal-2-notes' do + assert_select 'form[id=journal-2-form]' + assert_select 'textarea' + end + end + + def test_post_edit + @request.session[:user_id] = 1 + xhr :post, :edit, :id => 2, :notes => 'Updated notes' + assert_response :success + assert_select_rjs :replace, 'journal-2-notes' + assert_equal 'Updated notes', Journal.find(2).notes + end +end From 45b69c4a656491221eb12df85c282949955a50b8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 2 Feb 2008 10:54:31 +0000 Subject: [PATCH 167/710] Fixed an error on issue feeds. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1106 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index dfee53841..b722d9340 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -271,7 +271,7 @@ private # Retrieve query from session or build a new query def retrieve_query - if params[:query_id] + if !params[:query_id].blank? @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)}) session[:query] = {:id => @query.id, :project_id => @query.project_id} else From 12c0f5f66e21bbfdeed82f00eabe7b47ed99d85f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 2 Feb 2008 11:08:04 +0000 Subject: [PATCH 168/710] Do not show Roadmap menu item if the project doesn't define any versions. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1107 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/redmine.rb b/lib/redmine.rb index 1aee1767c..92a1cef14 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -93,7 +93,8 @@ end Redmine::MenuManager.map :project_menu do |menu| menu.push :overview, { :controller => 'projects', :action => 'show' }, :caption => :label_overview menu.push :activity, { :controller => 'projects', :action => 'activity' }, :caption => :label_activity - menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, :caption => :label_roadmap + menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, + :if => Proc.new { |p| p.versions.any? }, :caption => :label_roadmap menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new, :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) } From 0123dc36515d103d29fd9225bad22aae664a2c12 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 2 Feb 2008 15:51:48 +0000 Subject: [PATCH 169/710] Do not authorize project identifier with numbers only (would be interpreted as the project id in urls). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1108 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/project.rb b/app/models/project.rb index 42f2ddfd9..73a8d6404 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -217,6 +217,7 @@ protected def validate errors.add(parent_id, " must be a root project") if parent and parent.parent errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 + errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) end private From 225637c5dc23465c49b9f4733a08ba37e6a40b40 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 13:54:59 +0000 Subject: [PATCH 170/710] =?UTF-8?q?Translations=20updates=20(#254,=20#255,?= =?UTF-8?q?=20#260):=20*=20Traditional=20Chinese=20(Shortie=20Lo)=20*=20Ge?= =?UTF-8?q?rman=20(Thomas=20L=C3=B6ber)=20*=20Russian=20(Michael=20Pirogov?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1109 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/de.yml | 138 ++++++++++++++++++++++++------------------------- lang/ru.yml | 9 ++-- lang/zh-tw.yml | 85 +++++++++++++++--------------- 3 files changed, 115 insertions(+), 117 deletions(-) diff --git a/lang/de.yml b/lang/de.yml index 0cdf989bf..f0f054b1b 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -72,14 +72,23 @@ notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen. notice_email_sent: Eine E-Mail wurde an %s gesendet. notice_email_error: Beim Senden einer E-Mail ist ein Fehler aufgetreten (%s). notice_feeds_access_key_reseted: Ihr RSS-Zugriffsschlüssel wurde zurückgesetzt. +notice_failed_to_save_issues: "%d von %d ausgewählten Tickets konnte(n) nicht gespeichert werden: %s." +notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten." +notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators." +notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen. -error_scm_not_found: "Eintrag und/oder Revision besteht nicht im Projektarchiv." -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %s" +error_scm_not_found: Eintrag und/oder Revision besteht nicht im Projektarchiv. +error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %s" mail_subject_lost_password: Ihr Redmine-Kennwort mail_body_lost_password: 'Benutzen Sie den folgenden Link, um Ihr Kennwort zu ändern:' mail_subject_register: Redmine Kontoaktivierung mail_body_register: 'Um Ihr Konto zu aktivieren, benutzen Sie folgenden Link:' +mail_body_account_information_external: Sie können sich mit Ihrem Konto "%s" an Redmine anmelden. +mail_body_account_information: Ihre Redmine Konto-Informationen +mail_subject_account_activation_request: Antrag auf Redmine Kontoaktivierung +mail_body_account_activation_request: 'Ein neuer Benutzer (%s) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:' gui_validation_error: 1 Fehler gui_validation_error_plural: %d Fehler @@ -120,7 +129,7 @@ field_priority: Priorität field_fixed_version: Erledigt in Version field_user: Benutzer field_role: Rolle -field_homepage: Startseite +field_homepage: Projekt-Homepage field_is_public: Öffentlich field_parent: Unterprojekt von field_is_in_chlog: Ansicht im Change-Log @@ -157,13 +166,16 @@ field_hours: Stunden field_activity: Aktivität field_spent_on: Datum field_identifier: Kennung -field_is_filter: Als Fiter benutzen +field_is_filter: Als Filter benutzen field_issue_to_id: Zugehöriges Ticket field_delay: Pufferzeit field_assignable: Tickets können dieser Rolle zugewiesen werden field_redirect_existing_links: Existierende Links umleiten field_estimated_hours: Geschätzter Aufwand -field_default_value: Default +field_column_names: Spalten +field_time_zone: Zeitzone +field_searchable: Durchsuchbar +field_default_value: Standardwert setting_app_title: Applikations-Titel setting_app_subtitle: Applikations-Untertitel @@ -174,17 +186,25 @@ setting_self_registration: Anmeldung ermöglicht setting_attachment_max_size: Max. Dateigröße setting_issues_export_limit: Max. Anzahl Tickets bei CSV/PDF-Export setting_mail_from: E-Mail-Absender +setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden setting_host_name: Hostname setting_text_formatting: Textformatierung setting_wiki_compression: Wiki-Historie komprimieren setting_feeds_limit: Feed-Inhalt begrenzen -setting_autofetch_changesets: Commits automatisch abrufen -setting_sys_api_enabled: Webservice für Repository-Verwaltung benutzen +setting_autofetch_changesets: Changesets automatisch abrufen +setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen setting_commit_ref_keywords: Schlüsselwörter (Beziehungen) setting_commit_fix_keywords: Schlüsselwörter (Status) setting_autologin: Automatische Anmeldung setting_date_format: Datumsformat +setting_time_format: Zeitformat setting_cross_project_issue_relations: Ticket-Beziehungen zwischen Projekten erlauben +setting_issue_list_default_columns: Default-Spalten in der Ticket-Auflistung +setting_repositories_encodings: Kodierungen der Projektarchive +setting_emails_footer: E-Mail-Fußzeile +setting_protocol: Protokoll +setting_per_page_options: Objekte pro Seite +setting_user_format: Benutzer-Anzeigeformat label_user: Benutzer label_user_plural: Benutzer @@ -198,6 +218,7 @@ label_issue: Ticket label_issue_new: Neues Ticket label_issue_plural: Tickets label_issue_view_all: Alle Tickets ansehen +label_issues_by: Tickets von %s label_document: Dokument label_document_new: Neues Dokument label_document_plural: Dokumente @@ -255,6 +276,7 @@ label_min_max_length: Länge (Min. - Max.) label_list: Liste label_date: Datum label_integer: Zahl +label_float: Fließkommazahl label_boolean: Boolean label_string: Text label_text: Langer Text @@ -296,6 +318,7 @@ label_current_status: Gegenwärtiger Status label_new_statuses_allowed: Neue Berechtigungen label_all: alle label_none: kein +label_nobody: Niemand label_next: Weiter label_previous: Zurück label_used_by: Benutzt von @@ -333,11 +356,13 @@ label_contains: enthält label_not_contains: enthält nicht label_day_plural: Tage label_repository: Projektarchiv +label_repository_plural: Projektarchive label_browse: Codebrowser label_modification: %d Änderung label_modification_plural: %d Änderungen label_revision: Revision label_revision_plural: Revisionen +label_associated_revisions: Zugehörige Revisionen label_added: hinzugefügt label_modified: geändert label_deleted: gelöscht @@ -346,10 +371,10 @@ label_latest_revision_plural: Aktuellste Revisionen label_view_revisions: Revisionen anzeigen label_max_size: Maximale Größe label_on: von -label_sort_highest: Anfang -label_sort_higher: eins höher -label_sort_lower: eins tiefer -label_sort_lowest: Ende +label_sort_highest: An den Anfang +label_sort_higher: Eins höher +label_sort_lower: Eins tiefer +label_sort_lowest: Ans Ende label_roadmap: Roadmap label_roadmap_due_in: Fällig in label_roadmap_overdue: %s verspätet @@ -362,8 +387,8 @@ label_wiki_edit: Wiki-Bearbeitung label_wiki_edit_plural: Wiki-Bearbeitungen label_wiki_page: Wiki-Seite label_wiki_page_plural: Wiki-Seiten -label_index_by_title: Index by title -label_index_by_date: Index by date +label_index_by_title: Seiten nach Titel sortiert +label_index_by_date: Seiten nach Datum sortiert label_current_version: Gegenwärtige Version label_preview: Vorschau label_feed_plural: Feeds @@ -425,6 +450,25 @@ label_module_plural: Module label_added_time_by: Von %s vor %s hinzugefügt label_updated_time: Vor %s aktualisiert label_jump_to_a_project: Zu einem Projekt springen... +label_file_plural: Dateien +label_changeset_plural: Changesets +label_default_columns: Default-Spalten +label_no_change_option: (Keine Änderung) +label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten +label_theme: Stil +label_default: Default +label_search_titles_only: Nur Titel durchsuchen +label_user_mail_option_all: "Für alle Ereignisse in all meinen Projekten" +label_user_mail_option_selected: "Für alle Ereignisse in den ausgewählten Projekten..." +label_user_mail_option_none: "Nur für Dinge, die ich beobachte oder an denen ich beteiligt bin" +label_user_mail_no_self_notified: "Ich möchte nicht über Änderungen benachrichtigt werden, die ich selbst durchführe." +label_registration_activation_by_email: Kontoaktivierung durch E-Mail +label_registration_manual_activation: Manuelle Kontoaktivierung +label_registration_automatic_activation: Automatische Kontoaktivierung +label_display_per_page: 'Pro Seite: %s' +label_age: Alter +label_change_properties: Eigenschaften ändern +label_general: Allgemein button_login: Anmelden button_submit: OK @@ -443,7 +487,7 @@ button_lock: Sperren button_unlock: Entsperren button_download: Download button_list: Liste -button_view: Siehe +button_view: Ansehen button_move: Verschieben button_back: Zurück button_cancel: Abbrechen @@ -458,6 +502,10 @@ button_archive: Archivieren button_unarchive: Entarchivieren button_reset: Zurücksetzen button_rename: Umbenennen +button_change_password: Kennwort ändern +button_copy: Kopieren +button_annotate: Mit Anmerkungen versehen +button_update: Aktualisieren status_active: aktiv status_registered: angemeldet @@ -477,6 +525,7 @@ text_tip_task_end_day: Aufgabe, die an diesem Tag endet text_tip_task_begin_end_day: Aufgabe, die an diesem Tag beginnt und endet text_project_identifier_info: 'Kleinbuchstaben (a-z), Ziffern und Bindestriche erlaubt.
    Einmal gespeichert, kann die Kennung nicht mehr geändert werden.' text_caracters_maximum: Max. %d Zeichen. +text_caracters_minimum: Muss mindestens %d Zeichen lang sein. text_length_between: Länge zwischen %d und %d Zeichen. text_tracker_no_workflow: Kein Workflow für diesen Tracker definiert. text_unallowed_characters: Nicht erlaubte Zeichen @@ -488,13 +537,16 @@ text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sämtl text_issue_category_destroy_question: Einige Tickets (%d) sind dieser Kategorie zugeodnet. Was möchten Sie tun? text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen text_issue_category_reassign_to: Tickets dieser Kategorie zuordnen +text_user_mail_option: "Für nicht ausgewählte Projekte werden Sie nur Benachrichtigungen für Dinge erhalten, die Sie beobachten oder an denen Sie beteiligt sind (z. B. Tickets, deren Autor Sie sind oder die Ihnen zugewiesen sind)." +text_no_configuration_data: "Rollen, Tracker, Ticket-Status und Workflows wurden noch nicht konfiguriert.\nEs ist sehr zu empfehlen, die Standard-Konfiguration zu laden. Sobald sie geladen ist, können Sie sie abändern." +text_load_default_configuration: Standard-Konfiguration laden default_role_manager: Manager -default_role_developper: Developer +default_role_developper: Entwickler default_role_reporter: Reporter default_tracker_bug: Fehler default_tracker_feature: Feature -default_tracker_support: Support +default_tracker_support: Unterstützung default_issue_status_new: Neu default_issue_status_assigned: Zugewiesen default_issue_status_resolved: Gelöst @@ -509,60 +561,8 @@ default_priority_high: Hoch default_priority_urgent: Dringend default_priority_immediate: Sofort default_activity_design: Design -default_activity_development: Development +default_activity_development: Entwicklung enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) -label_file_plural: Dateien -label_changeset_plural: Changesets -field_column_names: Spalten -label_default_columns: Default-Spalten -setting_issue_list_default_columns: Default-Spalten in der Ticket-Auflistung -setting_repositories_encodings: Repository-Kodierung -notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten." -label_bulk_edit_selected_issues: Alle ausgewählten Tickets bearbeiten -label_no_change_option: (Keine Änderung) -notice_failed_to_save_issues: "%d von %d ausgewählten Tickets konnte(n) nicht gespeichert werden: %s." -label_theme: Stil -label_default: Default -label_search_titles_only: Nur Titel durchsuchen -label_nobody: Niemand -button_change_password: Kennwort ändern -text_user_mail_option: "Für nicht ausgewählte Projekte werden Sie nur Benachrichtigungen für Dinge erhalten, die Sie beobachten oder an denen Sie beteiligt sind (z.B. Tickets, deren Autor Sie sind oder die Ihnen zugewiesen sind)." -label_user_mail_option_selected: "Für alle Ereignisse in den ausgewählten Projekten..." -label_user_mail_option_all: "Für alle Ereignisse in all meinen Projekten" -label_user_mail_option_none: "Nur für Dinge, die ich beobachte oder an denen ich beteiligt bin" -setting_emails_footer: E-Mail-Fußzeile -label_float: Fließkommazahl -button_copy: Kopieren -mail_body_account_information_external: Sie können sich mit Ihrem Konto "%s" an Redmine anmelden. -mail_body_account_information: Ihre Redmine Konto-Informationen -setting_protocol: Protokoll -label_user_mail_no_self_notified: "Ich möchte nicht über Änderungen benachrichtigt werden, die ich selbst durchführe." -setting_time_format: Zeitformat -label_registration_activation_by_email: Kontoaktivierung durch E-Mail -mail_subject_account_activation_request: Antrag auf Redmine Kontoaktivierung -mail_body_account_activation_request: 'Ein neuer Benutzer (%s) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:' -label_registration_automatic_activation: Automatische Kontoaktivierung -label_registration_manual_activation: Manuelle Kontoaktivierung -notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators." -field_time_zone: Zeitzone -text_caracters_minimum: Muss mindestens %d Zeichen lang sein. -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 diff --git a/lang/ru.yml b/lang/ru.yml index 61ce06759..04b17b806 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -78,7 +78,8 @@ notice_account_pending: "Ваша учетная запись уже созда notice_default_data_loaded: Была загружена конфигурация по-умолчанию. error_scm_not_found: Хранилилище не содержит записи и/или исправления. -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_scm_command_failed: "Ошибка доступа к хранилищу: %s" +error_can_t_load_default_data: "Конфигурация по умолчанию не была загружена: %s" mail_subject_lost_password: Ваш Redmine пароль mail_body_lost_password: 'Для изменения Redmine пароля, зайдите по следующей ссылке:' @@ -204,6 +205,7 @@ setting_emails_footer: Подстрочные примечания Emailов setting_protocol: Протокол setting_bcc_recipients: Использовать скрытые списки (bcc) setting_per_page_options: Кол-во строк на страницу +setting_user_format: Формат отображения имени label_user: Пользователь label_user_plural: Пользователи @@ -354,7 +356,7 @@ label_contains: содержит label_not_contains: не содержит label_day_plural: дней(я) label_repository: Хранилище -label_browse: Искать +label_browse: Обзор label_modification: %d изменение label_modification_plural: %d изменений label_revision: Редакция @@ -565,6 +567,3 @@ default_activity_development: Разработка enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) - -error_can_t_load_default_data: "Конфигурация по умолчанию не была загружена: %s" -setting_user_format: Users display format diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 08b9057fa..d468b162b 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -30,22 +30,22 @@ activerecord_error_too_long: 長度過長 activerecord_error_too_short: 長度太短 activerecord_error_wrong_length: 長度不正確 activerecord_error_taken: 已經被使用 -activerecord_error_not_a_number: is not a number +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 yrs +general_fmt_age_plural: %d 年 general_fmt_date: %%m/%%d/%%Y general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p general_fmt_datetime_short: %%b %%d, %%I:%%M %%p general_fmt_time: %%I:%%M %%p -general_text_No: 'No' -general_text_Yes: 'Yes' -general_text_no: 'no' -general_text_yes: 'yes' +general_text_No: '否' +general_text_Yes: '是' +general_text_no: '否' +general_text_yes: '是' general_lang_name: 'Traditional Chinese (繁體中文)' general_csv_separator: ',' general_csv_encoding: Big5 @@ -57,30 +57,29 @@ notice_account_updated: 帳戶更新資訊已儲存 notice_account_invalid_creditentials: 帳戶或密碼不正確 notice_account_password_updated: 帳戶新密碼已儲存 notice_account_wrong_password: 密碼不正確 -notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you. -notice_account_unknown_email: Unknown user. -notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. -notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you. -notice_account_activated: Your account has been activated. You can now log in. +notice_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: Successful connection. -notice_file_not_found: The page you were trying to access doesn't exist or has been removed. -notice_locking_conflict: Data have been updated by another user. +notice_successful_connection: 連線成功 +notice_file_not_found: 您想要存取的頁面已經不存在或被搬移至其他位置。 +notice_locking_conflict: 資料已被其他使用者更新。 notice_not_authorized: 你未被授權存取此頁面。 notice_email_sent: 郵件已經成功寄送至以下收件者: %s notice_email_error: 寄送郵件的過程中發生錯誤 (%s) notice_feeds_access_key_reseted: 您的 RSS 存取鍵已被重新設定。 -notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s." -notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." +notice_failed_to_save_issues: " %d 個項目儲存失敗 (總共選取 %d 個項目): %s." +notice_no_issue_selected: "未選擇任何項目!請勾選您想要編輯的項目。" notice_account_pending: "您的帳號已經建立,正在等待管理員的審核。" -notice_default_data_loaded: Default configuration successfully loaded. +notice_default_data_loaded: 預設組態已載入成功。 -error_scm_not_found: SCM 儲存庫中找不到這個專案或版本。 -error_scm_command_failed: "An error occurred when trying to access the repository: %s" - -error_can_t_load_default_data: "Default configuration could not be loaded: %s" +error_can_t_load_default_data: "無法載入預設組態: %s" +error_scm_not_found: SCM 儲存庫中找不到這個專案與(或)版本。 +error_scm_command_failed: "嘗試存取儲存庫時發生錯誤:: %s" mail_subject_lost_password: 您的 Redmine 網站密碼 mail_body_lost_password: '欲變更您的 Redmine 網站密碼, 請點選以下鏈結:' @@ -133,8 +132,8 @@ field_role: 角色 field_homepage: 網站首頁 field_is_public: 公開 field_parent: 父專案 -field_is_in_chlog: Issues displayed in changelog -field_is_in_roadmap: Issues displayed in roadmap +field_is_in_chlog: 項目顯示於變更記錄中 +field_is_in_roadmap: 項目顯示於版本藍圖中 field_login: 帳戶名稱 field_mail_notification: 電子郵件提醒選項 field_admin: 管理者 @@ -176,7 +175,7 @@ field_estimated_hours: 預估工時 field_column_names: Columns field_time_zone: 時區 field_searchable: 可用做搜尋條件 -field_default_value: Default value +field_default_value: 預設值 setting_app_title: 標題 setting_app_subtitle: 副標題 @@ -205,6 +204,7 @@ setting_repositories_encodings: Repositories encodings setting_emails_footer: 電子郵件附帶說明 setting_protocol: 協定 setting_per_page_options: 每頁顯示個數選項 +setting_user_format: 使用者顯示格式 label_user: 用戶 label_user_plural: 用戶清單 @@ -315,7 +315,7 @@ label_closed_issues_plural: 已結束 label_total: 總計 label_permissions: 權限 label_current_status: 目前狀態 -label_new_statuses_allowed: New statuses allowed +label_new_statuses_allowed: 可變更至以下狀態 label_all: 全部 label_none: 空值 label_nobody: nobody @@ -362,6 +362,7 @@ label_modification: %d 變更 label_modification_plural: %d 變更 label_revision: 版次 label_revision_plural: 版次清單 +label_associated_revisions: Associated revisions label_added: 已新增 label_modified: 已修改 label_deleted: 已刪除 @@ -405,7 +406,7 @@ label_view_diff: View differences label_diff_inline: inline label_diff_side_by_side: side by side label_options: 選項清單 -label_copy_workflow_from: Copy workflow from +label_copy_workflow_from: 從以下追蹤標籤複製工作流程 label_permissions_report: 權限報表 label_watched_issues: 觀察中的項目清單 label_related_issues: 相關的項目清單 @@ -466,7 +467,7 @@ label_registration_manual_activation: 手動啟用帳戶 label_registration_automatic_activation: 自動啟用帳戶 label_display_per_page: '每頁顯示: %s 個' label_age: Age -label_change_properties: Change properties +label_change_properties: 變更屬性 label_general: 一般 button_login: 登入 @@ -512,33 +513,33 @@ status_locked: 鎖定中 text_select_mail_notifications: 選擇欲寄送提醒通知郵件之動作 text_regexp_info: eg. ^[A-Z0-9]+$ -text_min_max_length_info: 0 means no restriction -text_project_destroy_confirmation: Are you sure you want to delete this project and all related data ? -text_workflow_edit: Select a role and a tracker to edit the workflow +text_min_max_length_info: 0 代表「不限制」 +text_project_destroy_confirmation: 您確定要刪除這個專案和其他相關資料? +text_workflow_edit: 選擇角色與追蹤標籤以設定其工作流程 text_are_you_sure: 確定執行? text_journal_changed: 從 %s 變更為 %s text_journal_set_to: 設定為 %s -text_journal_deleted: 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_journal_deleted: 已刪除 +text_tip_task_begin_day: 今天起始的工作 +text_tip_task_end_day: 今天截止的的工作 +text_tip_task_begin_end_day: 今天起始與截止的工作 text_project_identifier_info: '只允許小寫英文字母(a-z)、阿拉伯數字與連字符號(-)。
    儲存後,代碼不可再被更改。' text_caracters_maximum: 最多 %d 個字元. text_caracters_minimum: 長度必須大於 %d 個字元. text_length_between: 長度必須介於 %d 至 %d 個字元之間. -text_tracker_no_workflow: No workflow defined for this tracker -text_unallowed_characters: Unallowed characters +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 個項目 text_issue_updated: 已更新 %s 個項目 -text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? -text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? -text_issue_category_destroy_assignments: Remove category assignments -text_issue_category_reassign_to: Reassign issues to this category +text_wiki_destroy_confirmation: 您確定要刪除這個 wiki 和其中的所有內容? +text_issue_category_destroy_question: 有 (%d) 個項目被指派到此分類. 請選擇您想要的動作? +text_issue_category_destroy_assignments: 移除這些項目的分類 +text_issue_category_reassign_to: 重新指派這些項目至其它分類 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." -text_load_default_configuration: Load the default configuration +text_load_default_configuration: 載入預設組態 default_role_manager: 管理人員 default_role_developper: 開發人員 @@ -565,5 +566,3 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) -label_associated_revisions: Associated revisions -setting_user_format: Users display format From b124501a4e825801024c873304757d3094753f02 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 14:02:23 +0000 Subject: [PATCH 171/710] Add 'Author' to the available columns for the issue list. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1110 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/query.rb b/app/models/query.rb index adfd5f42a..7127a464f 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -96,6 +96,7 @@ class Query < ActiveRecord::Base QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position"), QueryColumn.new(:subject), + QueryColumn.new(:author), QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), From 1ecef3a95ae0471707ce6490e9c725daabce9d67 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 14:38:04 +0000 Subject: [PATCH 172/710] ProjectsController#add_news moved to NewsController#new. Preview added when adding/editing a news (#590). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1111 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/news_controller.rb | 29 +++++++++++++++++++++++-- app/controllers/projects_controller.rb | 13 ----------- app/views/news/edit.rhtml | 12 ++++++++-- app/views/news/index.rhtml | 12 ++++++++-- app/views/news/new.rhtml | 14 ++++++++++++ app/views/news/show.rhtml | 10 ++++++++- app/views/projects/add_news.rhtml | 6 ----- lib/redmine.rb | 2 +- test/functional/news_controller_test.rb | 11 ++++++++++ 9 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 app/views/news/new.rhtml delete mode 100644 app/views/projects/add_news.rhtml diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 109afe454..66ed61cf4 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -17,7 +17,9 @@ class NewsController < ApplicationController layout 'base' - before_filter :find_project, :authorize, :except => :index + before_filter :find_news, :except => [:new, :index, :preview] + before_filter :find_project, :only => :new + before_filter :authorize, :except => [:index, :preview] before_filter :find_optional_project, :only => :index accept_key_auth :index @@ -36,6 +38,18 @@ class NewsController < ApplicationController def show end + def new + @news = News.new(:project => @project, :author => User.current) + if request.post? + @news.attributes = params[:news] + if @news.save + flash[:notice] = l(:notice_successful_create) + Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added') + redirect_to :controller => 'news', :action => 'index', :project_id => @project + end + end + end + def edit if request.post? and @news.update_attributes(params[:news]) flash[:notice] = l(:notice_successful_update) @@ -64,14 +78,25 @@ class NewsController < ApplicationController redirect_to :action => 'index', :project_id => @project end + def preview + @text = (params[:news] ? params[:news][:description] : nil) + render :partial => 'common/preview' + end + private - def find_project + def find_news @news = News.find(params[:id]) @project = @news.project rescue ActiveRecord::RecordNotFound render_404 end + def find_project + @project = Project.find(params[:project_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + def find_optional_project return true unless params[:project_id] @project = Project.find(params[:project_id]) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 30e7ef85f..9560a451f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -259,19 +259,6 @@ class ProjectsController < ApplicationController render :layout => false if request.xhr? end - # Add a news to @project - def add_news - @news = News.new(:project => @project, :author => User.current) - if request.post? - @news.attributes = params[:news] - if @news.save - flash[:notice] = l(:notice_successful_create) - Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added') - redirect_to :controller => 'news', :action => 'index', :project_id => @project - end - end - end - def add_file if request.post? @version = @project.versions.find_by_id(params[:version_id]) diff --git a/app/views/news/edit.rhtml b/app/views/news/edit.rhtml index 5e015c4c7..a7e5e6e36 100644 --- a/app/views/news/edit.rhtml +++ b/app/views/news/edit.rhtml @@ -1,6 +1,14 @@

    <%=l(:label_news)%>

    -<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" } do |f| %> +<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" }, + :html => { :id => 'news-form' } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> -<% end %> \ No newline at end of file +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview' }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
    diff --git a/app/views/news/index.rhtml b/app/views/news/index.rhtml index c6bb9ad5a..369eaf1b1 100644 --- a/app/views/news/index.rhtml +++ b/app/views/news/index.rhtml @@ -1,17 +1,25 @@
    <%= link_to_if_authorized(l(:label_news_new), - {:controller => 'projects', :action => 'add_news', :id => @project}, + {:controller => 'news', :action => 'new', :project_id => @project}, :class => 'icon icon-add', :onclick => 'Element.show("add-news"); return false;') if @project %>

    <%=l(:label_news_plural)%>

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

    <%=l(:label_news_new)%>

    + +<% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project }, + :html => { :id => 'news-form' } do |f| %> +<%= render :partial => 'news/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview' }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
    diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index bff737f40..cc9eed043 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -10,11 +10,19 @@

    <%=h @news.title %>

    <% unless @news.summary.empty? %><%=h @news.summary %>
    <% end %> diff --git a/app/views/projects/add_news.rhtml b/app/views/projects/add_news.rhtml deleted file mode 100644 index a6ecd3da7..000000000 --- a/app/views/projects/add_news.rhtml +++ /dev/null @@ -1,6 +0,0 @@ -

    <%=l(:label_news_new)%>

    - -<% labelled_tabular_form_for :news, @news, :url => { :action => "add_news" } do |f| %> -<%= render :partial => 'news/form', :locals => { :f => f } %> -<%= submit_tag l(:button_create) %> -<% end %> \ No newline at end of file diff --git a/lib/redmine.rb b/lib/redmine.rb index 92a1cef14..c4c55a932 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -51,7 +51,7 @@ Redmine::AccessControl.map do |map| end map.project_module :news do |map| - map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member + map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member map.permission :view_news, {:news => [:index, :show]}, :public => true map.permission :comment_news, {:news => :add_comment} end diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb index 8a02345fd..397e928f1 100644 --- a/test/functional/news_controller_test.rb +++ b/test/functional/news_controller_test.rb @@ -45,4 +45,15 @@ class NewsControllerTest < Test::Unit::TestCase assert_template 'index' assert_not_nil assigns(:newss) end + + def test_preview + get :preview, :project_id => 1, + :news => {:title => '', + :description => 'News description', + :summary => ''} + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' }, + :content => /News description/ + end end From c3d909e0cc11831cbe9cff385111d4960489034a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 15:15:52 +0000 Subject: [PATCH 173/710] Slight style changes on issue associated changesets list. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1112 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_changesets.rhtml | 12 ++++++------ public/stylesheets/application.css | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/views/issues/_changesets.rhtml b/app/views/issues/_changesets.rhtml index 1a4c1a5bd..caa983cbf 100644 --- a/app/views/issues/_changesets.rhtml +++ b/app/views/issues/_changesets.rhtml @@ -1,8 +1,8 @@ -
      <% changesets.each do |changeset| %> -
    • <%= link_to("#{l(:label_revision)} #{changeset.revision}", - :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %>
      - <%= changeset.committer %>, <%= format_time(changeset.committed_on) %> - <%= textilizable(changeset, :comments) %>
    • +
      +

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

      + <%= textilizable(changeset, :comments) %> +
      <% end %> -
    diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index eb4e855b4..1090d8c87 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -136,9 +136,10 @@ textarea.wiki-edit { width: 99%; } li p {margin-top: 0;} div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} -div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em;} -div#issue-changesets ul {list-style-position: outside; list-style-type:none; margin: 0; padding: 0;} -div#issue-changesets li { padding: 4px; } +div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} +div#issue-changesets .changeset { padding: 4px;} +div#issue-changesets .changeset { border-bottom: 1px solid #ddd; } +div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;} #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; } From 2920daf2c81a667ea6495cc5426de9a05e0e55e2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 15:34:58 +0000 Subject: [PATCH 174/710] Do not use RedCloth's glyphs (#210). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1113 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 904701c9e..b614614c4 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -272,7 +272,7 @@ class RedCloth < String @shelf = [] textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists, :block_textile_prefix, :inline_textile_image, :inline_textile_link, - :inline_textile_code, :inline_textile_span, :glyphs_textile] + :inline_textile_code, :inline_textile_span] markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule, :block_markdown_bq, :block_markdown_lists, :inline_markdown_reflink, :inline_markdown_link] From 0beafecd3bd490be646263bbbe9bce2c35bbdcb5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 16:21:51 +0000 Subject: [PATCH 175/710] Fixed #208: Issue list does not scroll up when you click next git-svn-id: http://redmine.rubyforge.org/svn/trunk@1114 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1dfb57ff8..c8b55a695 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -119,17 +119,23 @@ module ApplicationHelper html = '' html << link_to_remote(('« ' + l(:label_previous)), - {:update => "content", :url => url_param.merge(page_param => paginator.current.previous)}, + {:update => 'content', + :url => url_param.merge(page_param => paginator.current.previous), + :complete => 'window.scrollTo(0,0)'}, {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous html << (pagination_links_each(paginator, options) do |n| link_to_remote(n.to_s, - {:url => {:params => url_param.merge(page_param => n)}, :update => 'content'}, + {:url => {:params => url_param.merge(page_param => n)}, + :update => 'content', + :complete => 'window.scrollTo(0,0)'}, {:href => url_for(:params => url_param.merge(page_param => n))}) end || '') html << ' ' + link_to_remote((l(:label_next) + ' »'), - {:update => "content", :url => url_param.merge(page_param => paginator.current.next)}, + {:update => 'content', + :url => url_param.merge(page_param => paginator.current.next), + :complete => 'window.scrollTo(0,0)'}, {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next unless count.nil? From 5dd3c239d35742b586dfac2ba829782561ff4365 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 17:24:58 +0000 Subject: [PATCH 176/710] Fixed: error when uploading a file with no content-type specified by the browser. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1115 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/attachment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 927aa1735..c9783b9ce 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -42,7 +42,7 @@ class Attachment < ActiveRecord::Base if @temp_file.size > 0 self.filename = sanitize_filename(@temp_file.original_filename) self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename - self.content_type = @temp_file.content_type.chomp + self.content_type = @temp_file.content_type.to_s.chomp self.filesize = @temp_file.size end end From 9c451ac15728ffd104d98a605435dee3cb4b39cd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 17:34:31 +0000 Subject: [PATCH 177/710] Set wiki class to issue notes. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1116 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/journals_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index c97446448..2f902432d 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -27,7 +27,7 @@ module JournalsHelper content << content_tag('div', links.join(' '), :class => 'contextual') end content << textilizable(journal, :notes) - content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'editable' : nil)) + content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki')) end def link_to_in_place_notes_editor(text, field_id, url, options={}) From 09cfef094cc9ba49da9a2d4435eef5079c6682ad Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 18:05:26 +0000 Subject: [PATCH 178/710] New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1117 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/images/jstoolbar/bt_br.png | Bin 244 -> 0 bytes public/images/jstoolbar/bt_code.png | Bin 338 -> 1045 bytes public/images/jstoolbar/bt_del.png | Bin 368 -> 368 bytes public/images/jstoolbar/bt_em.png | Bin 311 -> 312 bytes public/images/jstoolbar/bt_h1.png | Bin 995 -> 362 bytes public/images/jstoolbar/bt_h2.png | Bin 994 -> 371 bytes public/images/jstoolbar/bt_h3.png | Bin 993 -> 376 bytes public/images/jstoolbar/bt_img.png | Bin 566 -> 1142 bytes public/images/jstoolbar/bt_ins.png | Bin 322 -> 1055 bytes public/images/jstoolbar/bt_link.png | Bin 900 -> 408 bytes public/images/jstoolbar/bt_ol.png | Bin 249 -> 363 bytes public/images/jstoolbar/bt_pre.png | Bin 224 -> 1040 bytes public/images/jstoolbar/bt_quote.png | Bin 323 -> 0 bytes public/images/jstoolbar/bt_strong.png | Bin 355 -> 360 bytes public/images/jstoolbar/bt_ul.png | Bin 239 -> 365 bytes public/javascripts/jstoolbar/jstoolbar.js | 9 --------- .../javascripts/jstoolbar/lang/jstoolbar-bg.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-cs.js | 1 - .../javascripts/jstoolbar/lang/jstoolbar-de.js | 1 - .../javascripts/jstoolbar/lang/jstoolbar-en.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-es.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-fi.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-fr.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-he.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-it.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-ja.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-ko.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-lt.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-nl.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-pl.js | 3 +-- .../jstoolbar/lang/jstoolbar-pt-br.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-pt.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-ro.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-ru.js | 1 - .../javascripts/jstoolbar/lang/jstoolbar-sr.js | 3 +-- .../javascripts/jstoolbar/lang/jstoolbar-sv.js | 3 +-- .../jstoolbar/lang/jstoolbar-zh-tw.js | 1 - .../javascripts/jstoolbar/lang/jstoolbar-zh.js | 3 +-- 38 files changed, 18 insertions(+), 49 deletions(-) delete mode 100644 public/images/jstoolbar/bt_br.png delete mode 100644 public/images/jstoolbar/bt_quote.png diff --git a/public/images/jstoolbar/bt_br.png b/public/images/jstoolbar/bt_br.png deleted file mode 100644 index f8211a997071140a527e3ade540842b927a1baaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAf3?x51|2hvyF&8^|hH!9j+44{%5|C-v?4t;1OBOz`!j8!i<;h*8Kqrij}xVlsFfq zCYEI8=P86_=B6?j80s5X=o^?+6*DLSRVaW|1m~xflqVLYGWaGY7v<-srer26xMdcl zmgg5`7c2Ni?4GEl0#vK&>EaktF(;XWm66RxMLW!h$H%SVB#Xh#0}Tqo2db10q)3EK ZW)$+2)3>XAkPFnn;OXk;vd$@?2>>LEMzsI{ diff --git a/public/images/jstoolbar/bt_code.png b/public/images/jstoolbar/bt_code.png index 52924abf7c038c03fd1709422b0b426c18b39622..8b6aefbb5effeedceb8929e2bc6631d64771190d 100644 GIT binary patch literal 1045 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!eu-;DiE~kEVo7Fxo*8o|0J?9FfcO&_=LFrXJEK@?_N(& zPjPYaxpU{Tv$I=TT9T8K_wL>M{{8!)prEyD*DhVU^w_auQ>ILL{`~p(@85w&je^k- z7#<YcE$Lv+Psoh|jLcK2s?DxEp{ b-iq=5ZpIyE$5sGyBLjn{tDnm{r-UW|=&*Ik literal 338 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAd3?%E9GuQzs=3*z$5DpHG+YkL80J&NLJ|V6^ z`sw5AU7byVfxgF&?p9GzDl5&OIdkUio96@s_!ASNjg5^rZ(Q-_)xEu2<*k6K`AUNP zg8w4}2G;|3P5@;%3p^r=85p>QK$!8;-MT+OL9r6oh!W?b)Wnj^{5*w_%-mE414DfS z3w;BVs$vEupb7<$is1aTlJdl&R0iL~|zDqh}{#lRDf#3 zJzX3_D&{2b;AZD>F$`_?KIb%pm4}CCPr@@%w!6~O3TIa9l)t~%V!-gTe~DWM4f*y?%F diff --git a/public/images/jstoolbar/bt_del.png b/public/images/jstoolbar/bt_del.png index c6f3a8b40ed5d3845587a888d25f0941536764cb..36a912b65151f3bdad6e6ac7bf82a850bcf5e002 100644 GIT binary patch literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!eu-;DiE~kEVo7Fxo2BR0pdd@Sqpu?a!^VE@KZ&eBzF&Y(i0gj_hP7+gT3T8L z1qDr+GG*z~r9C}8)z#JCzkgR$RNT6CtAvDvp`oFZlarU1*WSH*i;Ih+qN0GhTUuI@ zlar4fJ9h2bwR`vOojZ4K&YU^nR^M}ga*QQGe!&b5&u*jvIkBEDjv*Dd=Jwv^Yf#{E zHfLYaz`+#v@4tI%%YnGBD%qFPd>QLDomeyf1k-xP32o;&yU!_G1Ph4YStHaLbL#Ay zeL`x*A|cY6?XyewE=&zgu5Jka8!5`|;c+x=|3{`CCZ@fS0iCTtix@mz{an^LB{Ts5 Di@1HC literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa%*9TgAsieWw;%dH0CJ53d_r7- z^qW`rCQs^$2oKiQ);xFiP+4g{A0O|=^~*dxT+`Ch?%ux8(p+<3-xgUJX)7!9MGI#L z`5w^%Y7;C8@(cdY@E;Yh-t)HrN^%x>L>4nJa0`JjPgJp2m3F609wl6>FVdQ&MBb@09uNP{r~^~ diff --git a/public/images/jstoolbar/bt_em.png b/public/images/jstoolbar/bt_em.png index f08de4f307569c741e1d120cd718d3073317e4b3..caa808236c6afd3284c222e34c3554616130d36c 100644 GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!eu-;DiE~kEVo7Fxog1K&|X0p1!W^HyDLP^!cQn z<}?6>I14-?iy0WWg+Q3`(%rg0KtYytM_)$E)e-c@Ne6s+b5ZC_<3{$2|Nls3V zii!#f3OaZ0oS~tiqN3v7y?dRUoT{s<@7=rS<>fVJ&YYf}p5o%-ty{NdXJ;?@ey0y; z0%J*#UoeBivm0qZj;*JQV@SoVx&5Ae3<^9<+H1Y<{{5eB`#3O9K#gT${#)h{uW4eR x&Q6(_IGOoNd%<(3f_VboK1ZfVHc0*RkL?p<`1HWbbUM%=22WQ%mvv4FO#u8CWHgdOUMOd?Xt6_lB;h-mn#I!Y+$cyQk~WcB0_ cNMe#?U^&QWnY-YWH&7dcr>mdKI;Vst0GR|Z`2YX_ diff --git a/public/images/jstoolbar/bt_h2.png b/public/images/jstoolbar/bt_h2.png index 36b15048e67810f070655de8d1c67fa7d2d3b04a..1e88cb936d295fffc18d41ff2dfafd98473bd91a 100644 GIT binary patch delta 360 zcmaFF{+UU!Gr-TCmrII^fq{Y7)59eQNDF{42Q!e=(B%)Fs3=xnmYS!hU}&L`m6%ti zr(k4YV4+}SXk~0{Wn`hCZD43%@JuP(2B@CB#M9T6{RX3uh%TRI$v-Bb5NCl$WHAE+ zw-5+3UbC&Z6PEOCCKkw=3 z*}He|`K}|mKtmZzg8YIR9G=}s19H+lT^vIyZmm7<&DWs7!*U^L;=^bMyL~x+f?lyeD;Ydp{an^LB{Ts59_fGD literal 994 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`X#OrCC){ui6xo&c?uz!xv2~( znYnrjhQgdKTth;iKnkC`wd1R5gp?U_GXY_oCO|{#S9GG zLLkg|>2BR0pdd@Sqpu?a!^VE@KZ&eBzF&Y(h%3;5IdkSzS63Go7jNCVb?w@<$;ru4 zQBf@|^({d`L7JMHQ>IK&R8%~6?AWzy*CZq)va_@Q|NnpQ-o2il9!pEh=g*&ed3h~e zy41_lxFK1C3@Z3GxeOaCmkj4aiCLba4!+xV84YH(!GS56gw14S%KviSGRG z%Ia&=80({^zghEu{B;+jlJ-?eFZ(C-9GL8xQ*qGOdB=|j&*C+_v&$Hk{%Dxny~$WU z-ALHRk7M21s-(chw)Z}tG zQe_Ji;wgd?0v(btiIVPik{pF~y$24;o;pAc8CY=)p}AQFgb zVTkHsNN&+gp2CnkMId>KVe%Y?Y#>?+MCXc^G88X8R=o6m@zU?bYo8ad{a(EFfAtin z>N#H3YXz#e9;@DZu4Rr_%bcK=rP(c84O_MbO<9{gWv}IwV?Z=z&N0iS=Yp1=Ygu}( zXYIMDwbzojUQ6D4uXyjh>T~DL-Me@1IS{=6{vHIrzyJRIKLf-6Q7{?;v=4y?^Y?;s zp>au&Uoh#mF%-_*+XvKM@9E+gQgJKy{Ar;k1D@8072RU4?yDXw@Q@8;Q`++3AOG^s z*<5v}>(4!NuxD6&;X*&#i&X1}F~1!h%AP;G;5skg!nRB4V%7dm6ZZ1euUBbp*4aC6 zyU?lUcW2z0ar)s5uM>|8&N|J}v)IBI?4@|lL2R|E(z&Bg@;97I&is@5;~OvYEydrC T@24LDI)%a0)z4*}Q$iB}Fpym5 delta 521 zcmV+k0`~p(2(|8AZlT5b}k??FfceEF*rIfFgh?XAS*C2FfjZuDYF0o00(qQO+^RT0Sys0F*uc- zhmmh5e*ua~L_t(|+QpN-PQx%1g>A=95NQYqkf!O(?ISJ# z=OC7e6HCkiQGx*yNev(aaC&_T5BB#a;ehKEf4K$VUvIJ?79FpekxVc`ILU#@0@K{k z^h2Hj>lk5dr0@L_Q!Bub$xIk(L>~jjOx(eZvD(7VL{=IaYz#Xz`IbBbF;w!X(Ghm9#>(*PRPZjgWC_D+|51h`WBi6y9 z4IDc^LjHBO;pjelo|mQ#YoG^H1BWrzBlrC!%PJHKlkf~Rzx(_RY_zgTe~DWM4f D=q`5E literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAd3?%E9GuQzs=3*z$5DpHG+YkL80J#PMJ|V6^ z`pv6*EzLC%;lWl`<_Gp|*|>h$-fqJ=X&JzUe$(qv_%CyJ@g z2I>(g3Gxg6&+s1wTr7%~1Ilq0ctjR6FmMZjFyp1Wb$@_@VkNE-CC){ui6xo&c?uz! zxv2~WhWZ8;`UWOd#SBV76$&5~!TD(=<%vb948DoUMftg@DVd21Zka`?<@rU~#R|R= zyC-U?0M*)ix;TbZ%t;n#VP=c**{FT1X?BfTldCwx#>EO#XRu1ME|qthVJyPBR7lb1 sV!Y)u1M`k$iB|3nj4Tp`yT#cU8r39TeXkTL0~*EP>FVdQ&MBb@0Iuk4&j0`b diff --git a/public/images/jstoolbar/bt_link.png b/public/images/jstoolbar/bt_link.png index 9b3acbae5fdaf7957c5131a2e7a777b0d6862d57..8d67e3d905c3556ed49ab61f59eda1015dc16195 100644 GIT binary patch literal 408 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!eu-;DiE~kEVo7Fxo?0v(btiIVPik{pF~z5-y^^$#PvS|!@Ya=&Ye4V z?AWojYu83aMI|REXJ=>6nKS46_wQ4tOerod?&<0I|NsBerAyCUyVlau;^gEM6ckil zUG3%NwRi8{=g*(NfB*iC>!p=IGZ{;Q{DK)Ap4~_Ta=JZT978H@`JVUWJEFkDazH}3 zr&CfP=l}ng8&raRJ#b!mbyELg%^IfTT}j`xW40@t|8lS9w(J*u1O1x^nB5ARzwf>* z7ttO3i+x*7qVC=Up=%uEkAITddTBuyv%^N=EWV8r(|tQpC7PTd$+V#o7rKZ9q+`eF3(!*?@r^4kCo`r0XmDp)78&qol`;+0JUSATmS$7 literal 900 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa%*9TgAsieWw;%dHU|?V@4DbnY z6_QbT`uKX$-iYezYH61xN3QR7s5$cO+qcMwkj%AvrcUXxceFk5v2^vSg%{pD=TR&1 znyFmBcJ=n{+tX)kiC)xUS9rjpyh74bw`FeOl{dR)ZoHhoX=2#aHEC&SItkmiU6^WY zY<&CXxj9Gv14Tp>9Rgd=7c4n!8eA~@(o!I>5A|;8pRXIadF`&##%X7!tT{XXO6sAr z51#zEXP@_E(#d`aJNs2T=e~J$uY3LVJ9n~w5wM8l3n?(Ii9 z7M3jAa!M~@)#ig&-u}GeSAXaK|Nr;yT)cPho}sbzi)XjBqId5*zOT8t+0Dma#&;pm z@B<&})~;QspC2G@m8D`;~B{}4l@LW=5l^c z+u-CjBQ`7{VZq$~UZp$lxg-M=T3jdGRhLrKn7y8pagz+k!808dd2BR0pdd@Sqpu?a!^VE@KZ&eBeqex4i0gj_hT^5qr)+)y z{Qdv6Yu9ROYAPx!N=Qfu2ncX+Z~#@ctbJa+_W9nud!wSFf`Wp)yu6&8oGdLZ4Gj%@ zwmzS;_x;ji?~ffjR$N@1ot>SWoXjb>?=estV@Z%-FoVOh8)-mJsHcl#NX4zzUT?kz z1p#MkC5=T5@&Y^lJ6_!&E%auc{$GY(mlTDf2Jt(ZPh+}DO$9H6b2QzLIU}90`P}>C xnpfF-BJ15cJ0?GCHSk)xEb{fzu(o$kn0J{lE&Z2X&yW>IQ+eo=O@f^Wp`iCQW^ zwR)Z|jv*Cul7IaF&&kz0{TSK&=d(u6{1-oD!MT1 z9he6c;wTR6hzvLtxm4KumCp9?+AFB|(0{3=Yq3qyahJo-U3d6}Ptb zIr23)a4@%TI`F?fr~K=qn;uTg!WswU4TGDmMi?_?&DkRL#Ef5HtIeIA2erb@C!}0a q&Od!a>~=_Th>XzBhU+)a&yO$q!f<2FGq)q4fb?|rb6Mw<&;$UBwp*V7 delta 194 zcmbQh@qjU`Gr-TCmrII^fq{Y7)59eQNQ;0lBQucP?%wYQq{K>GBTAf$QWHxu^Yau! zGILWI3=H)REc6Xbs)`wufGXHaJbhi+Z!q%mFk4A>Y@0PPUp<+DEx;$l6)5@U)x86$ zCHsIZrjj7P;QtIyw;Ol?dAgo1jv*Ddl7IaF&&;e>6S2JE;hHCGZJVb#QJpKp9+V7RJk_(rq0_#jXtgQu&X%Q~loCIHWIK7arK diff --git a/public/images/jstoolbar/bt_quote.png b/public/images/jstoolbar/bt_quote.png deleted file mode 100644 index 25b2b8abe112f25e24c200f27548faeaa67c3000..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 323 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAd3?%E9GuQzs=3*z$5DpHG+YkL80J-V`J|V6^ z`sw5AGiS~W4D>yIbhnC%QdehFS!w>}jVo^7JZEfdoR|>(=G8p`@BNuTrMx9Ue!>5d z0E6udr8b}hXMsm#F#`j)5C}6~x?A@LC@5Cq8d2h0l$uzQnV+W+l9`*zU|^_kV4-hd zQdP{L1XQ5_QW2b=R#Ki=l*-_nm|T>fo0^iDsNj}alv5>CyIC*TA wOUzKQwR<`-MSCW|+4o;X9C3Q9`0%$ISr>mdKI;Vst0BZZU0vXy zuQ=v(={I**I1j`AGXIig)9;1&%w({g#=AwK`sM*KsqRn-pP4OXr7kQ5)30ZJTYWv~ vE!QVIi`8y?ov)XO>{Iv1nR}e!|2KwSDW)^5G68Hrn;1M@{an^LB{Ts5Kzn&$ literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa%*9TgAsieWw;%dH0CJ53d_r7- z^qW`r%1ZOCtjrJW+cJ4l7at$c0M~P84@HCrZ(P5urMX5{M*8mU3)bzCRh^W7yO^$KPq6o=WhX&)cknvu8p_9L~U`~k)y z@)K4EH(8`FPHwn%SnAB1TZbR9b~aC(Cn9@I`Auokm$a-zQwjBl!NtqW&(59j_VV;; a515%$nF_DmHBAEA!{F)a=d#Wzp$P!bQiKNp diff --git a/public/images/jstoolbar/bt_ul.png b/public/images/jstoolbar/bt_ul.png index 6e20851ec2e1cf21f69dd1e18af25a3453deb860..df9ecc0cfd80167a3b3ca46f0d9c7c73797c4c57 100644 GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!eu-;DiE~kEVo7FxonZ#981GS;^N}$?Cj*^co)_tpi8S96504)Ujjd4xT*O8pAl9H3HO19GkCiCxvXyW>IQ+eo=O@f^Wp`iCQW^ zwJM%2jv*Cul7IaF&&+J}kLL)#G?#>&)6M^lIUmdKI;Vst0RNgsK>z>% diff --git a/public/javascripts/jstoolbar/jstoolbar.js b/public/javascripts/jstoolbar/jstoolbar.js index 7941bd135..7a2a2a49e 100644 --- a/public/javascripts/jstoolbar/jstoolbar.js +++ b/public/javascripts/jstoolbar/jstoolbar.js @@ -406,15 +406,6 @@ jsToolBar.prototype.elements.del = { } } -// quote -jsToolBar.prototype.elements.quote = { - type: 'button', - title: 'Inline quote', - fn: { - wiki: function() { this.singleTag('??') } - } -} - // code jsToolBar.prototype.elements.code = { type: 'button', diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-bg.js b/public/javascripts/jstoolbar/lang/jstoolbar-bg.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-bg.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-bg.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-cs.js b/public/javascripts/jstoolbar/lang/jstoolbar-cs.js index fa18a871c..8a59a8162 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-cs.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-cs.js @@ -3,7 +3,6 @@ jsToolBar.strings['Strong'] = 'Tučné'; jsToolBar.strings['Italic'] = 'Kurzíva'; jsToolBar.strings['Underline'] = 'Podtržené'; jsToolBar.strings['Deleted'] = 'Přeškrtnuté '; -jsToolBar.strings['Inline quote'] = 'Citace'; jsToolBar.strings['Code'] = 'Zobrazení kódu'; jsToolBar.strings['Heading 1'] = 'Záhlaví 1'; jsToolBar.strings['Heading 2'] = 'Záhlaví 2'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-de.js b/public/javascripts/jstoolbar/lang/jstoolbar-de.js index 0c7a8799e..e2ba3fc1c 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-de.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-de.js @@ -3,7 +3,6 @@ jsToolBar.strings['Strong'] = 'Fett'; jsToolBar.strings['Italic'] = 'Kursiv'; jsToolBar.strings['Underline'] = 'Unterstrichen'; jsToolBar.strings['Deleted'] = 'Durchgestrichen'; -jsToolBar.strings['Inline quote'] = 'Inline-Zitat'; jsToolBar.strings['Code'] = 'Quelltext'; jsToolBar.strings['Heading 1'] = 'Überschrift 1. Ordnung'; jsToolBar.strings['Heading 2'] = 'Überschrift 2. Ordnung'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-en.js b/public/javascripts/jstoolbar/lang/jstoolbar-en.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-en.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-en.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-es.js b/public/javascripts/jstoolbar/lang/jstoolbar-es.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-es.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-es.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-fi.js b/public/javascripts/jstoolbar/lang/jstoolbar-fi.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-fi.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-fi.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-fr.js b/public/javascripts/jstoolbar/lang/jstoolbar-fr.js index 402c34d29..3cbc67863 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-fr.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-fr.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Gras'; jsToolBar.strings['Italic'] = 'Italique'; jsToolBar.strings['Underline'] = 'Souligné'; jsToolBar.strings['Deleted'] = 'Rayé'; -jsToolBar.strings['Inline quote'] = 'Citation'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Code en ligne'; jsToolBar.strings['Heading 1'] = 'Titre niveau 1'; jsToolBar.strings['Heading 2'] = 'Titre niveau 2'; jsToolBar.strings['Heading 3'] = 'Titre niveau 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-he.js b/public/javascripts/jstoolbar/lang/jstoolbar-he.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-he.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-he.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-it.js b/public/javascripts/jstoolbar/lang/jstoolbar-it.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-it.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-it.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-ja.js b/public/javascripts/jstoolbar/lang/jstoolbar-ja.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ja.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-ja.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-ko.js b/public/javascripts/jstoolbar/lang/jstoolbar-ko.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ko.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-ko.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-lt.js b/public/javascripts/jstoolbar/lang/jstoolbar-lt.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-lt.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-lt.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-nl.js b/public/javascripts/jstoolbar/lang/jstoolbar-nl.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-nl.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-nl.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-pl.js b/public/javascripts/jstoolbar/lang/jstoolbar-pl.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pl.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-pl.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js b/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-pt.js b/public/javascripts/jstoolbar/lang/jstoolbar-pt.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pt.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-pt.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-ro.js b/public/javascripts/jstoolbar/lang/jstoolbar-ro.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ro.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-ro.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-ru.js b/public/javascripts/jstoolbar/lang/jstoolbar-ru.js index 92940f23a..6370a3e2d 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ru.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-ru.js @@ -3,7 +3,6 @@ jsToolBar.strings['Strong'] = 'Жирный'; jsToolBar.strings['Italic'] = 'Курсив'; jsToolBar.strings['Underline'] = 'Подчеркнутый'; jsToolBar.strings['Deleted'] = 'Зачеркнутый'; -jsToolBar.strings['Inline quote'] = 'Цитата'; jsToolBar.strings['Code'] = 'Вставка кода'; jsToolBar.strings['Heading 1'] = 'Заголовок 1'; jsToolBar.strings['Heading 2'] = 'Заголовок 2'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-sr.js b/public/javascripts/jstoolbar/lang/jstoolbar-sr.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-sr.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-sr.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-sv.js b/public/javascripts/jstoolbar/lang/jstoolbar-sv.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-sv.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-sv.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js index 7e795ec6d..1e46e2470 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js @@ -3,7 +3,6 @@ jsToolBar.strings['Strong'] = '粗體'; jsToolBar.strings['Italic'] = '斜體'; jsToolBar.strings['Underline'] = '底線'; jsToolBar.strings['Deleted'] = '刪除線'; -jsToolBar.strings['Inline quote'] = '內嵌引文'; jsToolBar.strings['Code'] = '程式碼'; jsToolBar.strings['Heading 1'] = '標題 1'; jsToolBar.strings['Heading 2'] = '標題 2'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-zh.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh.js index 72bab0be3..cd36a4b55 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-zh.js @@ -3,8 +3,7 @@ jsToolBar.strings['Strong'] = 'Strong'; jsToolBar.strings['Italic'] = 'Italic'; jsToolBar.strings['Underline'] = 'Underline'; jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Inline quote'] = 'Inline quote'; -jsToolBar.strings['Code'] = 'Code'; +jsToolBar.strings['Code'] = 'Inline Code'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; From 4e244be21cc1c9fbd885081a6d02527b67a5034d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 3 Feb 2008 19:53:52 +0000 Subject: [PATCH 179/710] Slight changes on users list view and hide Anonymous user. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1118 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/users_controller.rb | 4 +-- app/helpers/users_helper.rb | 2 +- app/views/users/list.rhtml | 50 +++++++++++++---------------- public/stylesheets/application.css | 6 ++++ 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f67d1ae53..ceb70ab92 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -33,8 +33,8 @@ class UsersController < ApplicationController sort_init 'login', 'asc' sort_update - @status = params[:status] ? params[:status].to_i : 1 - conditions = nil + @status = params[:status] ? params[:status].to_i : 1 + conditions = "status <> 0" conditions = ["status=?", @status] unless @status == 0 @user_count = User.count(:conditions => conditions) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 9dc87c5cc..7bd137161 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -17,7 +17,7 @@ module UsersHelper def status_options_for_select(selected) - options_for_select([[l(:label_all), "*"], + options_for_select([[l(:label_all), ''], [l(:status_active), 1], [l(:status_registered), 2], [l(:status_locked), 3]], selected) diff --git a/app/views/users/list.rhtml b/app/views/users/list.rhtml index 50054c989..7415af308 100644 --- a/app/views/users/list.rhtml +++ b/app/views/users/list.rhtml @@ -4,11 +4,10 @@

    <%=l(:label_user_plural)%>

    -<% form_tag() do %> +<% form_tag({}, :method => :get) do %>
    <%= l(:label_filter_plural) %> <%= select_tag 'status', status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> -<%= submit_tag l(:button_apply), :class => "small" %>
    <% end %>   @@ -20,38 +19,33 @@ <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
    <%= sort_header_tag('admin', :caption => l(:field_admin)) %> - <%= sort_header_tag('status', :caption => l(:field_status)) %> <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on)) %> -<% for user in @users %> - "> - - - - - - - - - +<% for user in @users -%> + <%= %w(anon active registered locked)[user.status] %>"> + + + + + + + + -<% end %> +<% end -%>
    <%=l(:field_mail)%>
    <%= link_to user.login, :action => 'edit', :id => user %><%= user.firstname %><%= user.lastname %><%= user.mail %><%= image_tag 'true.png' if user.admin? %><%= image_tag 'locked.png' if user.locked? %><%= image_tag 'user_new.png' if user.registered? %><%= format_time(user.created_on) %><%= format_time(user.last_login_on) unless user.last_login_on.nil? %> - <% form_tag({:action => 'edit', :id => user}) do %> - <% if user.locked? %> - <%= hidden_field_tag 'user[status]', User::STATUS_ACTIVE %> - <%= submit_tag l(:button_unlock), :class => "button-small" %> - <% elsif user.registered? %> - <%= hidden_field_tag 'user[status]', User::STATUS_ACTIVE %> - <%= submit_tag l(:button_activate), :class => "button-small" %> - <% else %> - <%= hidden_field_tag 'user[status]', User::STATUS_LOCKED %> - <%= submit_tag l(:button_lock), :class => "button-small" %> - <% end %> - <% end %> -
    <%= link_to user.login, :action => 'edit', :id => user %><%= user.firstname %><%= user.lastname %><%= image_tag('true.png') if user.admin? %><%= format_time(user.created_on) %> + + <% if user.locked? -%> + <%= link_to l(:button_unlock), {:action => 'edit', :id => user, :user => {:status => User::STATUS_ACTIVE}}, :method => :post, :class => 'icon icon-unlock' %> + <% elsif user.registered? -%> + <%= link_to l(:button_activate), {:action => 'edit', :id => user, :user => {:status => User::STATUS_ACTIVE}}, :method => :post, :class => 'icon icon-unlock' %> + <% else -%> + <%= link_to l(:button_lock), {:action => 'edit', :id => user, :user => {:status => User::STATUS_LOCKED}}, :method => :post, :class => 'icon icon-lock' %> + <% end -%> + +
    diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 1090d8c87..1abd1f6fd 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -94,6 +94,12 @@ tr.message td.last_message { font-size: 80%; } tr.message.locked td.subject a { background-image: url(../images/locked.png); } tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; } +tr.user td { width:13%; } +tr.user td.email { width:18%; } +tr.user td { white-space: nowrap; } +tr.user.locked, tr.user.registered { color: #aaa; } +tr.user.locked a, tr.user.registered a { color: #aaa; } + table.list tbody tr:hover { background-color:#ffffdd; } table td {padding:2px;} table p {margin:0;} From a3dda6dc4a3dd898357b5aaab6acffdb03f4da5e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 4 Feb 2008 18:01:38 +0000 Subject: [PATCH 180/710] Use Postgresql's reset_pk_sequence in Trac importer to reset issue id sequence (#595). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1119 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 63a719b35..2eca13dc3 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -373,16 +373,8 @@ namespace :redmine do end end - # update issue id sequence if needed - begin - case ActiveRecord::Base.connection.adapter_name.downcase - when 'mysql' - # nothing to do - when 'postgresql' - sql = "SELECT setval('#{Issue.table_name}_id_seq', (SELECT MAX(id) FROM #{Issue.table_name}))" - ActiveRecord::Base.connection.execute(sql) - end - end + # update issue id sequence if needed (postgresql) + Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!') puts # Wiki From 80a60247f527825aa5e02e19bcaec4bd864fce2a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 4 Feb 2008 19:37:23 +0000 Subject: [PATCH 181/710] Slight changes to the activity view. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1120 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/projects_helper.rb | 4 ++++ app/views/projects/activity.rhtml | 23 ++++++++++++++--------- public/stylesheets/application.css | 6 ++++++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 883be0ead..c07bbd2c2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -26,6 +26,10 @@ module ProjectsHelper }, options end + def format_activity_description(text) + h(truncate(text, 250)) + end + def project_settings_tabs tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index 41a8ae100..bde806554 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -1,17 +1,22 @@

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

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

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

    -
      - <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| %> -
    • <%= format_time(e.event_datetime, false) %> <%= link_to truncate(e.event_title, 100), e.event_url %>
      - <% unless e.event_description.blank? %><%= truncate(e.event_description, 500) %>
      <% end %> - <%= e.event_author if e.respond_to?(:event_author) %>

    • +

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

      +
      +<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> +
      <%= format_time(e.event_datetime, false) %> + <%= link_to truncate(e.event_title, 100), e.event_url %>
      +
      <% unless e.event_description.blank? -%> + <%= format_activity_description(e.event_description) %>
      <% end %> -
    -<% end %> + <%= e.event_author if e.respond_to?(:event_author) %> +<% end -%> + +<% end -%> +
    -<% if @events_by_day.empty? %>

    <%= l(:label_no_data) %>

    <% end %> +<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
    <% prev_params = params.clone.update :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 1abd1f6fd..b3b8b341d 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -147,6 +147,12 @@ 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; } +div#activity dt { margin-bottom: 1px; } +div#activity dt .time { color: #777; font-size: 80%; } +div#activity dd .description { font-style: italic; } + .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;} #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; } From 54ff294da164d9c8a0659610019f801ae4788068 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 5 Feb 2008 17:28:02 +0000 Subject: [PATCH 182/710] More appropriate default sort order on sortable columns. Sortable column added on issue subject (#580). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1121 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/queries_helper.rb | 4 +++- app/helpers/sort_helper.rb | 11 +++++++---- app/models/query.rb | 11 ++++++----- app/views/admin/projects.rhtml | 6 +++--- app/views/issues/_list.rhtml | 2 +- app/views/users/list.rhtml | 8 ++++---- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 3011d3aec..a58c5d0ea 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -22,7 +22,9 @@ module QueriesHelper end def column_header(column) - column.sortable ? sort_header_tag(column.sortable, :caption => column.caption) : content_tag('th', column.caption) + column.sortable ? sort_header_tag(column.sortable, :caption => column.caption, + :default_order => column.default_order) : + content_tag('th', column.caption) end def column_content(column, issue) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index dfd681fff..155e7e5e3 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -92,7 +92,7 @@ module SortHelper # - The optional caption explicitly specifies the displayed link text. # - A sort icon image is positioned to the right of the sort link. # - def sort_link(column, caption=nil) + def sort_link(column, caption, default_order) key, order = session[@sort_name][:key], session[@sort_name][:order] if key == column if order.downcase == 'asc' @@ -104,11 +104,13 @@ module SortHelper end else icon = nil - order = 'desc' # changed for desc order by default + order = default_order end caption = titleize(Inflector::humanize(column)) unless caption - url = {:sort_key => column, :sort_order => order, :issue_id => params[:issue_id], :project_id => params[:project_id]} + url = {:sort_key => column, :sort_order => order, :status => params[:status], + :issue_id => params[:issue_id], + :project_id => params[:project_id]} link_to_remote(caption, {:update => "content", :url => url}, @@ -138,8 +140,9 @@ module SortHelper # def sort_header_tag(column, options = {}) caption = options.delete(:caption) || titleize(Inflector::humanize(column)) + default_order = options.delete(:default_order) || 'asc' options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title] - content_tag('th', sort_link(column, caption), options) + content_tag('th', sort_link(column, caption, default_order), options) end private diff --git a/app/models/query.rb b/app/models/query.rb index 7127a464f..a4103c08a 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -16,12 +16,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class QueryColumn - attr_accessor :name, :sortable + attr_accessor :name, :sortable, :default_order include GLoc def initialize(name, options={}) self.name = name self.sortable = options[:sortable] + self.default_order = options[:default_order] end def caption @@ -94,18 +95,18 @@ class Query < ActiveRecord::Base @@available_columns = [ QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"), QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"), - QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position"), - QueryColumn.new(:subject), + QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'), + QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), QueryColumn.new(:author), QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"), - QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on"), + QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"), QueryColumn.new(:fixed_version), QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"), - QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on"), + QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), ] cattr_reader :available_columns diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index d35c484b5..c42845622 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -17,9 +17,9 @@ <%= sort_header_tag('name', :caption => l(:label_project)) %> <%=l(:field_description)%> - <%=l(:field_is_public)%> <%=l(:label_subproject_plural)%> - <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + <%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> @@ -28,8 +28,8 @@ "> <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> <%= textilizable project.short_description, :project => project %> - <%= image_tag 'true.png' if project.is_public? %> <%= project.children.size %> + <%= image_tag 'true.png' if project.is_public? %> <%= format_date(project.created_on) %> diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index d8e3102df..ff91f34f7 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -6,7 +6,7 @@ :method => :get}, {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %> - <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %> + <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> <% query.columns.each do |column| %> <%= column_header(column) %> <% end %> diff --git a/app/views/users/list.rhtml b/app/views/users/list.rhtml index 7415af308..e12aa3425 100644 --- a/app/views/users/list.rhtml +++ b/app/views/users/list.rhtml @@ -17,10 +17,10 @@ <%= sort_header_tag('login', :caption => l(:field_login)) %> <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %> <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %> - <%=l(:field_mail)%> - <%= sort_header_tag('admin', :caption => l(:field_admin)) %> - <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> - <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on)) %> + <%= sort_header_tag('mail', :caption => l(:field_mail)) %> + <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %> From 14b2d3f4b9ab95a19c7bcac2e5f3563077103eb0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 5 Feb 2008 18:38:05 +0000 Subject: [PATCH 183/710] Slight change to the issue added/updated email templates. Translations updates: * Simplified Chinese (Liang Jin) * Bulgarian (Nikolay Solakov) * Lithuanian (Sergej Jegorov) * Traditional Chinese (Shortie Lo) git-svn-id: http://redmine.rubyforge.org/svn/trunk@1122 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/mailer/issue_add.text.html.rhtml | 2 +- app/views/mailer/issue_add.text.plain.rhtml | 3 +- app/views/mailer/issue_edit.text.html.rhtml | 5 +- app/views/mailer/issue_edit.text.plain.rhtml | 9 +- lang/bg.yml | 104 ++--- lang/cs.yml | 4 +- lang/de.yml | 4 +- lang/en.yml | 4 +- lang/es.yml | 4 +- lang/fi.yml | 4 +- lang/fr.yml | 4 +- lang/he.yml | 4 +- lang/it.yml | 4 +- lang/ja.yml | 4 +- lang/ko.yml | 4 +- lang/lt.yml | 134 +++---- lang/nl.yml | 4 +- lang/pl.yml | 4 +- lang/pt-br.yml | 4 +- lang/pt.yml | 4 +- lang/ro.yml | 4 +- lang/ru.yml | 4 +- lang/sr.yml | 4 +- lang/sv.yml | 4 +- lang/zh-tw.yml | 18 +- lang/zh.yml | 376 +++++++++--------- .../jstoolbar/lang/jstoolbar-lt.js | 18 +- 27 files changed, 372 insertions(+), 369 deletions(-) diff --git a/app/views/mailer/issue_add.text.html.rhtml b/app/views/mailer/issue_add.text.html.rhtml index 6d20c333f..b1c4605e6 100644 --- a/app/views/mailer/issue_add.text.html.rhtml +++ b/app/views/mailer/issue_add.text.html.rhtml @@ -1,3 +1,3 @@ -<%= l(:text_issue_added, "##{@issue.id}") %> +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %>
    <%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_add.text.plain.rhtml b/app/views/mailer/issue_add.text.plain.rhtml index 38c17e777..c6cb0837f 100644 --- a/app/views/mailer/issue_add.text.plain.rhtml +++ b/app/views/mailer/issue_add.text.plain.rhtml @@ -1,3 +1,4 @@ -<%= l(:text_issue_added, "##{@issue.id}") %> +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %> + ---------------------------------------- <%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.html.rhtml b/app/views/mailer/issue_edit.text.html.rhtml index 9af8592b4..7ce1f47c4 100644 --- a/app/views/mailer/issue_edit.text.html.rhtml +++ b/app/views/mailer/issue_edit.text.html.rhtml @@ -1,10 +1,11 @@ -<%= l(:text_issue_updated, "##{@issue.id}") %>
    -<%= @journal.user.name %> +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> +
      <% for detail in @journal.details %>
    • <%= show_detail(detail, true) %>
    • <% end %>
    + <%= textilizable(@journal.notes) %>
    <%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.plain.rhtml b/app/views/mailer/issue_edit.text.plain.rhtml index 04b1bc54a..b5a5ec978 100644 --- a/app/views/mailer/issue_edit.text.plain.rhtml +++ b/app/views/mailer/issue_edit.text.plain.rhtml @@ -1,8 +1,9 @@ -<%= l(:text_issue_updated, "##{@issue.id}") %> -<%= @journal.user.name %> -<% for detail in @journal.details %> +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> + +<% for detail in @journal.details -%> <%= show_detail(detail, true) %> -<% end %> +<% end -%> + <%= @journal.notes if @journal.notes? %> ---------------------------------------- <%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/lang/bg.yml b/lang/bg.yml index 7aac092cb..b783df592 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -73,8 +73,8 @@ notice_email_sent: Изпратен e-mail на %s notice_email_error: Грешка при изпращане на e-mail (%s) notice_feeds_access_key_reseted: Вашия ключ за RSS достъп беше променен. -error_scm_not_found: Несъществуващ обект в склада. -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_scm_not_found: Несъществуващ обект в хранилището. +error_scm_command_failed: "Грешка при опит за комуникация с хранилище: %s" mail_subject_lost_password: Вашата парола mail_body_lost_password: 'За да смените паролата си, използвайте следния линк:' @@ -95,9 +95,9 @@ field_filename: Файл field_filesize: Големина field_downloads: Downloads field_author: Автор -field_created_on: Създадена +field_created_on: От дата field_updated_on: Обновена -field_field_format: Формат +field_field_format: Тип field_is_for_all: За всички проекти field_possible_values: Възможни стойности field_regexp: Регулярен израз @@ -178,8 +178,8 @@ setting_host_name: Хост setting_text_formatting: Форматиране на текста setting_wiki_compression: Wiki компресиране на историята setting_feeds_limit: Лимит на Feeds -setting_autofetch_changesets: Автоматично обработване на commits в склада -setting_sys_api_enabled: Разрешаване на WS за управление на склада +setting_autofetch_changesets: Автоматично обработване на commits в хранилището +setting_sys_api_enabled: Разрешаване на WS за управление на хранилището setting_commit_ref_keywords: Отбелязващи ключови думи setting_commit_fix_keywords: Приключващи ключови думи setting_autologin: Автоматичен вход @@ -254,7 +254,7 @@ label_subproject_plural: Подпроекти label_min_max_length: Мин. - Макс. дължина label_list: Списък label_date: Дата -label_integer: Число +label_integer: Целочислен label_boolean: Чекбокс label_string: Текст label_text: Дълъг текст @@ -332,7 +332,7 @@ label_ago: преди label_contains: съдържа label_not_contains: не съдържа label_day_plural: дни -label_repository: Склад +label_repository: Хранилище label_browse: Разглеждане label_modification: %d промяна label_modification_plural: %d промени @@ -418,7 +418,7 @@ label_week: Седмица label_date_from: От label_date_to: До label_language_based: В зависимост от езика -label_sort_by: Sort by %s +label_sort_by: Сортиране по %s label_send_test_email: Изпращане на тестов e-mail label_feeds_access_key_created_on: %s от създаването на RSS ключа label_module_plural: Модули @@ -482,8 +482,8 @@ text_tracker_no_workflow: Няма дефиниран работен проце text_unallowed_characters: Непозволени символи text_comma_separated: Позволено е изброяване (с разделител запетая). text_issues_ref_in_commit_messages: Отбелязване и приключване на задачи от commit съобщения -text_issue_added: Публикувана е нова задача с номер %s. -text_issue_updated: Задача %s е обновена. +text_issue_added: Публикувана е нова задача с номер %s (by %s). +text_issue_updated: Задача %s е обновена (by %s). text_wiki_destroy_confirmation: Сигурни ли сте, че искате да изтриете това Wiki и цялото му съдържание? text_issue_category_destroy_question: Има задачи (%d) обвързани с тази категория. Какво ще изберете? text_issue_category_destroy_assignments: Премахване на връзките с категорията @@ -514,12 +514,12 @@ default_activity_development: Разработка enumeration_issue_priorities: Приоритети на задачи enumeration_doc_categories: Категории документи enumeration_activities: Дейности (time tracking) -label_file_plural: Files +label_file_plural: Файлове label_changeset_plural: Changesets field_column_names: Колони label_default_columns: По подразбиране setting_issue_list_default_columns: Показвани колони по подразбиране -setting_repositories_encodings: Encodings на складовете +setting_repositories_encodings: Кодови таблици на хранилищата notice_no_issue_selected: "Няма избрани задачи." label_bulk_edit_selected_issues: Редактиране на задачи label_no_change_option: (Без промяна) @@ -527,42 +527,42 @@ notice_failed_to_save_issues: "Неуспешен запис на %d задач label_theme: Тема label_default: По подразбиране label_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 into Redmine. -mail_body_account_information: Your Redmine 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: Redmine 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 +label_nobody: никой +button_change_password: Промяна на парола +text_user_mail_option: "За неизбраните проекти, ще получавате известия само за наблюдавани дейности или в които участвате (т.е. автор или назначени на мен)." +label_user_mail_option_selected: "За всички събития само в избраните проекти..." +label_user_mail_option_all: "За всяко събитие в проектите, в които участвам" +label_user_mail_option_none: "Само за наблюдавани или в които участвам (автор или назначени на мен)" +setting_emails_footer: Подтекст за e-mail +label_float: Дробно +button_copy: Копиране +mail_body_account_information_external: Можете да използвате вашия "%s" акаунт за вход в Redmine. +mail_body_account_information: Информацията за акаунта ви в Redmine +setting_protocol: Протокол +label_user_mail_no_self_notified: "Не искам известия за извършени от мен промени" +setting_time_format: Формат на часа +label_registration_activation_by_email: активиране на акаунта по email +mail_subject_account_activation_request: Заявка за активиране на акаунт в Redmine +mail_body_account_activation_request: 'Има новорегистриран потребител (%s), очакващ вашето одобрение:' +label_registration_automatic_activation: автоматично активиране +label_registration_manual_activation: ръчно активиране +notice_account_pending: "Акаунтът Ви е създаден и очаква одобрение от администратор." +field_time_zone: Часова зона +text_caracters_minimum: Минимум %d символа. +setting_bcc_recipients: Blind carbon copy (bcc) получатели +button_annotate: Анотация +label_issues_by: Задачи по %s +field_searchable: С възможност за търсене +label_display_per_page: 'На страница по: %s' +setting_per_page_options: Опции за страниране +label_age: Възраст +notice_default_data_loaded: Примерната информацията е успешно заредена. +text_load_default_configuration: Зареждане на примерна информация +text_no_configuration_data: "Все още не са конфигурирани Роли, тракери, статуси на задачи и работен процес.\nСтрого се препоръчва зареждането на примерната информация. Веднъж заредена ще имате възможност да я редактирате." +error_can_t_load_default_data: "Грешка при зареждане на примерната информация: %s" +button_update: Обновяване +label_change_properties: Промяна на настройки +label_general: Основни +label_repository_plural: Хранилища +label_associated_revisions: Асоциирани ревизии +setting_user_format: Потребителски формат diff --git a/lang/cs.yml b/lang/cs.yml index d6307babe..cec7d2a31 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -505,10 +505,10 @@ button_rename: Rename text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? label_module_plural: Modules label_jump_to_a_project: Jump to a project... -text_issue_updated: Issue %s has been updated. +text_issue_updated: Issue %s has been updated by %s. field_redirect_existing_links: Redirect existing links text_issue_category_reassign_to: Reassing issues to this category -text_issue_added: Issue %s has been reported. +text_issue_added: Issue %s has been reported by %s. label_file_plural: Files text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? label_updated_time: Updated %s ago diff --git a/lang/de.yml b/lang/de.yml index f0f054b1b..a2222af83 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -531,8 +531,8 @@ text_tracker_no_workflow: Kein Workflow für diesen Tracker definiert. text_unallowed_characters: Nicht erlaubte Zeichen text_comma_separated: Mehrere Werte erlaubt (durch Komma getrennt). text_issues_ref_in_commit_messages: Ticket-Beziehungen und -Status in Commit-Log-Meldungen -text_issue_added: Ticket %s wurde erstellt. -text_issue_updated: Ticket %s wurde aktualisiert. +text_issue_added: Ticket %s wurde erstellt by %s. +text_issue_updated: Ticket %s wurde aktualisiert by %s. text_wiki_destroy_confirmation: Sind Sie sicher, dass Sie dieses Wiki mit sämtlichem Inhalt löschen möchten? text_issue_category_destroy_question: Einige Tickets (%d) sind dieser Kategorie zugeodnet. Was möchten Sie tun? text_issue_category_destroy_assignments: Kategorie-Zuordnung entfernen diff --git a/lang/en.yml b/lang/en.yml index 534a4834e..8c54e1c79 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -531,8 +531,8 @@ text_tracker_no_workflow: No workflow defined for this tracker text_unallowed_characters: Unallowed characters text_comma_separated: Multiple values allowed (comma separated). text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages -text_issue_added: Issue %s has been reported. -text_issue_updated: Issue %s has been updated. +text_issue_added: Issue %s has been reported by %s. +text_issue_updated: Issue %s has been updated by %s. text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? text_issue_category_destroy_assignments: Remove category assignments diff --git a/lang/es.yml b/lang/es.yml index 9023f2b6a..f8cd9e300 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -505,12 +505,12 @@ label_jump_to_a_project: Ir al proyecto... field_assignable: Se pueden asignar peticiones a este perfil label_sort_by: Ordenar por %s setting_issue_list_default_columns: Columnas por defecto para la lista de peticiones -text_issue_updated: La petición %s ha sido actualizada. +text_issue_updated: La petición %s ha sido actualizada por %s. notice_feeds_access_key_reseted: Su clave de acceso para RSS ha sido reiniciada field_redirect_existing_links: Redireccionar enlaces existentes text_issue_category_reassign_to: Reasignar las peticiones a la categoría notice_email_sent: Se ha enviado un correo a %s -text_issue_added: Petición añadida +text_issue_added: Petición añadida por %s. field_comments: Comentario label_file_plural: Archivos text_wiki_destroy_confirmation: ¿Seguro que quiere borrar el wiki y todo su contenido? diff --git a/lang/fi.yml b/lang/fi.yml index 5dbe5e8a5..cf04b23b4 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -533,8 +533,8 @@ text_tracker_no_workflow: Ei työnkulkua määritelty tälle tiketille text_unallowed_characters: Kiellettyjä merkkejä text_comma_separated: Useat arvot sallittu (pilkku eroteltuna). text_issues_ref_in_commit_messages: Liitän ja korjaan ongelmia syötetyssä viestissä -text_issue_added: Tapahtuma %s on kirjattu. -text_issue_updated: Tapahtuma %s on päivitetty. +text_issue_added: Tapahtuma %s on kirjattu by %s. +text_issue_updated: Tapahtuma %s on päivitetty by %s. text_wiki_destroy_confirmation: Oletko varma että haluat poistaa tämän wiki:n ja kaikki sen sisältämän tiedon? text_issue_category_destroy_question: Jotkut tapahtumat (%d) ovat nimetty tälle luokalle. Mitä haluat tehdä? text_issue_category_destroy_assignments: Poista luokan tehtävät diff --git a/lang/fr.yml b/lang/fr.yml index 1c1e7b5c2..7df8ec622 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -532,8 +532,8 @@ text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker text_unallowed_characters: Caractères non autorisés text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules). text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits -text_issue_added: La demande %s a été soumise. -text_issue_updated: La demande %s a été mise à jour. +text_issue_added: La demande %s a été soumise par %s. +text_issue_updated: La demande %s a été mise à jour par %s. text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ? text_issue_category_destroy_question: Des demandes (%d) sont affectées à cette catégories. Que voulez-vous faire ? text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie diff --git a/lang/he.yml b/lang/he.yml index 9b274c58d..7f26341b8 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -494,8 +494,8 @@ text_tracker_no_workflow: זרימת עבודה לא הוגדרה עבור עו text_unallowed_characters: תווים לא מורשים text_comma_separated: הכנסת ערכים מרובים מותרת (מופרדים בפסיקים). text_issues_ref_in_commit_messages: קישור ותיקום נושאים בהודעות הפקדות -text_issue_added: הנושא %s דווח. -text_issue_updated: הנושא %s עודכן. +text_issue_added: הנושא %s דווח (by %s). +text_issue_updated: הנושא %s עודכן (by %s). text_wiki_destroy_confirmation: האם אתה בטוח שברצונך למחוק את הWIKI הזה ואת כל תוכנו? text_issue_category_destroy_question: כמה נושאים (%d) מוצבים לקטגוריה הזו. מה ברצונך לעשות? text_issue_category_destroy_assignments: הסר הצבת קטגוריה diff --git a/lang/it.yml b/lang/it.yml index 5f07a2f24..7b27a54cb 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -482,8 +482,8 @@ text_tracker_no_workflow: Nessun workflow definito per questo tracker text_unallowed_characters: Unallowed characters text_comma_separated: Multiple values allowed (comma separated). text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages -text_issue_added: "E' stata segnalata l'anomalia %s." -text_issue_updated: "L'anomalia %s e' stata aggiornata." +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 diff --git a/lang/ja.yml b/lang/ja.yml index 3266256c2..0b3926a23 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -483,8 +483,8 @@ text_tracker_no_workflow: このトラッカーにワークフローが定義さ text_unallowed_characters: 使えない文字です text_comma_separated: (カンマで区切った)複数の値が使えます text_issues_ref_in_commit_messages: コミットメッセージ内で問題の参照/修正 -text_issue_added: 問題 %s が報告されました。 -text_issue_updated: 問題 %s が更新されました。 +text_issue_added: 問題 %s が報告されました。 (by %s) +text_issue_updated: 問題 %s が更新されました。 (by %s) text_wiki_destroy_confirmation: 本当にこのwikiとその内容の全てを削除しますか? text_issue_category_destroy_question: このカテゴリに割り当て済みの問題(%d)があります。何をしようとしていますか? text_issue_category_destroy_assignments: カテゴリの割り当てを削除する diff --git a/lang/ko.yml b/lang/ko.yml index 7f93fe94a..b9b9542fc 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -500,8 +500,8 @@ text_tracker_no_workflow: 이 추적타입(tracker)에 워크플로우가 정의 text_unallowed_characters: 허용되지 않는 문자열 text_comma_separated: 복수의 값들이 허용됩니다.(구분자 ,) text_issues_ref_in_commit_messages: 커밋메시지에서 티켓을 참조하거나 해결하기 -text_issue_added: 티켓[%s]이 보고되었습니다. -text_issue_updated: 티켓[%s]이 수정되었습니다. +text_issue_added: 티켓[%s]이 보고되었습니다 (by %s). +text_issue_updated: 티켓[%s]이 수정되었습니다 (by %s). text_wiki_destroy_confirmation: 이 위키와 모든 내용을 지우시겠습니까? text_issue_category_destroy_question: 일부 티켓들(%d개)이 이 카테고리에 할당되어 있습니다. 어떻게 하시겠습니까? text_issue_category_destroy_assignments: 카테고리 할당 지우기 diff --git a/lang/lt.yml b/lang/lt.yml index 51686f9ed..6f2965353 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -2,11 +2,11 @@ _gloc_rule_default: '|n| n==1 ? "" : "_plural" ' actionview_datehelper_select_day_prefix: actionview_datehelper_select_month_names: sausis,vasaris,kovas,balandis,gegužė,birželis,liepa,rugpjūtis,rugsėjis,spalis,lapkritis,gruodis -actionview_datehelper_select_month_names_abbr: sausis,vasaris,kovas,balandis,gegužė,birželis,liepa,rugpjūtis,rugsėjis,spalis,lapkritis,gruodis -actionview_datehelper_select_month_prefix: +actionview_datehelper_select_month_names_abbr: Sau,Vas,Kov,Bal,Geg,Brž,Lie,Rgp,Rgs,Spl,Lap,Grd +actionview_datehelper_select_month_prefix: actionview_datehelper_select_year_prefix: -actionview_datehelper_time_in_words_day: 1 diena -actionview_datehelper_time_in_words_day_plural: %d dienos +actionview_datehelper_time_in_words_day: 1 diena +actionview_datehelper_time_in_words_day_plural: %d dienų actionview_datehelper_time_in_words_hour_about: apytiksliai valanda actionview_datehelper_time_in_words_hour_about_plural: apie %d valandas actionview_datehelper_time_in_words_hour_about_single: apytiksliai valanda @@ -68,6 +68,7 @@ notice_successful_delete: Sėkmingas panaikinimas. notice_successful_connection: Sėkmingas susijungimas. notice_file_not_found: Puslapis, į kurį ketinate įeiti, neegzistuoja arba pašalintas. notice_locking_conflict: Duomenys atnaujinti kito vartotojo. +notice_scm_error: Duomenys ir/ar pakeitimai saugykloje(repozitorojoje) neegzistuoja. notice_not_authorized: Jūs neturite teisių gauti prieigą prie šio puslapio. notice_email_sent: Laiškas išsiųstas %s notice_email_error: Laiško siųntimo metu įvyko klaida (%s) @@ -105,7 +106,7 @@ field_author: Autorius field_created_on: Sukūrta field_updated_on: Atnaujinta field_field_format: Formatas -field_is_for_all: Visiems laukasms +field_is_for_all: Visiems projektams field_possible_values: Galimos reikšmės field_regexp: Pastovi išraiška field_min_length: Minimalus ilgis @@ -114,14 +115,14 @@ field_value: Vertė field_category: Kategorija field_title: Pavadinimas field_project: Projektas -field_issue: Svarstoma problema +field_issue: Darbas field_status: Būsena field_notes: Pastabos -field_is_closed: Svarstoma problema uždaryta +field_is_closed: Darbas uždarytas field_is_default: Numatytoji vertė field_tracker: Pėdsekys -field_subject: Dalykas -field_due_date: Mokėjimo terminas +field_subject: Tema +field_due_date: Užbaigimo data field_assigned_to: Paskirtas field_priority: Prioritetas field_fixed_version: Pastovi versija @@ -129,9 +130,9 @@ field_user: Vartotojas field_role: Vaidmuo field_homepage: Pagrindinis puslapis field_is_public: Viešas -field_parent: Yra subprojektas -field_is_in_chlog: Svarstomos problemos rodomos pokyčių žurnale -field_is_in_roadmap: Svarstomos problemos rodomos veiklos grafike +field_parent: Priklauso projektui +field_is_in_chlog: Darbai rodomi pokyčių žurnale +field_is_in_roadmap: Darbai rodomi veiklos grafike field_login: Registracijos vardas field_mail_notification: Elektroninio pašto pranešimai field_admin: Administratorius @@ -165,16 +166,15 @@ field_activity: Veikla field_spent_on: Data field_identifier: Identifikuotojas field_is_filter: Panaudotas kaip filtras -field_issue_to_id: Susijusi svarstoma problema +field_issue_to_id: Susijęs darbas field_delay: Užlaikymas -field_assignable: Svarstomos problemos gali būti paskirtos šiam vaidmeniui +field_assignable: Darbai gali būti paskirti šiam vaidmeniui field_redirect_existing_links: Peradresuokite egzistuojančias sąsajas -field_estimated_hours: Apskaičiuotas laikas +field_estimated_hours: Numatyta trukmė field_column_names: Skiltys field_time_zone: Laiko juosta field_searchable: Randamas field_default_value: Numatytoji vertė - setting_app_title: Programos pavadinimas setting_app_subtitle: Programos paantraštė setting_welcome_text: Pasveikinimas @@ -182,7 +182,7 @@ setting_default_language: Numatytoji kalba setting_login_required: Reikalingas autentiškumo nustatymas setting_self_registration: Saviregistracija setting_attachment_max_size: Priedo maks. dydis -setting_issues_export_limit pagal dydį: Svarstomų problemų eksportavimo riba +setting_issues_export_limit pagal dydį: Darbų eksportavimo riba setting_mail_from: Emisijos elektroninio pašto adresas setting_bcc_recipients: Akli tikslios kopijos gavėjai (bcc) setting_host_name: Pagrindinio kompiuterio vardas @@ -196,9 +196,9 @@ setting_commit_fix_keywords: Fiksavimo reikšminiai žodžiai setting_autologin: Autoregistracija setting_date_format: Datos formatas setting_time_format: Laiko formatas -setting_cross_project_issue_relations: Leisti tarprojektinius svarstomos problemos ryšius -setting_issue_list_default_columns: Numatytosios skiltys svarstomos problemos sąraše -setting_repositories_encodings: Saugyklos encodingas +setting_cross_project_issue_relations: Leisti tarprojektinius darbų ryšius +setting_issue_list_default_columns: Numatytosios skiltys darbų sąraše +setting_repositories_encodings: Saugyklos enkodingas setting_emails_footer: elektroninio pašto puslapinė poraštė setting_protocol: Protokolas @@ -210,11 +210,11 @@ label_project_new: Naujas projektas label_project_plural: Projektai label_project_all: Visi Projektai label_project_latest: Paskutiniai projektai -label_issue: Svarstoma problema -label_issue_new: Nauja svarstoma problema -label_issue_plural: Svarstomos problemos -label_issue_view_all: Peržiūrėti visas svarstomas problemas -label_issues_by: Svarstomos problemos pagal %s +label_issue: Darbas +label_issue_new: Naujas darbas +label_issue_plural: Darbai +label_issue_view_all: Peržiūrėti visus darbus +label_issues_by: Darbai pagal %s label_document: Dokumentas label_document_new: Naujas dokumentas label_document_plural: Dokumentai @@ -229,11 +229,11 @@ label_tracker: Pėdsekys label_tracker_plural: Pėdsekiai label_tracker_new: Naujas pėdsekys label_workflow: Darbų eiga -label_issue_status: Svarstomos problemos padėtis -label_issue_status_plural: Svarstomos problemos padėtys +label_issue_status: Darbo padėtis +label_issue_status_plural: Darbų padėtys label_issue_status_new: Nauja padėtis -label_issue_category: Svarstomos problemos kategorija -label_issue_category_plural: Svarstomos problemos kategorijos +label_issue_category: Darbo kategorija +label_issue_category_plural: Darbo kategorijos label_issue_category_new: Nauja kategorija label_custom_field: Kliento laukas label_custom_field_plural: Kliento laukai @@ -247,14 +247,14 @@ label_register: Užsiregistruoti label_password_lost: Prarastas slaptažodis label_home: Pagrindinis label_my_page: Mano puslapis -label_my_account: Mano pranešimas +label_my_account: Mano paskyra label_my_projects: Mano projektai -label_administration: Administracija +label_administration: Administravimas label_login: Prisijungti label_logout: Atsijungti label_help: Pagalba -label_reported_issues: Praneštos svarstomos problemos -label_assigned_to_me_issues: Svarstomos problemos, paskirtos man +label_reported_issues: Pranešti darbai +label_assigned_to_me_issues: Darbai, priskirti man label_last_login: Paskutinis ryšys label_last_updates: Paskutinis atnaujinimas label_last_updates_plural: %d paskutinis atnaujinimas @@ -304,10 +304,10 @@ label_confirmation: Patvirtinimas label_export_to: Eksportuoti į label_read: Skaitykite... label_public_projects: Vieši projektai -label_open_issues: atidarytas -label_open_issues_plural: atidaryti -label_closed_issues: uždarytas -label_closed_issues_plural: uždaryti +label_open_issues: atidaryta +label_open_issues_plural: atidarytos +label_closed_issues: uždaryta +label_closed_issues_plural: uždarytos label_total: Bendra suma label_permissions: Leidimai label_current_status: Einamoji padėtis @@ -364,15 +364,15 @@ label_latest_revision: Paskutinė revizija label_latest_revision_plural: Paskutinės revizijos label_view_revisions: Pežiūrėti revizijas label_max_size: Maksimalus dydis -label_on: 'ant' +label_on: 'iš' label_sort_highest: Perkelti į viršūnę label_sort_higher: Perkelti į viršų label_sort_lower: Perkelti žemyn label_sort_lowest: Perkelti į apačią label_roadmap: Veiklos grafikas -label_roadmap_due_in: Baigiama +label_roadmap_due_in: Baigiasi po label_roadmap_overdue: %s vėluojama -label_roadmap_no_issues: Jokios svarstomos problemos šiai versijai +label_roadmap_no_issues: Jokio darbo šiai versijai nėra label_search: Ieškoti label_result_plural: Rezultatai label_all_words: Visi žodžiai @@ -387,7 +387,7 @@ label_current_version: Einamoji versija label_preview: Peržiūra label_feed_plural: Įeitys(Feeds) label_changes_details: Visų pakeitimų detalės -label_issue_tracking: Svarstomų problemų sekimas +label_issue_tracking: Darbų sekimas label_spent_time: Sugaištas laikas label_f_hour: %.2f valanda label_f_hour_plural: %.2f valandų @@ -402,8 +402,8 @@ label_diff_side_by_side: šalia label_options: Pasirinkimai label_copy_workflow_from: Kopijuoti darbų eiga iš label_permissions_report: Leidimų pranešimas -label_watched_issues: Stebėtos svarstomos problemos -label_related_issues: Susijusios svarstomos problemos +label_watched_issues: Stebimi darbai +label_related_issues: Susiję darbai label_applied_status: Taikomoji padėtis label_loading: Kraunama... label_relation_new: Naujas ryšys @@ -448,7 +448,7 @@ label_file_plural: Bylos label_changeset_plural: Changesets label_default_columns: Numatytosios skiltys label_no_change_option: (Jokio pakeitimo) -label_bulk_edit_selected_issues: Masinis pasirinktų svarstomųjų problemų(issues) redagavimas +label_bulk_edit_selected_issues: Masinis pasirinktų darbų(issues) redagavimas label_theme: Tema label_default: Numatyta(as) label_search_titles_only: Ieškoti pavadinimų tiktai @@ -483,7 +483,7 @@ button_back: Atgal button_cancel: Atšaukti button_activate: Aktyvinti button_sort: Rūšiuoti -button_log_time: Log laikas +button_log_time: Praleistas laikas button_rollback: Grįžti į šią versiją button_watch: Stebėti button_unwatch: Nestebėti @@ -500,7 +500,7 @@ status_active: aktyvus status_registered: užregistruotas status_locked: užrakintas -text_select_mail_notifications: Išrinkite veiksmus, apie kuriuos būtų pranešta elektroniniu pasštu. +text_select_mail_notifications: Išrinkite veiksmus, apie kuriuos būtų pranešta elektroniniu paštu. text_regexp_info: pvz. ^[A-Z0-9]+$ text_min_max_length_info: 0 reiškia jokių apribojimų text_project_destroy_confirmation: Ar esate įsitikinęs, kad jūs norite pašalinti šį projektą ir visus susijusius duomenis? @@ -511,7 +511,7 @@ text_journal_set_to: nustatyta į %s text_journal_deleted: ištrintas text_tip_task_begin_day: užduotis, prasidedanti šią dieną text_tip_task_end_day: užduotis, pasibaigianti šią dieną -text_tip_task_begin_end_day: užduoties prasidedanti ir pasibaigianti šią dieną +text_tip_task_begin_end_day: užduotis, prasidedanti ir pasibaigianti šią dieną text_project_identifier_info: 'Mažosios raidės (a-z), skaičiai ir brūkšniai galimi.
    Išsaugojus, identifikuotojas negali būti keičiamas.' text_caracters_maximum: %d simbolių maksimumas. text_caracters_minimum: Turi būti mažiausiai %d simbolių ilgio. @@ -519,14 +519,14 @@ text_length_between: Ilgis tarp %d ir %d simbolių. text_tracker_no_workflow: Jokia darbų eiga neapibrėžta šiam pėdsekiui text_unallowed_characters: Neleistini simboliai text_comma_separated: Leistinos kelios reikšmės (atskirtos kableliu). -text_issues_ref_in_commit_messages: Nurodymas ir fiksavimas svarstomų problemų pavedimų(commit) pranešimuose -text_issue_added: Svarstoma problema %s buvo pranešta. -text_issue_updated: Svarstoma problema %s buvo atnaujinta. +text_issues_ref_in_commit_messages: Darbų pavedimų(commit) nurodymas ir fiksavimas pranešimuose +text_issue_added: Darbas %s buvo praneštas (by %s). +text_issue_updated: Darbas %s buvo atnaujintas (by %s). text_wiki_destroy_confirmation: Ar esate įsitikinęs, kad jūs norite pašalinti wiki ir visą jos turinį? -text_issue_category_destroy_question: Kai kurios svarstomos problemos (%d) yra paskirtos šiai kategorijai. Ką jūs norite padaryti? +text_issue_category_destroy_question: Kai kurie darbai (%d) yra paskirti šiai kategorijai. Ką jūs norite daryti? text_issue_category_destroy_assignments: Pašalinti kategorijos užduotis -text_issue_category_reassign_to: Iš naujo paskirti svarstomas problemas šiai kategorijai -text_user_mail_option: "neišrinktiems projektams, jūs tiktai gausite pranešimus apie daiktus, kuriuos jūs stebite, ar jūs esate įtrauktas į (eg. svarstomos problemos, jūs esate autorius ar įgaliotinis)." +text_issue_category_reassign_to: Iš naujo priskirti darbus šiai kategorijai +text_user_mail_option: "neišrinktiems projektams, jūs tiktai gausite pranešimus apie įvykius, kuriuos jūs stebite, arba į kuriuos esate įtrauktas (pvz. darbai, jūs esate autorius ar įgaliotinis)." default_role_manager: Vadovas default_role_developper: Projektuotojas @@ -550,20 +550,20 @@ default_priority_immediate: Neatidėliotinas default_activity_design: Projektavimas default_activity_development: Vystymas -enumeration_issue_priorities: Svarstomos problemos prioritetai +enumeration_issue_priorities: Darbo prioritetai enumeration_doc_categories: Dokumento kategorijos enumeration_activities: Veiklos (laiko sekimas) -label_display_per_page: 'Per page: %s' +label_display_per_page: '%s įrašų puslapyje' setting_per_page_options: Objects per page options -notice_default_data_loaded: Default configuration successfully loaded. -label_age: Age -label_general: General -button_update: Update -setting_issues_export_limit: Issues export limit -label_change_properties: Change properties -text_load_default_configuration: Load the default configuration -text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." -label_repository_plural: Repositories -error_can_t_load_default_data: "Default configuration could not be loaded: %s" -label_associated_revisions: Associated revisions -setting_user_format: Users display format +notice_default_data_loaded: Numatytoji konfiguracija sėkmingai užkrauta. +label_age: Amžius +label_general: Bendri +button_update: Atnaujinti +setting_issues_export_limit: Darbų eksportavimo limitas +label_change_properties: Pakeisti nustatymus +text_load_default_configuration: Užkrauti numatytąj konfiguraciją +text_no_configuration_data: "Vaidmenys, pėdsekiai, darbų būsenos ir darbų eiga dar nebuvo konfigūruoti.\nGriežtai rekomenduojam užkrauti numatytąją(default)konfiguraciją. Užkrovus, galėsite ją modifikuoti." +label_repository_plural: Saugiklos +error_can_t_load_default_data: "Numatytoji konfiguracija negali būti užkrauta: %s" +label_associated_revisions: susijusios revizijos +setting_user_format: Vartotojo atvaizdavimo formatas diff --git a/lang/nl.yml b/lang/nl.yml index dc413b058..c2d692706 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -482,8 +482,8 @@ text_tracker_no_workflow: Geen workflow gedefinieerd voor deze tracker text_unallowed_characters: Niet toegestane tekens text_coma_separated: Meerdere waarden toegestaan (door komma's gescheiden). text_issues_ref_in_commit_messages: Opzoeken en aanpassen van issues in commit berichten -text_issue_added: Issue %s is gerapporteerd. -text_issue_updated: Issue %s is gewijzigd. +text_issue_added: Issue %s is gerapporteerd (by %s). +text_issue_updated: Issue %s is gewijzigd (by %s). text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? text_issue_category_destroy_assignments: Remove category assignments diff --git a/lang/pl.yml b/lang/pl.yml index 10b5cd029..0f99628af 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -500,12 +500,12 @@ label_this_week: ten tydzień label_jump_to_a_project: Skocz do projektu... field_assignable: Zagadnienia mogą być przypisane do tej roli label_sort_by: Sortuj po %s -text_issue_updated: Zagadnienie %s zostało zaktualizowane. +text_issue_updated: Zagadnienie %s zostało zaktualizowane (by %s). notice_feeds_access_key_reseted: Twój klucz dostępu RSS został zrestetowany. field_redirect_existing_links: Przekierowanie istniejących odnośników text_issue_category_reassign_to: Przydziel zagadnienie do tej kategorii notice_email_sent: Email został wysłany do %s -text_issue_added: Zagadnienie %s zostało wprowadzone. +text_issue_added: Zagadnienie %s zostało wprowadzone (by %s). text_wiki_destroy_confirmation: Jesteś pewien, że chcesz usunąć to wiki i całą jego zawartość ? notice_email_error: Wystąpił błąd w trakcie wysyłania maila (%s) label_updated_time: Zaktualizowane %s temu diff --git a/lang/pt-br.yml b/lang/pt-br.yml index b246cfd30..3c6fd1265 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -482,8 +482,8 @@ 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. -text_issue_updated: Tarefa %s foi alterada. +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 diff --git a/lang/pt.yml b/lang/pt.yml index aa98d15a8..3b5647d53 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -482,8 +482,8 @@ text_tracker_no_workflow: Sem workflow definido para este tipo. text_unallowed_characters: Caracteres não permitidos text_comma_separated: Permitido múltiplos valores (separados por vírgula). text_issues_ref_in_commit_messages: Referenciando e arrumando tarefas nas mensagens de commit -text_issue_added: Tarefa %s foi incluída. -text_issue_updated: Tarefa %s foi alterada. +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 diff --git a/lang/ro.yml b/lang/ro.yml index d9011c21e..2fed755ac 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -480,8 +480,8 @@ text_tracker_no_workflow: Nu este definit nici un workflow pentru acest tip de t text_unallowed_characters: Caractere nepermise text_comma_separated: Se poate folosi valori multiple (separate de virgula). text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages -text_issue_added: Tichetul %s a fost raportat. -text_issue_updated: tichetul %s a fost modificat. +text_issue_added: Tichetul %s a fost raportat (by %s). +text_issue_updated: tichetul %s a fost modificat (by %s). text_wiki_destroy_confirmation: Sunteti sigur ca vreti sa stergeti acest wiki si continutul ei ? text_issue_category_destroy_question: Cateva tichete (%d) apartin acestei categorii. Cum vreti sa procedati ? text_issue_category_destroy_assignments: Remove category assignments diff --git a/lang/ru.yml b/lang/ru.yml index 04b17b806..44b1664cf 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -531,8 +531,8 @@ text_tracker_no_workflow: Для этого трекера последоват text_unallowed_characters: Запрещенные символы text_comma_separated: Допустимы несколько значений (разделенные запятой). text_issues_ref_in_commit_messages: Сопоставление и изменение статуса задач исходя из текста сообщений -text_issue_added: О вопросе %s был создает отчет. -text_issue_updated: Вопрос %s был обновлен. +text_issue_added: О вопросе %s был создает отчет (by %s). +text_issue_updated: Вопрос %s был обновлен (by %s). text_wiki_destroy_confirmation: Вы уверены, что хотите удалить данную вики и все содержание? text_issue_category_destroy_question: Несколько задач (%d) назначено в данную категорию. Что вы хотите предпринять? text_issue_category_destroy_assignments: Удалить назначения категории diff --git a/lang/sr.yml b/lang/sr.yml index 71503be01..3132beeec 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -504,8 +504,8 @@ text_tracker_no_workflow: Tok rada nije definisan za ovaj tracker text_unallowed_characters: Nedozvoljeni karakteri text_comma_separated: Višestruke vrednosti su dozvoljene (razdvojene zarezom). text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages -text_issue_added: Kartica %s je prijavljena. -text_issue_updated: Kartica %s je izmenjena. +text_issue_added: Kartica %s je prijavljena (by %s). +text_issue_updated: Kartica %s je izmenjena (by %s). text_wiki_destroy_confirmation: Da li ste sigurni da želite da izbrišete ovaj wiki i svu njegovu sadržinu ? text_issue_category_destroy_question: Neke kartice (%d) su dodeljene ovoj kategoriji. Šta želite da uradite ? text_issue_category_destroy_assignments: Ukloni dodeljivanje kategorija diff --git a/lang/sv.yml b/lang/sv.yml index 604590aff..b8a9a3a11 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -482,8 +482,8 @@ text_tracker_no_workflow: Inget workflow definerat för denna tracker text_unallowed_characters: Unallowed characters text_comma_separated: Multiple values allowed (comma separated). text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages -text_issue_added: Brist %s har rapporterats. -text_issue_updated: Brist %s har uppdaterats. +text_issue_added: Brist %s har rapporterats (by %s). +text_issue_updated: Brist %s har uppdaterats (by %s). text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ? text_issue_category_destroy_assignments: Remove category assignments diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index d468b162b..94631b86d 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -191,16 +191,16 @@ setting_host_name: 主機名稱 setting_text_formatting: 文字格式 setting_wiki_compression: 壓縮 Wiki 歷史文章 setting_feeds_limit: Feed content limit -setting_autofetch_changesets: Autofetch commits -setting_sys_api_enabled: Enable WS for repository management -setting_commit_ref_keywords: Referencing keywords -setting_commit_fix_keywords: Fixing keywords +setting_autofetch_changesets: 自動取得送交版次 +setting_sys_api_enabled: 啟用管理版本庫之網頁服務 (Web Service) +setting_commit_ref_keywords: 用於參照項目之關鍵字 +setting_commit_fix_keywords: 用於修正項目之關鍵字 setting_autologin: 自動登入 setting_date_format: 日期格式 setting_time_format: 時間格式 setting_cross_project_issue_relations: 允許關聯至其它專案的項目 setting_issue_list_default_columns: 預設顯示於項目清單的欄位 -setting_repositories_encodings: Repositories encodings +setting_repositories_encodings: 版本庫編碼 setting_emails_footer: 電子郵件附帶說明 setting_protocol: 協定 setting_per_page_options: 每頁顯示個數選項 @@ -362,7 +362,7 @@ label_modification: %d 變更 label_modification_plural: %d 變更 label_revision: 版次 label_revision_plural: 版次清單 -label_associated_revisions: Associated revisions +label_associated_revisions: 相關版次 label_added: 已新增 label_modified: 已修改 label_deleted: 已刪除 @@ -530,9 +530,9 @@ 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 個項目 -text_issue_updated: 已更新 %s 個項目 +text_issues_ref_in_commit_messages: 送交訊息中參照(或修正)項目之關鍵字 +text_issue_added: 已通報 %s 個項目 (by %s) +text_issue_updated: 已更新 %s 個項目 (by %s) text_wiki_destroy_confirmation: 您確定要刪除這個 wiki 和其中的所有內容? text_issue_category_destroy_question: 有 (%d) 個項目被指派到此分類. 請選擇您想要的動作? text_issue_category_destroy_assignments: 移除這些項目的分類 diff --git a/lang/zh.yml b/lang/zh.yml index 469bfb0c5..70ee8b137 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -1,5 +1,5 @@ -# translated by andy wu -# email:andywu.zh@gmail.com +# Originally translated by andy wu (email:andywu.zh@gmail.com) +# new translations added by the translation team from 72pines at http://code.72pines.org/projects/show/72pines-redmine/ _gloc_rule_default: '|n| n==1 ? "" : "_plural" ' @@ -32,12 +32,12 @@ activerecord_error_blank: 不能是空格 activerecord_error_too_long: 太长 activerecord_error_too_short: 太短 activerecord_error_wrong_length: 长度有问题 -activerecord_error_taken: has already been taken +activerecord_error_taken: 已经存在了 activerecord_error_not_a_number: 不是数字 activerecord_error_not_a_date: 不是有效的日期 activerecord_error_greater_than_start_date: 必需大于开始日期 -activerecord_error_not_same_project: doesn't belong to the same project -activerecord_error_circular_dependency: This relation would create a circular dependency +activerecord_error_not_same_project: 不属于同一个项目 +activerecord_error_circular_dependency: 这个设置会造成循环关系的错误 general_fmt_age: %d yr general_fmt_age_plural: %d yrs @@ -58,12 +58,12 @@ general_first_day_of_week: '7' notice_account_updated: 帐户更新成功。 notice_account_invalid_creditentials: 用户名或密码不正确 -notice_account_password_updated: 成功更新口令 -notice_account_wrong_password: 错误的口令 +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_can_t_change_password: 该帐户使用了外部认证。无法更改密码。 +notice_account_lost_email_sent: 邮件已被发送,邮件中有关于选择新密码的指导 notice_account_activated: 您的帐号已被激活。您现在可以登录了。 notice_successful_create: 创建成功 notice_successful_update: 更新成功 @@ -71,18 +71,18 @@ notice_successful_delete: 删除成功 notice_successful_connection: 连接成功 notice_file_not_found: 您访问的页面不存在或已被删除。 notice_locking_conflict: 数据已被另一个用户更新 -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: 对不起,您无权访问此页面. +notice_email_sent: 已经成功发送email到 %s +notice_email_error: 发送email时发生错误 (%s) +notice_feeds_access_key_reseted: 您的RSS访问代码已经被重置了. -error_scm_not_found: 在版本库中不存在该条目或修订 -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_scm_not_found: 在源代码库中不存在该条目或修订 +error_scm_command_failed: "访问源代码库的时候发生错误: %s" -mail_subject_lost_password: 您的redMine口令 -mail_body_lost_password: 'To change your Redmine password, click on the following link:' +mail_subject_lost_password: 您的redMine密码 +mail_body_lost_password: '点击以下的链接来修改您在redmine系统的密码:' mail_subject_register: redMine帐户激活 -mail_body_register: 'To activate your Redmine account, click on the following link:' +mail_body_register: '点击以下的链接来激活您的redMine帐号:' gui_validation_error: 1 个错误 gui_validation_error_plural: %d 个错误 @@ -110,10 +110,10 @@ field_value: 值 field_category: 分类 field_title: 标题 field_project: 项目 -field_issue: 任务 +field_issue: 问题 field_status: 状态 field_notes: 说明 -field_is_closed: 已关闭的任务 +field_is_closed: 已关闭的问题 field_is_default: 默认状态 field_tracker: 跟踪 field_subject: 主题 @@ -126,16 +126,16 @@ field_role: 角色 field_homepage: 主页 field_is_public: 公开 field_parent: 上级项目 -field_is_in_chlog: 在更新日志中显示任务 -field_is_in_roadmap: 在路线图中显示任务 +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: 密码 +field_new_password: 新密码 field_password_confirmation: 确认 field_version: 版本 field_type: 类别 @@ -147,7 +147,7 @@ field_attr_login: 登录名属性 field_attr_firstname: 名字属性 field_attr_lastname: 姓属性 field_attr_mail: 邮件属性 -field_onthefly: On-the-fly user creation +field_onthefly: 即时的用户生成 field_start_date: 开始 field_done_ratio: %% 完成 field_auth_source: 认证模式 @@ -159,14 +159,14 @@ field_subproject: 子项目 field_hours: Hours field_activity: 活动 field_spent_on: 日期 -field_identifier: Identifier -field_is_filter: Used as a filter -field_issue_to_id: Related issue -field_delay: Delay -field_assignable: Issues can be assigned to this role -field_redirect_existing_links: Redirect existing links -field_estimated_hours: Estimated time -field_default_value: Default value +field_identifier: 标识 +field_is_filter: 作为过滤条件 +field_issue_to_id: 相关的问题报告 +field_delay: 延期 +field_assignable: 是否可以分配到问题 +field_redirect_existing_links: 重定向现有的链接 +field_estimated_hours: 预期时间 +field_default_value: 默认值 setting_app_title: 应用程序标题 setting_app_subtitle: 应用程序子标题 @@ -175,19 +175,19 @@ setting_default_language: 默认语言 setting_login_required: 要求认证 setting_self_registration: 允许自注册 setting_attachment_max_size: 附件最大尺寸 -setting_issues_export_limit: Issues export limit -setting_mail_from: Emission mail address +setting_issues_export_limit: 问题输出条目的限制 +setting_mail_from: 邮件发送人的地址 setting_host_name: 主机名称 setting_text_formatting: 文本格式 -setting_wiki_compression: Wiki history compression -setting_feeds_limit: Feed content limit -setting_autofetch_changesets: Autofetch commits -setting_sys_api_enabled: Enable WS for repository management -setting_commit_ref_keywords: Referencing keywords -setting_commit_fix_keywords: Fixing keywords -setting_autologin: Autologin -setting_date_format: Date format -setting_cross_project_issue_relations: Allow cross-project issue relations +setting_wiki_compression: Wiki文档的历史记录压缩 +setting_feeds_limit: RSS Feed内容条数 +setting_autofetch_changesets: 自动获取程序变动 +setting_sys_api_enabled: 启用后台的WS代码用于管理 +setting_commit_ref_keywords: 提交变动时引用问题号码的关键字 +setting_commit_fix_keywords: 提交变动时自动标志问题状态的关键字 +setting_autologin: 自动登录 +setting_date_format: 日期格式 +setting_cross_project_issue_relations: 允许不同项目之间的问题关联 label_user: 用户 label_user_plural: 用户列表 @@ -195,12 +195,12 @@ label_user_new: 新建用户 label_project: 项目 label_project_new: 新建项目 label_project_plural: 项目列表 -label_project_all: All Projects -label_project_latest: 最近的项目列表 -label_issue: 任务 -label_issue_new: 新建任务 -label_issue_plural: 任务列表 -label_issue_view_all: 查看所有任务 +label_project_all: 所有的项目 +label_project_latest: 最近更新的项目 +label_issue: 问题 +label_issue_new: 新建问题 +label_issue_plural: 问题列表 +label_issue_view_all: 查看所有问题 label_document: 文档 label_document_new: 新建文档 label_document_plural: 文档列表 @@ -214,13 +214,13 @@ 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_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: 新建自定义字段 @@ -230,7 +230,7 @@ label_information: 信息 label_information_plural: 信息 label_please_login: 请登录 label_register: 注册 -label_password_lost: 忘记口令 +label_password_lost: 忘记密码 label_home: 主页 label_my_page: 我的工作台 label_my_account: 我的帐号 @@ -240,7 +240,7 @@ label_login: 登录 label_logout: 退出 label_help: 帮助 label_reported_issues: 已报告的问题 -label_assigned_to_me_issues: 分配给我的任务 +label_assigned_to_me_issues: 分配给我的问题 label_last_login: 最后登录 label_last_updates: 最后更新 label_last_updates_plural: %d 最后更新 @@ -255,12 +255,12 @@ label_auth_source_new: 新建认证模式 label_auth_source_plural: 认证模式列表 label_subproject_plural: 子项目列表 label_min_max_length: 最小 - 最大 长度 -label_list: list -label_date: Date -label_integer: Integer -label_boolean: Boolean -label_string: Text -label_text: Long text +label_list: 列表 +label_date: 日期 +label_integer: 整数 +label_boolean: 是否 +label_string: 文字 +label_text: 长段文字 label_attribute: 属性 label_attribute_plural: 属性 label_download: %d 个下载次数 @@ -296,7 +296,7 @@ label_closed_issues_plural: 已关闭 label_total: 合计 label_permissions: 权限列表 label_current_status: 当前状态 -label_new_statuses_allowed: New statuses allowed +label_new_statuses_allowed: 启用了新状态 label_all: 全部 label_none: 无 label_next: 下一个 @@ -306,7 +306,7 @@ label_details: 详情 label_add_note: 添加说明 label_per_page: 每面 label_calendar: 日历 -label_months_from: months from +label_months_from: 个月以来 label_gantt: 甘特图(Gantt) label_internal: 内部 label_last_changes: 最近的 %d 次更改 @@ -328,14 +328,14 @@ label_in_less_than: 剩余天数小于 label_in_more_than: 剩余天数大于 label_in: 剩余天数 label_today: 今天 -label_this_week: this week +label_this_week: 本周 label_less_than_ago: 之前天数少于 label_more_than_ago: 之前天数大于 label_ago: 之前天数 label_contains: 包含 label_not_contains: 不包含 label_day_plural: 天数 -label_repository: 版本库 +label_repository: 源代码库 label_browse: 浏览 label_modification: %d 个更新 label_modification_plural: %d 个更新 @@ -354,45 +354,45 @@ label_sort_higher: 上移 label_sort_lower: 下移 label_sort_lowest: 置底 label_roadmap: 路线图 -label_roadmap_due_in: Due in +label_roadmap_due_in: 截止日期到 label_roadmap_overdue: %s late -label_roadmap_no_issues: 该版本没有任务 +label_roadmap_no_issues: 该版本没有问题 label_search: 查找 label_result_plural: 个结果 label_all_words: 所有单词 label_wiki: Wiki -label_wiki_edit: Wiki edit -label_wiki_edit_plural: Wiki edits -label_wiki_page_plural: Wiki pages +label_wiki_edit: Wiki 编辑 +label_wiki_edit_plural: Wiki 编辑记录 +label_wiki_page_plural: Wiki 页面 label_index_by_title: 索引 -label_index_by_date: Index by date +label_index_by_date: 按照日期排序的索引 label_current_version: 当前版本 label_preview: 预览 label_feed_plural: Feeds label_changes_details: 所有更改的详情 -label_issue_tracking: 任务跟踪 +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 per month -label_commits_per_author: Commits per author -label_view_diff: View differences +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: Options -label_copy_workflow_from: Copy workflow from -label_permissions_report: Permissions report -label_watched_issues: Watched issues -label_related_issues: Related issues -label_applied_status: Applied status -label_loading: Loading... -label_relation_new: New relation -label_relation_delete: Delete relation -label_relates_to: related to -label_duplicates: duplicates +label_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: blocks label_blocked_by: blocked by label_precedes: precedes @@ -401,32 +401,32 @@ 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_stay_logged_in: 保持一直登录 +label_disabled: 禁用了 +label_show_completed_versions: 显示已经完成的版本 +label_me: 我 +label_board: 讨论区 +label_board_new: 新的版面 +label_board_plural: 讨论区 +label_topic_plural: 主题 +label_message_plural: 帖子 +label_message_last: 最新的帖子 +label_message_new: 新贴 +label_reply_plural: 回复 +label_send_information: 给用户发送帐号信息 +label_year: 年 +label_month: 月 +label_week: 周 +label_date_from: 从 +label_date_to: 到 +label_language_based: 语言 +label_sort_by: 根据 %s 排序 +label_send_test_email: 发送测试email +label_feeds_access_key_created_on: RSS 访问代码是在 %s 之前建立的 +label_module_plural: 模块 +label_added_time_by: 由 %s 在 %s 之前添加 +label_updated_time: 更新于 %s 前 +label_jump_to_a_project: 选择一个项目... button_login: 登录 button_submit: 提交 @@ -452,14 +452,14 @@ button_cancel: 取消 button_activate: 激活 button_sort: 排序 button_log_time: 登记工时 -button_rollback: Rollback to this version -button_watch: Watch -button_unwatch: Unwatch -button_reply: Reply -button_archive: Archive -button_unarchive: Unarchive -button_reset: Reset -button_rename: Rename +button_rollback: 恢复到这个版本 +button_watch: 跟踪 +button_unwatch: 取消跟踪 +button_reply: 回复 +button_archive: 存档 +button_unarchive: 取消存档 +button_reset: 重置 +button_rename: 重命名 status_active: 激活 status_registered: 已注册 @@ -469,7 +469,7 @@ text_select_mail_notifications: 选择需要发送邮件通知的动作。 text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 表示没有限制 text_project_destroy_confirmation: 您确信要删除这个项目以及所有相关的数据吗? -text_workflow_edit: 选择一个角色和跟踪标签来编辑这个工作流 +text_workflow_edit: 选择一个角色和跟踪标签来编辑这个工作流程 text_are_you_sure: 您确定? text_journal_changed: 从 %s 更改为 %s text_journal_set_to: 设置为 %s @@ -477,19 +477,19 @@ text_journal_deleted: 已删除 text_tip_task_begin_day: 开始于此 text_tip_task_end_day: 在此结束 text_tip_task_begin_end_day: 开始并结束于此 -text_project_identifier_info: 'Lower case letters (a-z), numbers and dashes allowed.
    Once saved, the identifier can not be changed.' -text_caracters_maximum: %d characters maximum. -text_length_between: Length between %d and %d characters. -text_tracker_no_workflow: No workflow defined for this tracker -text_unallowed_characters: Unallowed characters -text_comma_separated: Multiple values allowed (comma separated). -text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages -text_issue_added: %s ѱ -text_issue_updated: %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_project_identifier_info: '只允许使用小写字母 (a-z), 数字和-短横号.
    注意,保存以后, 标志名就不能修改了.' +text_caracters_maximum: 最多 %d 个字符. +text_length_between: 字符长度应该在 %d 和 %d 个之间. +text_tracker_no_workflow: 还没有定义工作流程 +text_unallowed_characters: 有不符合规定的字符 +text_comma_separated: 可以使用多个值 (用逗号,分开). +text_issues_ref_in_commit_messages: 信息里面直接可以使用的ref关联或者是fix修复标签设置 +text_issue_added: 问题 %s 成功提交 (by %s). +text_issue_updated: 问题 %s 成功更新 (by %s). +text_wiki_destroy_confirmation: 您是否确定要删除以下的 wiki 页面及其内容? +text_issue_category_destroy_question: 有一些问题 (%d 个) 属于这个分类. 您还想继续操作吗? +text_issue_category_destroy_assignments: 取消分类的设置 +text_issue_category_reassign_to: 重新添加到分类 default_role_manager: 管理员 default_role_developper: 开发人员 @@ -513,59 +513,59 @@ default_priority_immediate: 立刻 default_activity_design: 设计 default_activity_development: 开发 -enumeration_issue_priorities: 任务优先级 +enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 -enumeration_activities: Activities (time tracking) -label_wiki_page: Wiki page -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 +enumeration_activities: 活动情况 (时间跟踪) +label_wiki_page: Wiki 页面 +label_file_plural: 文件 +label_changeset_plural: 程序变动 +field_column_names: 列 +label_default_columns: 默认列 +setting_issue_list_default_columns: 问题列表中显示的默认列 +setting_repositories_encodings: 源代码库编码 +notice_no_issue_selected: "没有选择任何的问题! 请选择您要编辑的问题." +label_bulk_edit_selected_issues: 批量修改已选择的问题 +label_no_change_option: (不变) +notice_failed_to_save_issues: "在保存 %d 个问题时(总共 %d 个)失败: %s." +label_theme: 主题 +label_default: 默认 +label_search_titles_only: 仅在标题中搜索 label_nobody: nobody -button_change_password: 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 +button_change_password: 修改密码 +text_user_mail_option: "对于没有选择的项目, 您将只会收到您已经跟踪或者是参与的项目的通知 (比如说,您是问题的报告者, 或是负责解决这个问题的)." +label_user_mail_option_selected: "对于所选择的项目,收取任何相关的通知..." +label_user_mail_option_all: "对于我参与的项目,收取任何相关的通知" +label_user_mail_option_none: "只收取和我跟踪或者参与的项目的通知" +setting_emails_footer: Emails 末尾的信息 label_float: Float -button_copy: Copy -mail_body_account_information_external: You can use your "%s" account to log into Redmine. -mail_body_account_information: Your Redmine 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: Redmine 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 +button_copy: 复制 +mail_body_account_information_external: 您可以使用 "%s" 帐号登录了. +mail_body_account_information: 您的帐号信息 +setting_protocol: 协议(Protocol) +label_user_mail_no_self_notified: "对于我提交的修改,不要给我发送通知" +setting_time_format: 时间格式 +label_registration_activation_by_email: 通过email认证激活帐号 +mail_subject_account_activation_request: Redmine 帐号激活请求 +mail_body_account_activation_request: '新用户 (%s) 已经注册成功. 他的帐号需要您的认可后生效:' +label_registration_automatic_activation: 自动激活帐号 +label_registration_manual_activation: 手动激活帐号 +notice_account_pending: "您的帐号已经成功建立, 但是还需要等待管理员的认可后才能激活." +field_time_zone: 时区 +text_caracters_minimum: "至少需要 %d 个字符." +setting_bcc_recipients: 抄送地址 (bcc) +button_annotate: 注释 label_issues_by: Issues by %s field_searchable: Searchable -label_display_per_page: 'Per page: %s' -setting_per_page_options: Objects per page options +label_display_per_page: '每页显示: %s' +setting_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 +notice_default_data_loaded: 成功载入默认设置. +text_load_default_configuration: 载入默认设置 +text_no_configuration_data: "用户权限,跟踪标签, 问题状态列表和工作流程还没有设置好.\n强烈推荐载入默认的设置. 载入后您可以再进行定制." +error_can_t_load_default_data: "不能载入默认设置: %s" +button_update: 更新 +label_change_properties: 修改属性 +label_general: 一般 +label_repository_plural: 源代码库 +label_associated_revisions: 相关的版本 +setting_user_format: 用户显示格式 diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-lt.js b/public/javascripts/jstoolbar/lang/jstoolbar-lt.js index cd36a4b55..f0a7c5d90 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-lt.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-lt.js @@ -1,14 +1,14 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; +jsToolBar.strings['Strong'] = 'Pastorinti'; jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Code'] = 'Inline Code'; +jsToolBar.strings['Underline'] = 'Pabraukti'; +jsToolBar.strings['Deleted'] = 'Užbraukti'; +jsToolBar.strings['Code'] = 'Kodas'; jsToolBar.strings['Heading 1'] = 'Heading 1'; jsToolBar.strings['Heading 2'] = 'Heading 2'; jsToolBar.strings['Heading 3'] = 'Heading 3'; -jsToolBar.strings['Unordered list'] = '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['Unordered list'] = 'Nenumeruotas sąrašas'; +jsToolBar.strings['Ordered list'] = 'Numeruotas sąrašas'; +jsToolBar.strings['Preformatted text'] = 'Preformatuotas tekstas'; +jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį'; +jsToolBar.strings['Image'] = 'Paveikslas'; From fde2a497da752b4780a82a22b6becb20abb1953f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 5 Feb 2008 18:50:21 +0000 Subject: [PATCH 184/710] Activity test fix (r1120). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1123 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/functional/projects_controller_test.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 16eb2906e..71a3e3dfe 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -163,9 +163,10 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_tag :tag => "h3", :content => /#{2.days.ago.to_date.day}/, - :sibling => { :tag => "ul", - :child => { :tag => "li", - :child => { :tag => "p", + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => 'journal' }, + :child => { :tag => "a", :content => /(#{IssueStatus.find(2).name})/, } } @@ -178,9 +179,10 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_tag :tag => "h3", :content => /#{3.day.ago.to_date.day}/, - :sibling => { :tag => "ul", - :child => { :tag => "li", - :child => { :tag => "p", + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => 'issue' }, + :child => { :tag => "a", :content => /#{Issue.find(1).subject}/, } } From 68d3305ae22aaabc23200ede18809cb96d912a54 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 6 Feb 2008 18:56:52 +0000 Subject: [PATCH 185/710] Fixed settings.yml syntax (yaml parsing fails with JRuby 1.0.3: #606). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1124 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- config/settings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings.yml b/config/settings.yml index a45ae17ce..4f7be5132 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -86,12 +86,12 @@ cross_project_issue_relations: default: 0 notified_events: serialized: true - default: -- + default: - issue_added - issue_updated issue_list_default_columns: serialized: true - default: -- + default: - tracker - status - priority From 943ba3e34fed6d82f79a737e043c408746ab392a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 6 Feb 2008 19:05:42 +0000 Subject: [PATCH 186/710] Trac importer: handle nil usernames. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1125 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 2eca13dc3..5c006bc82 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -157,6 +157,8 @@ namespace :redmine do end def self.find_or_create_user(username, project_member = false) + return User.anonymous if username.blank? + u = User.find_by_login(username) if !u # Create a new user if not found From c80c1e1eada561326d36f2c0d115eb9ea76d16dd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 6 Feb 2008 20:02:30 +0000 Subject: [PATCH 187/710] Create a journal and send an email when an issue is closed by commit (#609). The redmine user is found using the committer username or email. Otherwise, the journal is created with anonymous user. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1126 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/changeset.rb | 16 +++++++++++++++- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/unit/repository_test.rb | 33 +++++++++++++++++++++++++++++---- 24 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 1b79104c4..3703ab927 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -48,6 +48,7 @@ class Changeset < ActiveRecord::Base def after_create scan_comment_for_issue_ids end + require 'pp' def scan_comment_for_issue_ids return if comments.blank? @@ -79,11 +80,14 @@ class Changeset < ActiveRecord::Base # update status of issues logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug? target_issues.each do |issue| - # don't change the status is the issue is already closed + # don't change the status is the issue is closed next if issue.status.is_closed? + user = committer_user || User.anonymous + journal = issue.init_journal(user, l(:text_status_changed_by_changeset, "r#{self.revision}")) issue.status = fix_status issue.done_ratio = done_ratio if done_ratio issue.save + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') end end referenced_issues += target_issues @@ -92,6 +96,16 @@ class Changeset < ActiveRecord::Base self.issues = referenced_issues.uniq end + # Returns the Redmine User corresponding to the committer + def committer_user + if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/ + username, email = $1.strip, $3 + u = User.find_by_login(username) + u ||= User.find_by_mail(email) unless email.blank? + u + end + end + # Returns the previous changeset def previous @previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC') diff --git a/lang/bg.yml b/lang/bg.yml index b783df592..6fb1cf6de 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -566,3 +566,4 @@ label_general: Основни label_repository_plural: Хранилища label_associated_revisions: Асоциирани ревизии setting_user_format: Потребителски формат +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/cs.yml b/lang/cs.yml index cec7d2a31..894568f20 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -566,3 +566,4 @@ 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. diff --git a/lang/de.yml b/lang/de.yml index a2222af83..668c4333c 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -566,3 +566,4 @@ default_activity_development: Entwicklung enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/en.yml b/lang/en.yml index 8c54e1c79..c056c6ce5 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -540,6 +540,7 @@ text_issue_category_reassign_to: Reassign issues to this category text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." text_load_default_configuration: Load the default configuration +text_status_changed_by_changeset: Applied in changeset %s. default_role_manager: Manager default_role_developper: Developer diff --git a/lang/es.yml b/lang/es.yml index f8cd9e300..b80d82178 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -569,3 +569,4 @@ 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. diff --git a/lang/fi.yml b/lang/fi.yml index cf04b23b4..3e297932d 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -570,3 +570,4 @@ enumeration_doc_categories: Dokumentin luokat enumeration_activities: Aktiviteetit (ajan seuranta) label_associated_revisions: Liittyvät versiot setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/fr.yml b/lang/fr.yml index 7df8ec622..ac615f03c 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -541,6 +541,7 @@ text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seulement des notifications pour ce que vous surveillez ou à quoi vous participez (exemple: demandes dont vous êtes l'auteur ou la personne assignée)." text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé." text_load_default_configuration: Charger le paramétrage par défaut +text_status_changed_by_changeset: Appliqué par commit %s. default_role_manager: Manager default_role_developper: Développeur diff --git a/lang/he.yml b/lang/he.yml index 7f26341b8..4063d2fcd 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -566,3 +566,4 @@ 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. diff --git a/lang/it.yml b/lang/it.yml index 7b27a54cb..216a1ada4 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -566,3 +566,4 @@ 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. diff --git a/lang/ja.yml b/lang/ja.yml index 0b3926a23..7607b21de 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -567,3 +567,4 @@ 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. diff --git a/lang/ko.yml b/lang/ko.yml index b9b9542fc..31ac5bf9f 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -566,3 +566,4 @@ label_general: 일반 label_repository_plural: 저장소들 label_associated_revisions: Associated revisions setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/lt.yml b/lang/lt.yml index 6f2965353..3aa86702a 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -567,3 +567,4 @@ label_repository_plural: Saugiklos error_can_t_load_default_data: "Numatytoji konfiguracija negali būti užkrauta: %s" label_associated_revisions: susijusios revizijos setting_user_format: Vartotojo atvaizdavimo formatas +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/nl.yml b/lang/nl.yml index c2d692706..c54273873 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -567,3 +567,4 @@ 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. diff --git a/lang/pl.yml b/lang/pl.yml index 0f99628af..e3fc957b4 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -566,3 +566,4 @@ label_general: Ogólne label_repository_plural: Repozytoria label_associated_revisions: Associated revisions setting_user_format: Users display format +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 3c6fd1265..ce68c5bac 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -566,3 +566,4 @@ 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. diff --git a/lang/pt.yml b/lang/pt.yml index 3b5647d53..246705599 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -566,3 +566,4 @@ 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. diff --git a/lang/ro.yml b/lang/ro.yml index 2fed755ac..97bb794dd 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -566,3 +566,4 @@ 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. diff --git a/lang/ru.yml b/lang/ru.yml index 44b1664cf..1358b648d 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -567,3 +567,4 @@ default_activity_development: Разработка enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/sr.yml b/lang/sr.yml index 3132beeec..e6a268004 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -567,3 +567,4 @@ 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. diff --git a/lang/sv.yml b/lang/sv.yml index b8a9a3a11..da03bf0ba 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -567,3 +567,4 @@ 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. diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 94631b86d..833e98b00 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -566,3 +566,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/lang/zh.yml b/lang/zh.yml index 70ee8b137..cdaeeaa04 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -569,3 +569,4 @@ label_general: 一般 label_repository_plural: 源代码库 label_associated_revisions: 相关的版本 setting_user_format: 用户显示格式 +text_status_changed_by_changeset: Applied in changeset %s. diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb index 5e0432c60..21fb00b80 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -18,7 +18,16 @@ require File.dirname(__FILE__) + '/../test_helper' class RepositoryTest < Test::Unit::TestCase - fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes + fixtures :projects, + :trackers, + :projects_trackers, + :repositories, + :issues, + :issue_statuses, + :changesets, + :changes, + :users, + :enumerations def setup @repository = Project.find(1).repository @@ -42,19 +51,35 @@ class RepositoryTest < Test::Unit::TestCase Setting.commit_fix_done_ratio = "90" Setting.commit_ref_keywords = 'refs , references, IssueID' Setting.commit_fix_keywords = 'fixes , closes' - + Setting.default_language = 'en' + ActionMailer::Base.deliveries.clear + # make sure issue 1 is not already closed - assert !Issue.find(1).status.is_closed? + fixed_issue = Issue.find(1) + assert !fixed_issue.status.is_closed? + old_status = fixed_issue.status Repository.scan_changesets_for_issue_ids assert_equal [101, 102], Issue.find(3).changeset_ids # fixed issues - fixed_issue = Issue.find(1) + fixed_issue.reload assert fixed_issue.status.is_closed? assert_equal 90, fixed_issue.done_ratio assert_equal [101], fixed_issue.changeset_ids + # issue change + journal = fixed_issue.journals.find(:first, :order => 'created_on desc') + assert_equal User.find_by_login('dlopper'), journal.user + assert_equal 'Applied in changeset r2.', journal.notes + + # 2 email notifications + assert_equal 2, ActionMailer::Base.deliveries.size + mail = ActionMailer::Base.deliveries.first + assert_kind_of TMail::Mail, mail + assert mail.subject.starts_with?("[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]") + assert mail.body.include?("Status changed from #{old_status} to #{fixed_issue.status}") + # ignoring commits referencing an issue of another project assert_equal [], Issue.find(4).changesets end From 9a2ec76a81d2c97c0ccc33452c2ef4557016a0a5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 6 Feb 2008 20:43:31 +0000 Subject: [PATCH 188/710] Mantis importer: few fixes in user mapping. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1127 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_mantis.rake | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/tasks/migrate_from_mantis.rake b/lib/tasks/migrate_from_mantis.rake index 36e7e1514..af8892a6b 100644 --- a/lib/tasks/migrate_from_mantis.rake +++ b/lib/tasks/migrate_from_mantis.rake @@ -87,18 +87,24 @@ task :migrate_from_mantis => :environment do set_table_name :mantis_user_table def firstname - realname.blank? ? username : realname.split.first[0..29] + @firstname = realname.blank? ? username : realname.split.first[0..29] + @firstname.gsub!(/[^\w\s\'\-]/i, '') + @firstname end def lastname - realname.blank? ? username : realname.split[1..-1].join(' ')[0..29] + @lastname = realname.blank? ? username : realname.split[1..-1].join(' ')[0..29] + @lastname.gsub!(/[^\w\s\'\-]/i, '') + @lastname = '-' if @lastname.blank? + @lastname end def email - if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) - read_attribute(:email) + if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) && + !User.find_by_mail(read_attribute(:email)) + @email = read_attribute(:email) else - "#{username}@foo.bar" + @email = "#{username}@foo.bar" end end @@ -246,7 +252,7 @@ task :migrate_from_mantis => :environment do u.password = 'mantis' u.status = User::STATUS_LOCKED if user.enabled != 1 u.admin = true if user.access_level == 90 - next unless u.save + next unless u.save! users_migrated += 1 users_map[user.id] = u.id print '.' From 6d109258c909f71edc3a4b43843c296acf66aad0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 7 Feb 2008 18:06:35 +0000 Subject: [PATCH 189/710] Added a few extensions/mimetypes. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1128 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/mime_type.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/redmine/mime_type.rb b/lib/redmine/mime_type.rb index c9db93956..e0bc3991d 100644 --- a/lib/redmine/mime_type.rb +++ b/lib/redmine/mime_type.rb @@ -19,7 +19,7 @@ module Redmine module MimeType MIME_TYPES = { - 'text/plain' => 'txt,tpl,properties', + 'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade', 'text/css' => 'css', 'text/html' => 'html,htm,xhtml', 'text/x-c' => 'c,cpp,h', @@ -30,25 +30,28 @@ module Redmine 'text/x-php' => 'php,php3,php4,php5', 'text/x-python' => 'py', 'text/x-ruby' => 'rb,rbw,ruby,rake', + 'text/x-csh' => 'csh', 'text/x-sh' => 'sh', - 'text/xml' => 'xml', + 'text/xml' => 'xml,xsd,mxml', 'text/yaml' => 'yml,yaml', 'image/gif' => 'gif', 'image/jpeg' => 'jpg,jpeg,jpe', 'image/png' => 'png', - 'image/tiff' => 'tiff,tif' + 'image/tiff' => 'tiff,tif', + 'image/x-ms-bmp' => 'bmp', + 'image/x-xpixmap' => 'xpm', }.freeze EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)| - exts.split(',').each {|ext| map[ext] = type} + exts.split(',').each {|ext| map[ext.strip] = type} map end # returns mime type for name or nil if unknown def self.of(name) return nil unless name - m = name.to_s.match(/\.([^\.]+)$/) - EXTENSIONS[m[1]] if m + m = name.to_s.match(/(^|\.)([^\.]+)$/) + EXTENSIONS[m[2].downcase] if m end def self.main_mimetype_of(name) From 43a6f312edde2399c9c986ed61b1e9b0e1066db6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 9 Feb 2008 16:11:18 +0000 Subject: [PATCH 190/710] Merged IssuesController #edit and #update into a single actions. Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status (#519, #581, #587). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1129 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 71 +++++++++------------- app/views/issues/_edit.rhtml | 39 ++++++++++++ app/views/issues/_form.rhtml | 8 +-- app/views/issues/_form_update.rhtml | 10 +++ app/views/issues/_update.rhtml | 54 ---------------- app/views/issues/context_menu.rhtml | 6 +- app/views/issues/edit.rhtml | 18 +----- app/views/issues/new.rhtml | 3 + app/views/issues/show.rhtml | 7 +-- app/views/issues/update.rhtml | 4 -- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + lib/redmine.rb | 4 +- public/images/note.png | Bin 469 -> 0 bytes public/stylesheets/application.css | 1 - test/functional/issues_controller_test.rb | 27 +++----- test/integration/issues_test.rb | 2 +- 37 files changed, 126 insertions(+), 150 deletions(-) create mode 100644 app/views/issues/_edit.rhtml create mode 100644 app/views/issues/_form_update.rhtml delete mode 100644 app/views/issues/_update.rhtml delete mode 100644 app/views/issues/update.rhtml delete mode 100644 public/images/note.png diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index b722d9340..dbb49405c 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -25,7 +25,7 @@ class IssuesController < ApplicationController before_filter :find_optional_project, :only => [:index, :changes] accept_key_auth :index, :changes - cache_sweeper :issue_sweeper, :only => [ :new, :edit, :update, :destroy ] + cache_sweeper :issue_sweeper, :only => [ :new, :edit, :destroy ] helper :journals helper :projects @@ -85,10 +85,12 @@ class IssuesController < ApplicationController end def show - @custom_values = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") + @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") - @status_options = @issue.new_statuses_allowed_to(User.current) + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + @edit_allowed = User.current.allowed_to?(:edit_issues, @project) @activities = Enumeration::get_values('ACTI') + @priorities = Enumeration::get_values('IPRI') respond_to do |format| format.html { render :template => 'issues/show.rhtml' } format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } @@ -140,48 +142,33 @@ class IssuesController < ApplicationController render :layout => !request.xhr? end - def edit - @priorities = Enumeration::get_values('IPRI') - @custom_values = [] - if request.get? - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } - else - begin - journal = @issue.init_journal(User.current) - # Retrieve custom fields and values - if 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 - @issue.attributes = params[:issue] - if @issue.save - flash[:notice] = l(:notice_successful_update) - Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') - redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) - end - rescue ActiveRecord::StaleObjectError - # Optimistic locking exception - flash[:error] = l(:notice_locking_conflict) - end - end - end - - # Attributes that can be updated on workflow transition + # Attributes that can be updated on workflow transition (without :edit permission) # TODO: make it configurable (at least per role) UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION) - def update - @status_options = @issue.new_statuses_allowed_to(User.current) + def edit + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) @activities = Enumeration::get_values('ACTI') - journal = @issue.init_journal(User.current, params[:notes]) - # User can change issue attributes only if a workflow transition is allowed - if !@status_options.empty? && params[:issue] - attrs = params[:issue].dup - attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } - attrs.delete(:status_id) unless @status_options.detect {|s| s.id.to_s == attrs[:status_id].to_s} - @issue.attributes = attrs - end - if request.post? + @priorities = Enumeration::get_values('IPRI') + @custom_values = [] + @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + 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 + @notes = params[:notes] + journal = @issue.init_journal(User.current, @notes) + # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed + if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] + attrs = params[:issue].dup + attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed + attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} + @issue.attributes = attrs + end + # Update custom fields if user has :edit permission + if @edit_allowed && params[:custom_fields] + @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } + @issue.custom_values = @custom_values + end attachments = attach_files(@issue, params[:attachments]) attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} if @issue.save @@ -243,7 +230,7 @@ class IssuesController < ApplicationController def preview issue = Issue.find_by_id(params[:id]) @attachements = issue.attachments if issue - @text = (params[:issue] ? params[:issue][:description] : nil) || params[:notes] + @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil) render :partial => 'common/preview' end diff --git a/app/views/issues/_edit.rhtml b/app/views/issues/_edit.rhtml new file mode 100644 index 000000000..0f843e855 --- /dev/null +++ b/app/views/issues/_edit.rhtml @@ -0,0 +1,39 @@ +<% labelled_tabular_form_for :issue, @issue, + :url => {:action => 'edit', :id => @issue}, + :html => {:id => 'issue-form', + :multipart => true} do |f| %> + <%= error_messages_for 'issue' %> +
    + <% if @edit_allowed || !@allowed_statuses.empty? %> +
    + <%= l(:label_change_properties) %> + <% if !@issue.new_record? && !@issue.errors.any? && @edit_allowed %> + (<%= link_to l(:label_more), {}, :onclick => 'Effect.toggle("issue_descr_fields", "appear", {duration:0.3}); return false;' %>) + <% end %> + + <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %> +
    + <% end %> + +
    <%= l(:field_notes) %> + <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> + <%= wikitoolbar_for 'notes' %> + +

    + <%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

    +
    +
    + + <%= f.hidden_field :lock_version %> + <%= submit_tag l(:button_submit) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, + :method => 'post', + :update => 'preview', + :with => 'Form.serialize("issue-form")', + :complete => "location.hash='preview'" + }, :accesskey => accesskey(:preview) %> +<% end %> + +
    diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index d11cea84c..6a4cd0f5f 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -1,6 +1,3 @@ -<%= error_messages_for 'issue' %> -
    - <% if @issue.new_record? %>

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

    <%= observe_field :issue_tracker_id, :url => { :action => :new }, @@ -8,15 +5,17 @@ :with => "Form.serialize('issue-form')" %> <% end %> +
    >

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

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

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

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

    <% else %>

    <%= @issue.status.name %>

    @@ -49,7 +48,6 @@ <%= image_to_function "add.png", "addFileField();return false" %> <%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

    <% end %> -
    <%= wikitoolbar_for 'issue_description' %> diff --git a/app/views/issues/_form_update.rhtml b/app/views/issues/_form_update.rhtml new file mode 100644 index 000000000..25e81a7fd --- /dev/null +++ b/app/views/issues/_form_update.rhtml @@ -0,0 +1,10 @@ +
    +

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

    +

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

    +
    +
    +

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

    +<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %> +
    diff --git a/app/views/issues/_update.rhtml b/app/views/issues/_update.rhtml deleted file mode 100644 index 49d1473d9..000000000 --- a/app/views/issues/_update.rhtml +++ /dev/null @@ -1,54 +0,0 @@ -<% labelled_tabular_form_for(:issue, @issue, :url => {:action => 'update', :id => @issue}, - :html => {:multipart => true, - :id => 'issue-form'}) do |f| %> - -
    -<% unless @status_options.empty? %> -<%= f.hidden_field :lock_version %> -
    <%= l(:label_change_properties) %> -
    -

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

    -

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

    -
    -
    -

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

    -

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

    -
    -
    -<% end%> -<% if authorize_for('timelog', 'edit') %> -
    <%= l(:button_log_time) %> - <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> -
    -

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

    -
    -
    -

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

    -

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

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

    -<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

    -
    -
    - -<%= submit_tag l(:button_submit) %> -<%= link_to_remote l(:label_preview), - { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, - :method => 'post', - :update => 'preview', - :with => "Form.serialize('issue-form')", - :complete => "window.location.hash='preview'" - }, :accesskey => accesskey(:preview) %> | -<%= toggle_link l(:button_cancel), 'update' %> -<% end %> - -
    diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index 9691a7713..46b177067 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -6,7 +6,7 @@ <%= l(:field_status) %>
      <% @statuses.each do |s| %> -
    • <%= context_menu_link s.name, {:controller => 'issues', :action => 'update', :id => @issue, :issue => {:status_id => s}}, +
    • <%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, :selected => (s == @issue.status), :disabled => !(@allowed_statuses.include?(s)) %>
    • <% end %>
    @@ -24,10 +24,10 @@ <%= l(:field_assigned_to) %>
      <% @assignables.each do |u| %> -
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'update', :id => @issue, :issue => {:assigned_to_id => u}, :back_to => back_to}, :method => :post, +
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => u}, :back_to => back_to}, :method => :post, :selected => (u == @issue.assigned_to), :disabled => !@can[:assign] %>
    • <% end %> -
    • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'update', :id => @issue, :issue => {:assigned_to_id => nil}, :back_to => back_to}, :method => :post, +
    • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => nil}, :back_to => back_to}, :method => :post, :selected => @issue.assigned_to.nil?, :disabled => !@can[:assign] %>
    diff --git a/app/views/issues/edit.rhtml b/app/views/issues/edit.rhtml index 1577216ed..97f26a205 100644 --- a/app/views/issues/edit.rhtml +++ b/app/views/issues/edit.rhtml @@ -1,19 +1,3 @@

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

    -<% labelled_tabular_form_for :issue, @issue, - :url => {:action => 'edit'}, - :html => {:id => 'issue-form'} do |f| %> - <%= render :partial => 'form', :locals => {:f => f} %> - <%= f.hidden_field :lock_version %> - <%= submit_tag l(:button_save) %> - <%= link_to_remote l(:label_preview), - { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, - :method => 'post', - :update => 'preview', - :with => "Form.serialize('issue-form')", - :complete => "location.href='#preview-top'" - }, :accesskey => accesskey(:preview) %> -<% end %> - - -
    +<%= render :partial => 'edit' %> diff --git a/app/views/issues/new.rhtml b/app/views/issues/new.rhtml index 8ff07f226..1e9e323fe 100644 --- a/app/views/issues/new.rhtml +++ b/app/views/issues/new.rhtml @@ -2,7 +2,10 @@ <% labelled_tabular_form_for :issue, @issue, :html => {:multipart => true, :id => 'issue-form'} do |f| %> + <%= error_messages_for 'issue' %> +
    <%= render :partial => 'issues/form', :locals => {:f => f} %> +
    <%= submit_tag l(:button_create) %> <%= link_to_remote l(:label_preview), { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index d29b1b88f..a16dc60e0 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -1,6 +1,5 @@
    -<%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-note') if authorize_for('issues', 'update') %> -<%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %> +<%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> <%= watcher_tag(@issue, User.current) %> <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> @@ -89,11 +88,11 @@ end %>
    <% end %> -<% if authorize_for('issues', 'update') %> +<% if authorize_for('issues', 'edit') %> <% end %> diff --git a/app/views/issues/update.rhtml b/app/views/issues/update.rhtml deleted file mode 100644 index 44e72da87..000000000 --- a/app/views/issues/update.rhtml +++ /dev/null @@ -1,4 +0,0 @@ -

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

    - -<%= error_messages_for 'issue' %> -<%= render :partial => 'update' %> diff --git a/lang/bg.yml b/lang/bg.yml index 6fb1cf6de..bc0e070c0 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -567,3 +567,4 @@ label_repository_plural: Хранилища label_associated_revisions: Асоциирани ревизии setting_user_format: Потребителски формат text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/cs.yml b/lang/cs.yml index 894568f20..35bda2b6a 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -567,3 +567,4 @@ 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 diff --git a/lang/de.yml b/lang/de.yml index 668c4333c..fd296b600 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -567,3 +567,4 @@ enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/en.yml b/lang/en.yml index c056c6ce5..b86eeef2b 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -469,6 +469,7 @@ label_display_per_page: 'Per page: %s' label_age: Age label_change_properties: Change properties label_general: General +label_more: More button_login: Login button_submit: Submit diff --git a/lang/es.yml b/lang/es.yml index b80d82178..60e4a4d13 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -570,3 +570,4 @@ 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 diff --git a/lang/fi.yml b/lang/fi.yml index 3e297932d..38cf19a46 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -571,3 +571,4 @@ enumeration_activities: Aktiviteetit (ajan seuranta) label_associated_revisions: Liittyvät versiot setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/fr.yml b/lang/fr.yml index ac615f03c..e428935af 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -470,6 +470,7 @@ label_display_per_page: 'Par page: %s' label_age: Age label_change_properties: Changer les propriétés label_general: Général +label_more: Plus button_login: Connexion button_submit: Soumettre diff --git a/lang/he.yml b/lang/he.yml index 4063d2fcd..ca577558a 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -567,3 +567,4 @@ 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 diff --git a/lang/it.yml b/lang/it.yml index 216a1ada4..f227c6942 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -567,3 +567,4 @@ 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 diff --git a/lang/ja.yml b/lang/ja.yml index 7607b21de..92b79b6ef 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -568,3 +568,4 @@ 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 diff --git a/lang/ko.yml b/lang/ko.yml index 31ac5bf9f..097d74788 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -567,3 +567,4 @@ label_repository_plural: 저장소들 label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/lt.yml b/lang/lt.yml index 3aa86702a..3106bd764 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -568,3 +568,4 @@ error_can_t_load_default_data: "Numatytoji konfiguracija negali būti užkrauta: label_associated_revisions: susijusios revizijos setting_user_format: Vartotojo atvaizdavimo formatas text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/nl.yml b/lang/nl.yml index c54273873..1c76180be 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -568,3 +568,4 @@ 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 diff --git a/lang/pl.yml b/lang/pl.yml index e3fc957b4..3ebce3e92 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -567,3 +567,4 @@ label_repository_plural: Repozytoria label_associated_revisions: Associated revisions setting_user_format: Users display format text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/pt-br.yml b/lang/pt-br.yml index ce68c5bac..688ce0cfd 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -567,3 +567,4 @@ 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 diff --git a/lang/pt.yml b/lang/pt.yml index 246705599..4734c66fc 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -567,3 +567,4 @@ 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 diff --git a/lang/ro.yml b/lang/ro.yml index 97bb794dd..be566b959 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -567,3 +567,4 @@ 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 diff --git a/lang/ru.yml b/lang/ru.yml index 1358b648d..6921169ce 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -568,3 +568,4 @@ enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/sr.yml b/lang/sr.yml index e6a268004..ddf84a5d3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -568,3 +568,4 @@ 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 diff --git a/lang/sv.yml b/lang/sv.yml index da03bf0ba..4bec394ce 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -568,3 +568,4 @@ 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 833e98b00..50a6384f2 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -567,3 +567,4 @@ enumeration_issue_priorities: 項目重要性 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lang/zh.yml b/lang/zh.yml index cdaeeaa04..dc431f5c0 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -570,3 +570,4 @@ label_repository_plural: 源代码库 label_associated_revisions: 相关的版本 setting_user_format: 用户显示格式 text_status_changed_by_changeset: Applied in changeset %s. +label_more: More diff --git a/lib/redmine.rb b/lib/redmine.rb index c4c55a932..69151012b 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -32,9 +32,9 @@ Redmine::AccessControl.map do |map| :reports => :issue_report}, :public => true map.permission :add_issues, {:issues => :new} map.permission :edit_issues, {:projects => :bulk_edit_issues, - :issues => [:edit, :update, :destroy_attachment]} + :issues => [:edit, :destroy_attachment]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} - map.permission :add_issue_notes, {:issues => :update} + map.permission :add_issue_notes, {:issues => :edit} map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin map.permission :delete_issues, {:issues => :destroy}, :require => :member # Queries diff --git a/public/images/note.png b/public/images/note.png deleted file mode 100644 index 256368397286776fa63c18805623ef4efdb750b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmV;`0V@89P)WdL?%ZZ04)F(7kgaC9ypGB7YVATlsIH8eUjIUp-AF)%O-KQw*-000Mc zNliru*9jF90SKU&=dl0)010qNS#tmY3labT3lag+-G2N4000DMK}|sb0I`n?{9y$E z005;>OjJex|Nrmr@8{>|z23b_dqQ)yYpuPl*4Ebd_xHW`t^fbd#^;=Ky<^74#?H>p zrSF{o|L26~Yf`;R-rnA|=e6(m=kNd4Ip;YD&Inq@QiR@wt^c)z_ly|lAtC1}QpQq@ z=ajww#+3h@2-XNn#zN=!=alD^od2bJrCP@S#%tbV-Z5H?drIf;=l9mu|0!CvLNWLM z_pQd1_r}imrL~l$Yu@+XY&1RZ0001ZNklsRFYFxLGY!dW#lB}Igt6X5|SJo9E==bK@m|gaS2H#AYfz! z3-AkY2@3JC3kx$rHF0ur^YHSqGs4VZW?^MxXF{0BzzEdKKnef=KAs3D*$m`T00000 LNkvXXu0mjfayQNg diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index b3b8b341d..b386c3285 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -512,7 +512,6 @@ vertical-align: middle; .icon-reload { background-image: url(../images/reload.png); } .icon-lock { background-image: url(../images/locked.png); } .icon-unlock { background-image: url(../images/unlock.png); } -.icon-note { background-image: url(../images/note.png); } .icon-checked { background-image: url(../images/true.png); } .icon22-projects { background-image: url(../images/22x22/projects.png); } diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index f4c99f1ed..6fdbb0341 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -217,18 +217,11 @@ class IssuesControllerTest < Test::Unit::TestCase assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}") end - def test_get_update - @request.session[:user_id] = 2 - get :update, :id => 1 - assert_response :success - assert_template 'update' - end - - def test_update_with_status_and_assignee_change + 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 :update, + post :edit, :id => 1, :issue => { :status_id => 2, :assigned_to_id => 3 }, :notes => 'Assigned to dlopper' @@ -243,10 +236,10 @@ class IssuesControllerTest < Test::Unit::TestCase assert mail.body.include?("Status changed from New to Assigned") end - def test_update_with_note_only + def test_post_edit_with_note_only notes = 'Note added by IssuesControllerTest#test_update_with_note_only' # anonymous user - post :update, + post :edit, :id => 1, :notes => notes assert_redirected_to 'issues/show/1' @@ -259,10 +252,10 @@ class IssuesControllerTest < Test::Unit::TestCase assert mail.body.include?(notes) end - def test_update_with_note_and_spent_time + def test_post_edit_with_note_and_spent_time @request.session[:user_id] = 2 spent_hours_before = Issue.find(1).spent_hours - post :update, + post :edit, :id => 1, :notes => '2.5 hours added', :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } @@ -280,9 +273,9 @@ class IssuesControllerTest < Test::Unit::TestCase assert_equal spent_hours_before + 2.5, issue.spent_hours end - def test_update_with_attachment_only + def test_post_edit_with_attachment_only # anonymous user - post :update, + post :edit, :id => 1, :notes => '', :attachments => [ test_uploaded_file('testfile.txt', 'text/plain') ] @@ -297,12 +290,12 @@ class IssuesControllerTest < Test::Unit::TestCase assert mail.body.include?('testfile.txt') end - def test_update_with_no_change + def test_post_edit_with_no_change issue = Issue.find(1) issue.journals.clear ActionMailer::Base.deliveries.clear - post :update, + post :edit, :id => 1, :notes => '' assert_redirected_to 'issues/show/1' diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index 7249ed3da..eda4ef676 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -48,7 +48,7 @@ class IssuesTest < ActionController::IntegrationTest def test_issue_attachements log_user('jsmith', 'jsmith') - post 'issues/update/1', + post 'issues/edit/1', :notes => 'Some notes', :attachments => ([] << ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/testfile.txt', 'text/plain')) assert_redirected_to "issues/show/1" From 4155c97222cea69406060979efca3ebaaed9dcec Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 10 Feb 2008 13:17:41 +0000 Subject: [PATCH 191/710] Issue list now supports bulk edit/move/delete (#563, #607). For now, issues from different projects can not be bulk edited/moved/deleted at once. There are 2 ways to select a set of issues on the issue list: * by using checkbox and/or the little pencil that will select/unselect all issues (#567) * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues Context menu was disabled on links so that the default context menu of the browser is displayed when right-clicking on a link (#545). All this was tested with Firefox 2, IE 6/7, Opera 8 (use Alt+Click instead of Right-click) and Safari 2/3. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1130 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 142 ++++++++++--- app/controllers/projects_controller.rb | 79 +------- app/views/issues/_list.rhtml | 28 +-- app/views/issues/_list_simple.rhtml | 11 +- ...{_bulk_edit_form.rhtml => bulk_edit.rhtml} | 23 ++- app/views/issues/context_menu.rhtml | 39 ++-- app/views/issues/index.rhtml | 12 -- .../move_issues.rhtml => issues/move.rhtml} | 23 +-- app/views/issues/show.rhtml | 2 +- app/views/my/page.rhtml | 2 +- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + lib/redmine.rb | 5 +- public/javascripts/application.js | 13 +- public/javascripts/context_menu.js | 186 ++++++++++++++---- test/functional/issues_controller_test.rb | 121 +++++++++++- test/functional/projects_controller_test.rb | 26 --- 37 files changed, 486 insertions(+), 248 deletions(-) rename app/views/issues/{_bulk_edit_form.rhtml => bulk_edit.rhtml} (74%) rename app/views/{projects/move_issues.rhtml => issues/move.rhtml} (53%) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index dbb49405c..85151e905 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -19,13 +19,14 @@ class IssuesController < ApplicationController layout 'base' menu_item :new_issue, :only => :new - before_filter :find_issue, :except => [:index, :changes, :preview, :new, :update_form] + before_filter :find_issue, :only => [:show, :edit, :destroy_attachment] + before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] before_filter :find_project, :only => [:new, :update_form] - before_filter :authorize, :except => [:index, :changes, :preview, :update_form] + before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu] before_filter :find_optional_project, :only => [:index, :changes] accept_key_auth :index, :changes - cache_sweeper :issue_sweeper, :only => [ :new, :edit, :destroy ] + cache_sweeper :issue_sweeper, :only => [ :new, :edit, :bulk_edit, :destroy ] helper :journals helper :projects @@ -152,18 +153,20 @@ class IssuesController < ApplicationController @priorities = Enumeration::get_values('IPRI') @custom_values = [] @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + + @notes = params[:notes] + journal = @issue.init_journal(User.current, @notes) + # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed + if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] + attrs = params[:issue].dup + attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed + attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} + @issue.attributes = attrs + end + if request.get? @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) } else - @notes = params[:notes] - journal = @issue.init_journal(User.current, @notes) - # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed - if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] - attrs = params[:issue].dup - attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed - attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} - @issue.attributes = attrs - end # 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]) } @@ -191,8 +194,78 @@ class IssuesController < ApplicationController flash.now[:error] = l(:notice_locking_conflict) end + # Bulk edit a set of issues + def bulk_edit + if request.post? + status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) + priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) + assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id]) + category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id]) + fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id]) + + unsaved_issue_ids = [] + @issues.each do |issue| + journal = issue.init_journal(User.current, params[:notes]) + issue.priority = priority if priority + issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' + issue.category = category if category + issue.fixed_version = fixed_version if fixed_version + issue.start_date = params[:start_date] unless params[:start_date].blank? + issue.due_date = params[:due_date] unless params[:due_date].blank? + issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? + # Don't save any change to the issue if the user is not authorized to apply the requested status + if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save + # Send notification for each issue (if changed) + Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') + else + # Keep unsaved issue ids to display them in flash error + unsaved_issue_ids << issue.id + end + end + if unsaved_issue_ids.empty? + flash[:notice] = l(:notice_successful_update) unless @issues.empty? + else + flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) + end + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return + end + # Find potential statuses the user could be allowed to switch issues to + @available_statuses = Workflow.find(:all, :include => :new_status, + :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq + end + + def move + @allowed_projects = [] + # find projects to which the user is allowed to move the issue + if User.current.admin? + # admin is allowed to move issues to any active (visible) project + @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') + else + User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)} + end + @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] + @target_project ||= @project + @trackers = @target_project.trackers + if request.post? + new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) + unsaved_issue_ids = [] + @issues.each do |issue| + unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) + end + if unsaved_issue_ids.empty? + flash[:notice] = l(:notice_successful_update) unless @issues.empty? + else + flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) + end + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + return + end + render :layout => false if request.xhr? + end + def destroy - @issue.destroy + @issues.each(&:destroy) redirect_to :action => 'index', :project_id => @project end @@ -208,17 +281,27 @@ class IssuesController < ApplicationController end def context_menu + @issues = Issue.find_all_by_id(params[:ids], :include => :project) + if (@issues.size == 1) + @issue = @issues.first + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + @assignables = @issue.assignable_users + @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + end + projects = @issues.collect(&:project).compact.uniq + @project = projects.first if projects.size == 1 + + @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), + :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))), + :move => (@project && User.current.allowed_to?(:move_issues, @project)), + :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) + } + @priorities = Enumeration.get_values('IPRI').reverse @statuses = IssueStatus.find(:all, :order => 'position') - @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) - @can = {:edit => User.current.allowed_to?(:edit_issues, @project), - :assign => (@allowed_statuses.any? || User.current.allowed_to?(:edit_issues, @project)), - :add => User.current.allowed_to?(:add_issues, @project), - :move => User.current.allowed_to?(:move_issues, @project), - :copy => (@project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), - :delete => User.current.allowed_to?(:delete_issues, @project)} + @back = request.env['HTTP_REFERER'] + render :layout => false end @@ -242,6 +325,21 @@ private render_404 end + # Filter for bulk operations + def find_issues + @issues = Issue.find_all_by_id(params[:id] || params[:ids]) + raise ActiveRecord::RecordNotFound if @issues.empty? + projects = @issues.collect(&:project).compact.uniq + if projects.size == 1 + @project = projects.first + else + # TODO: let users bulk edit/move/destroy issues from different projects + render_error 'Can not bulk edit/move/destroy issues from different projects' and return false + end + rescue ActiveRecord::RecordNotFound + render_404 + end + def find_project @project = Project.find(params[:project_id]) rescue ActiveRecord::RecordNotFound diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 9560a451f..cddfb6f81 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -22,7 +22,7 @@ class ProjectsController < ApplicationController menu_item :roadmap, :only => :roadmap menu_item :files, :only => [:list_files, :add_file] menu_item :settings, :only => :settings - menu_item :issues, :only => [:bulk_edit_issues, :changelog, :move_issues] + menu_item :issues, :only => [:changelog] before_filter :find_project, :except => [ :index, :list, :add ] before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ] @@ -182,83 +182,6 @@ class ProjectsController < ApplicationController end end - # Bulk edit issues - def bulk_edit_issues - if request.post? - status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) - priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) - assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id]) - category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id]) - fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id]) - issues = @project.issues.find_all_by_id(params[:issue_ids]) - unsaved_issue_ids = [] - issues.each do |issue| - journal = issue.init_journal(User.current, params[:notes]) - issue.priority = priority if priority - issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' - issue.category = category if category - issue.fixed_version = fixed_version if fixed_version - issue.start_date = params[:start_date] unless params[:start_date].blank? - issue.due_date = params[:due_date] unless params[:due_date].blank? - issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? - # Don't save any change to the issue if the user is not authorized to apply the requested status - if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save - # Send notification for each issue (if changed) - Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') - else - # Keep unsaved issue ids to display them in flash error - unsaved_issue_ids << issue.id - end - end - if unsaved_issue_ids.empty? - flash[:notice] = l(:notice_successful_update) unless issues.empty? - else - flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #')) - end - redirect_to :controller => 'issues', :action => 'index', :project_id => @project - return - end - # Find potential statuses the user could be allowed to switch issues to - @available_statuses = Workflow.find(:all, :include => :new_status, - :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq - render :update do |page| - page.hide 'query_form' - page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form' - end - end - - def move_issues - @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids] - redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues - - @projects = [] - # find projects to which the user is allowed to move the issue - if User.current.admin? - # admin is allowed to move issues to any active (visible) project - @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') - else - User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)} - end - @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] - @target_project ||= @project - @trackers = @target_project.trackers - if request.post? - new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) - unsaved_issue_ids = [] - @issues.each do |issue| - unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) - end - if unsaved_issue_ids.empty? - flash[:notice] = l(:notice_successful_update) unless @issues.empty? - else - flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) - end - redirect_to :controller => 'issues', :action => 'index', :project_id => @project - return - end - render :layout => false if request.xhr? - end - def add_file if request.post? @version = @project.versions.find_by_id(params[:version_id]) diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index ff91f34f7..1f5470200 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -1,10 +1,7 @@ -
    - +<% form_tag({}) do -%> +
    - <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> <% query.columns.each do |column| %> @@ -12,14 +9,21 @@ <% end %> - <% issues.each do |issue| %> + <% issues.each do |issue| -%> "> - + - <% query.columns.each do |column| %> - <%= content_tag 'td', column_content(column, issue), :class => column.name %> - <% end %> + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> - <% end %> + <% end -%>
    <%= link_to_remote(image_tag('edit.png'), - {:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project }, - :method => :get}, - {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %> + <%= link_to image_tag('edit.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;' %>
    <%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %><%= check_box_tag("ids[]", issue.id, false) %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
    +<% end -%> + +<% content_for :header_tags do -%> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end -%> + + +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml index eb93f8ea1..8900b7359 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -1,5 +1,6 @@ -<% if issues.length > 0 %> - +<% if issues && issues.any? %> +<% form_tag({}) do %> +
    @@ -9,6 +10,7 @@ <% for issue in issues %> ">
    # <%=l(:field_tracker)%>
    + <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> <%=h issue.project.name %> - <%= issue.tracker.name %>
    @@ -20,6 +22,7 @@ <% end %>
    +<% end %> <% else %> - <%=l(:label_no_data)%> -<% end %> \ No newline at end of file +

    <%= l(:label_no_data) %>

    +<% end %> diff --git a/app/views/issues/_bulk_edit_form.rhtml b/app/views/issues/bulk_edit.rhtml similarity index 74% rename from app/views/issues/_bulk_edit_form.rhtml rename to app/views/issues/bulk_edit.rhtml index e9e1cef86..d19262271 100644 --- a/app/views/issues/_bulk_edit_form.rhtml +++ b/app/views/issues/bulk_edit.rhtml @@ -1,6 +1,12 @@ -
    -
    <%= l(:label_bulk_edit_selected_issues) %> +

    <%= l(:label_bulk_edit_selected_issues) %>

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

    <% if @available_statuses.any? %>

    - -
    -<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %> -
    -

    <%= submit_tag l(:button_apply) %> -<%= link_to l(:button_cancel), {}, :onclick => 'Element.hide("bulk-edit-fields"); if ($("query_form")) {Element.show("query_form")}; return false;' %>

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

    <%= submit_tag l(:button_submit) %> +<% end %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index 46b177067..b3a03b05d 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -1,40 +1,45 @@ -<% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>

      +<% if !@issue.nil? -%>
    • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon-edit', :disabled => !@can[:edit] %>
    • <%= l(:field_status) %>
        - <% @statuses.each do |s| %> + <% @statuses.each do |s| -%>
      • <%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, - :selected => (s == @issue.status), :disabled => !(@allowed_statuses.include?(s)) %>
      • - <% end %> + :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %> + <% end -%>
    • <%= l(:field_priority) %>
        - <% @priorities.each do |p| %> -
      • <%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post, + <% @priorities.each do |p| -%> +
      • <%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post, :selected => (p == @issue.priority), :disabled => !@can[:edit] %>
      • - <% end %> + <% end -%>
    • <%= l(:field_assigned_to) %>
        - <% @assignables.each do |u| %> -
      • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => u}, :back_to => back_to}, :method => :post, - :selected => (u == @issue.assigned_to), :disabled => !@can[:assign] %>
      • - <% end %> -
      • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:assigned_to_id => nil}, :back_to => back_to}, :method => :post, - :selected => @issue.assigned_to.nil?, :disabled => !@can[:assign] %>
      • + <% @assignables.each do |u| -%> +
      • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post, + :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %>
      • + <% end -%> +
      • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post, + :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %>
    • <%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon-copy', :disabled => !@can[:copy] %>
    • -
    • <%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, - :class => 'icon-move', :disabled => !@can[:move] %> -
    • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, - :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %>
    • +<% else -%> +
    • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
    • +<% end -%> + +
    • <%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, + :class => 'icon-move', :disabled => !@can[:move] %>
    • +
    • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)}, + :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %>
    diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 48697c505..c5f26dfb6 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -31,7 +31,6 @@ <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> <% end %>
    -

    <%=h @query.name %>

    <% html_title @query.name %> @@ -41,7 +40,6 @@ <% if @issues.empty? %>

    <%= l(:label_no_data) %>

    <% else %> -<% form_tag({:controller => 'projects', :action => 'bulk_edit_issues', :id => @project}, :id => 'issues_form', :onsubmit => "if (!checkBulkEdit(this)) {alert('#{l(:notice_no_issue_selected)}'); return false;}" ) do %> <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
    <%= l(:label_export_to) %> @@ -51,7 +49,6 @@

    <%= pagination_links_full @issue_pages, @issue_count %>

    <% end %> <% end %> -<% end %> <% content_for :sidebar do %> <%= render :partial => 'issues/sidebar' %> @@ -60,13 +57,4 @@ <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> - <%= javascript_include_tag 'calendar/calendar' %> - <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> - <%= javascript_include_tag 'calendar/calendar-setup' %> - <%= stylesheet_link_tag 'calendar' %> - <%= javascript_include_tag 'context_menu' %> - <%= stylesheet_link_tag 'context_menu' %> <% end %> - - -<%= javascript_tag 'new ContextMenu({})' %> diff --git a/app/views/projects/move_issues.rhtml b/app/views/issues/move.rhtml similarity index 53% rename from app/views/projects/move_issues.rhtml rename to app/views/issues/move.rhtml index 95eaf9dec..c74270f1a 100644 --- a/app/views/projects/move_issues.rhtml +++ b/app/views/issues/move.rhtml @@ -1,23 +1,15 @@ -

    <%=l(:button_move)%>

    +

    <%= l(:button_move) %>

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

    -<% for issue in @issues %> - <%= link_to_issue issue %>: <%=h issue.subject %> - <%= hidden_field_tag "issue_ids[]", issue.id %>
    -<% end %> -(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)

    - -  - - +

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

    @@ -25,5 +17,6 @@

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

    + <%= submit_tag l(:button_move) %> <% end %> diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index a16dc60e0..3392aef83 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -3,7 +3,7 @@ <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> <%= watcher_tag(@issue, User.current) %> <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> -<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %> <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
    diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml index 26ee44fcc..4d4c921b6 100644 --- a/app/views/my/page.rhtml +++ b/app/views/my/page.rhtml @@ -37,6 +37,6 @@ <% end %> -<%= javascript_tag 'new ContextMenu({})' %> +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> <% html_title(l(:label_my_page)) -%> diff --git a/lang/bg.yml b/lang/bg.yml index bc0e070c0..164d67671 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -568,3 +568,4 @@ label_associated_revisions: Асоциирани ревизии setting_user_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) ?' diff --git a/lang/cs.yml b/lang/cs.yml index 35bda2b6a..86a111cc7 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/de.yml b/lang/de.yml index fd296b600..c5a6cdf95 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -568,3 +568,4 @@ enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) 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) ?' diff --git a/lang/en.yml b/lang/en.yml index b86eeef2b..a37be93c7 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -542,6 +542,7 @@ text_user_mail_option: "For unselected projects, you will only receive notificat text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." text_load_default_configuration: Load the default configuration text_status_changed_by_changeset: Applied in changeset %s. +text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' default_role_manager: Manager default_role_developper: Developer diff --git a/lang/es.yml b/lang/es.yml index 60e4a4d13..1bc7d00fb 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -571,3 +571,4 @@ 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) ?' diff --git a/lang/fi.yml b/lang/fi.yml index 38cf19a46..7a8848302 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -572,3 +572,4 @@ label_associated_revisions: Liittyvät versiot 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) ?' diff --git a/lang/fr.yml b/lang/fr.yml index e428935af..8c569fc5a 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -543,6 +543,7 @@ text_user_mail_option: "Pour les projets non sélectionnés, vous recevrez seule text_no_configuration_data: "Les rôles, trackers, statuts et le workflow ne sont pas encore paramétrés.\nIl est vivement recommandé de charger le paramétrage par defaut. Vous pourrez le modifier une fois chargé." text_load_default_configuration: Charger le paramétrage par défaut text_status_changed_by_changeset: Appliqué par commit %s. +text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?' default_role_manager: Manager default_role_developper: Développeur diff --git a/lang/he.yml b/lang/he.yml index ca577558a..6631a457f 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/it.yml b/lang/it.yml index f227c6942..3c77c97a6 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/ja.yml b/lang/ja.yml index 92b79b6ef..50d23e80f 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -569,3 +569,4 @@ 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) ?' diff --git a/lang/ko.yml b/lang/ko.yml index 097d74788..62ec95ef3 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/lt.yml b/lang/lt.yml index 3106bd764..a7b12736a 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -569,3 +569,4 @@ label_associated_revisions: susijusios revizijos setting_user_format: Vartotojo atvaizdavimo formatas 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) ?' diff --git a/lang/nl.yml b/lang/nl.yml index 1c76180be..7b5f6bf3a 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -569,3 +569,4 @@ 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) ?' diff --git a/lang/pl.yml b/lang/pl.yml index 3ebce3e92..5c12c658f 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 688ce0cfd..4616e83a7 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/pt.yml b/lang/pt.yml index 4734c66fc..31df179f2 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/ro.yml b/lang/ro.yml index be566b959..52e856683 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -568,3 +568,4 @@ 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) ?' diff --git a/lang/ru.yml b/lang/ru.yml index 6921169ce..c720dfb72 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -569,3 +569,4 @@ enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) 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) ?' diff --git a/lang/sr.yml b/lang/sr.yml index ddf84a5d3..c32394dc3 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -569,3 +569,4 @@ 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) ?' diff --git a/lang/sv.yml b/lang/sv.yml index 4bec394ce..40a1ce8f6 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -569,3 +569,4 @@ 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) ?' diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 50a6384f2..601f5c2ec 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -568,3 +568,4 @@ enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (time tracking) 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) ?' diff --git a/lang/zh.yml b/lang/zh.yml index dc431f5c0..fd513a96a 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -571,3 +571,4 @@ label_associated_revisions: 相关的版本 setting_user_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) ?' diff --git a/lib/redmine.rb b/lib/redmine.rb index 69151012b..9bec55409 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -31,11 +31,10 @@ Redmine::AccessControl.map do |map| :queries => :index, :reports => :issue_report}, :public => true map.permission :add_issues, {:issues => :new} - map.permission :edit_issues, {:projects => :bulk_edit_issues, - :issues => [:edit, :destroy_attachment]} + map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]} map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} map.permission :add_issue_notes, {:issues => :edit} - map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin + map.permission :move_issues, {:issues => :move}, :require => :loggedin map.permission :delete_issues, {:issues => :destroy}, :require => :member # Queries map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 5ad04e91d..d77362a06 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -1,3 +1,6 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + function checkAll (id, checked) { var el = document.getElementById(id); for (var i = 0; i < el.elements.length; i++) { @@ -49,16 +52,6 @@ function promptToRemote(text, param, url) { } } -/* checks that at least one checkbox is checked (used when submitting bulk edit form) */ -function checkBulkEdit(form) { - for (var i = 0; i < form.elements.length; i++) { - if (form.elements[i].checked) { - return true; - } - } - return false; -} - function collapseScmEntry(id) { var els = document.getElementsByClassName(id, 'browser'); for (var i = 0; i < els.length; i++) { diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js index 11754cde8..e3f128d89 100644 --- a/public/javascripts/context_menu.js +++ b/public/javascripts/context_menu.js @@ -1,47 +1,161 @@ +/* redMine - project management software + Copyright (C) 2006-2008 Jean-Philippe Lang */ + +var observingContextMenuClick; + ContextMenu = Class.create(); ContextMenu.prototype = { - initialize: function (options) { - this.options = Object.extend({selector: '.hascontextmenu'}, options || { }); - - Event.observe(document, 'click', function(e){ - var t = Event.findElement(e, 'a'); - if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) { - Event.stop(e); - } else { - $('context-menu').hide(); - if (this.selection) { - this.selection.removeClassName('context-menu-selection'); - } - } - - }.bind(this)); - - $$(this.options.selector).invoke('observe', (window.opera ? 'click' : 'contextmenu'), function(e){ - if (window.opera && !e.ctrlKey) { - return; - } - this.show(e); - }.bind(this)); - + initialize: function (url) { + this.url = url; + + // prevent selection when using Ctrl/Shit key + var tables = $$('table.issues'); + for (i=0; i 0) { inputs[0].checked = checked; } + }, + + isSelected: function(tr) { + return Element.hasClassName(tr, 'context-menu-selection'); + } +} - var tr = Event.findElement(e, 'tr'); - tr.addClassName('context-menu-selection'); - this.selection = tr; - var id = tr.id.substring(6, tr.id.length); - /* TODO: do not hard code path */ - new Ajax.Updater({success:'context-menu'}, '../../issues/context_menu/' + id, {asynchronous:true, evalScripts:true, onComplete:function(request){ - Effect.Appear('context-menu', {duration: 0.20}); - if (window.parseStylesheets) { window.parseStylesheets(); } - }}) +function toggleIssuesSelection(el) { + var boxes = el.getElementsBySelector('input[type=checkbox]'); + var all_checked = true; + for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } + for (i = 0; i < boxes.length; i++) { + if (all_checked) { + boxes[i].checked = false; + boxes[i].up('tr').removeClassName('context-menu-selection'); + } else if (boxes[i].checked == false) { + boxes[i].checked = true; + boxes[i].up('tr').addClassName('context-menu-selection'); + } } } diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 6fdbb0341..4f28ab224 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -197,6 +197,28 @@ class IssuesControllerTest < Test::Unit::TestCase assert_not_nil assigns(:issue) assert_equal Issue.find(1), assigns(:issue) end + + def test_get_edit_with_params + @request.session[:user_id] = 2 + get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 } + assert_response :success + assert_template 'edit' + + issue = assigns(:issue) + assert_not_nil issue + + assert_equal 5, issue.status_id + assert_tag :select, :attributes => { :name => 'issue[status_id]' }, + :child => { :tag => 'option', + :content => 'Closed', + :attributes => { :selected => 'selected' } } + + assert_equal 7, issue.priority_id + assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, + :child => { :tag => 'option', + :content => 'Urgent', + :attributes => { :selected => 'selected' } } + end def test_post_edit @request.session[:user_id] = 2 @@ -305,12 +327,105 @@ class IssuesControllerTest < Test::Unit::TestCase # No email should be sent assert ActionMailer::Base.deliveries.empty? end - - def test_context_menu + + def test_bulk_edit @request.session[:user_id] = 2 - get :context_menu, :id => 1 + # update issues priority + post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => '' + assert_response 302 + # check that the issues were updated + assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} + assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes + end + + def test_move_one_issue_to_another_project + @request.session[:user_id] = 1 + post :move, :id => 1, :new_project_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + assert_equal 2, Issue.find(1).project_id + end + + def test_bulk_move_to_another_project + @request.session[:user_id] = 1 + post :move, :ids => [1, 2], :new_project_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + # Issues moved to project 2 + assert_equal 2, Issue.find(1).project_id + assert_equal 2, Issue.find(2).project_id + # No tracker change + assert_equal 1, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_move_to_another_tracker + @request.session[:user_id] = 1 + post :move, :ids => [1, 2], :new_tracker_id => 2 + assert_redirected_to 'projects/ecookbook/issues' + assert_equal 2, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_context_menu_one_issue + @request.session[:user_id] = 2 + get :context_menu, :ids => [1] assert_response :success assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/edit/1', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5', + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8', + :class => '' } + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1', + :class => 'icon-copy' } + assert_tag :tag => 'a', :content => 'Move', + :attributes => { :href => '/issues/move?ids%5B%5D=1', + :class => 'icon-move' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues/destroy?ids%5B%5D=1', + :class => 'icon-del' } + end + + def test_context_menu_one_issue_by_anonymous + get :context_menu, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } + end + + def test_context_menu_multiple_issues_of_same_project + @request.session[:user_id] = 2 + get :context_menu, :ids => [1, 2] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Move', + :attributes => { :href => '/issues/move?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-move' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues/destroy?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-del' } + end + + def test_context_menu_multiple_issues_of_different_project + @request.session[:user_id] = 2 + get :context_menu, :ids => [1, 2, 4] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } end def test_destroy diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 71a3e3dfe..92ac6f09a 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -93,32 +93,6 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_nil Project.find_by_id(1) end - def test_bulk_edit_issues - @request.session[:user_id] = 2 - # update issues priority - post :bulk_edit_issues, :id => 1, :issue_ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => '' - assert_response 302 - # check that the issues were updated - assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} - assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes - end - - def test_move_issues_to_another_project - @request.session[:user_id] = 1 - post :move_issues, :id => 1, :issue_ids => [1, 2], :new_project_id => 2 - assert_redirected_to 'projects/ecookbook/issues' - assert_equal 2, Issue.find(1).project_id - assert_equal 2, Issue.find(2).project_id - end - - def test_move_issues_to_another_tracker - @request.session[:user_id] = 1 - post :move_issues, :id => 1, :issue_ids => [1, 2], :new_tracker_id => 2 - assert_redirected_to 'projects/ecookbook/issues' - assert_equal 2, Issue.find(1).tracker_id - assert_equal 2, Issue.find(2).tracker_id - end - def test_list_files get :list_files, :id => 1 assert_response :success From 14b3d6d0120549cd7f36c395bedc39272c322f9a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 10 Feb 2008 14:14:18 +0000 Subject: [PATCH 192/710] Fixed: Anonymous users may not see the issue list headers in the correct language. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1131 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index f4883cf52..a6d881ff3 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -65,7 +65,7 @@ class ApplicationController < ActionController::Base 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 + User.current.language = accept_lang end end rescue From d859d38dd9d430f1a1fe330d2c9149a94aa667af Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 10 Feb 2008 14:24:51 +0000 Subject: [PATCH 193/710] =?UTF-8?q?Translation=20updates:=20*=20Finnish=20?= =?UTF-8?q?(Antti=20Perki=C3=B6m=C3=A4ki,=20#612)=20*=20Russian=20(Michael?= =?UTF-8?q?=20Pirogov,=20#622)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1132 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/fi.yml | 10 +++---- lang/ru.yml | 10 +++---- .../jstoolbar/lang/jstoolbar-fi.js | 26 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lang/fi.yml b/lang/fi.yml index 7a8848302..dda26242b 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -79,7 +79,7 @@ notice_default_data_loaded: Vakio asetusten palautus onnistui. error_can_t_load_default_data: "Vakio asetuksia ei voitu ladata: %s" error_scm_not_found: "Syötettä ja/tai versiota ei löydy säiliöstä." -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_scm_command_failed: "Säiliöön pääsyssä tapahtui virhe: %s" mail_subject_lost_password: Sinun Redmine salasanasi mail_body_lost_password: 'Vaihtaaksesi Redmine salasanasi, paina seuraavaa linkkiä:' @@ -533,8 +533,8 @@ text_tracker_no_workflow: Ei työnkulkua määritelty tälle tiketille text_unallowed_characters: Kiellettyjä merkkejä text_comma_separated: Useat arvot sallittu (pilkku eroteltuna). text_issues_ref_in_commit_messages: Liitän ja korjaan ongelmia syötetyssä viestissä -text_issue_added: Tapahtuma %s on kirjattu by %s. -text_issue_updated: Tapahtuma %s on päivitetty by %s. +text_issue_added: Tapahtuma %s on kirjattu. +text_issue_updated: Tapahtuma %s on päivitetty. text_wiki_destroy_confirmation: Oletko varma että haluat poistaa tämän wiki:n ja kaikki sen sisältämän tiedon? text_issue_category_destroy_question: Jotkut tapahtumat (%d) ovat nimetty tälle luokalle. Mitä haluat tehdä? text_issue_category_destroy_assignments: Poista luokan tehtävät @@ -569,7 +569,7 @@ enumeration_issue_priorities: Tapahtuman prioriteetit enumeration_doc_categories: Dokumentin luokat enumeration_activities: Aktiviteetit (ajan seuranta) label_associated_revisions: Liittyvät versiot -setting_user_format: Users display format +setting_user_format: Käyttäjien esitysmuoto text_status_changed_by_changeset: Applied in changeset %s. -label_more: More text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' +label_more: More diff --git a/lang/ru.yml b/lang/ru.yml index c720dfb72..f4cbb9976 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -531,9 +531,9 @@ text_tracker_no_workflow: Для этого трекера последоват text_unallowed_characters: Запрещенные символы text_comma_separated: Допустимы несколько значений (разделенные запятой). text_issues_ref_in_commit_messages: Сопоставление и изменение статуса задач исходя из текста сообщений -text_issue_added: О вопросе %s был создает отчет (by %s). -text_issue_updated: Вопрос %s был обновлен (by %s). -text_wiki_destroy_confirmation: Вы уверены, что хотите удалить данную вики и все содержание? +text_issue_added: По задаче %s был создан отчет (%s). +text_issue_updated: Задача %s была обновлена (%s). +text_wiki_destroy_confirmation: Вы уверены, что хотите удалить данную вики и все содержимое? text_issue_category_destroy_question: Несколько задач (%d) назначено в данную категорию. Что вы хотите предпринять? text_issue_category_destroy_assignments: Удалить назначения категории text_issue_category_reassign_to: Переназначить задачи для данной категории @@ -567,6 +567,6 @@ default_activity_development: Разработка enumeration_issue_priorities: Приоритеты задач enumeration_doc_categories: Категории документов enumeration_activities: Действия (учет времени) -text_status_changed_by_changeset: Applied in changeset %s. -label_more: More +text_status_changed_by_changeset: Реализовано в %s редакции. +label_more: Больше text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-fi.js b/public/javascripts/jstoolbar/lang/jstoolbar-fi.js index cd36a4b55..357d25951 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-fi.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-fi.js @@ -1,14 +1,14 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Code'] = 'Inline Code'; -jsToolBar.strings['Heading 1'] = 'Heading 1'; -jsToolBar.strings['Heading 2'] = 'Heading 2'; -jsToolBar.strings['Heading 3'] = 'Heading 3'; -jsToolBar.strings['Unordered list'] = 'Unordered list'; -jsToolBar.strings['Ordered list'] = 'Ordered list'; -jsToolBar.strings['Preformatted text'] = 'Preformatted text'; -jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; -jsToolBar.strings['Image'] = 'Image'; +jsToolBar.strings['Strong'] = 'Lihavoitu'; +jsToolBar.strings['Italic'] = 'Kursivoitu'; +jsToolBar.strings['Underline'] = 'Alleviivattu'; +jsToolBar.strings['Deleted'] = 'Yliviivattu'; +jsToolBar.strings['Code'] = 'Koodi näkymä'; +jsToolBar.strings['Heading 1'] = 'Otsikko 1'; +jsToolBar.strings['Heading 2'] = 'Otsikko 2'; +jsToolBar.strings['Heading 3'] = 'Otsikko 3'; +jsToolBar.strings['Unordered list'] = 'Järjestämätön lista'; +jsToolBar.strings['Ordered list'] = 'Järjestetty lista'; +jsToolBar.strings['Preformatted text'] = 'Ennaltamuotoiltu teksti'; +jsToolBar.strings['Wiki link'] = 'Linkki Wiki sivulle'; +jsToolBar.strings['Image'] = 'Kuva'; From 913a806f9011ac3d47cd365d1c694660534f52f4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 11 Feb 2008 17:59:41 +0000 Subject: [PATCH 194/710] Fixed: context menu not available if the initial issue list is empty. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1133 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_list.rhtml | 8 -------- app/views/issues/index.rhtml | 5 +++++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index 1f5470200..343f7173d 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -19,11 +19,3 @@ <% end -%> - -<% content_for :header_tags do -%> - <%= javascript_include_tag 'context_menu' %> - <%= stylesheet_link_tag 'context_menu' %> -<% end -%> - - -<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index c5f26dfb6..4d1f4f176 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -57,4 +57,9 @@ <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> <% end %> + + +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> From 5152771fdd59cee5369ba15c4946fa8a64fefe2a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 11 Feb 2008 18:06:38 +0000 Subject: [PATCH 195/710] Fixed: 404 error when selecting an other project on issues/move. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1134 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/move.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/move.rhtml b/app/views/issues/move.rhtml index c74270f1a..35761e160 100644 --- a/app/views/issues/move.rhtml +++ b/app/views/issues/move.rhtml @@ -9,7 +9,7 @@

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

    From 15ed53cf17ffa4603e290a3777f56ff97cae7b61 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 11 Feb 2008 18:08:36 +0000 Subject: [PATCH 196/710] Footer update. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1135 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/layouts/base.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml index 91c3b2634..1f7472006 100644 --- a/app/views/layouts/base.rhtml +++ b/app/views/layouts/base.rhtml @@ -68,7 +68,7 @@
    From 93ef8b7f77ec84bef19d7a872a15b8d406cdb46e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 11 Feb 2008 20:45:46 +0000 Subject: [PATCH 197/710] Replaced window.hash= by Element.scrollTo() git-svn-id: http://redmine.rubyforge.org/svn/trunk@1136 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 2 +- app/views/issues/_edit.rhtml | 2 +- app/views/issues/new.rhtml | 3 +-- app/views/issues/show.rhtml | 1 - app/views/wiki/edit.rhtml | 3 +-- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c8b55a695..791c6d806 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -59,7 +59,7 @@ module ApplicationHelper def show_and_goto_link(name, id, options={}) onclick = "Element.show('#{id}'); " onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ") - onclick << "location.href='##{id}-anchor'; " + onclick << "Element.scrollTo('#{id}'); " onclick << "return false;" link_to(name, "#", options.merge(:onclick => onclick)) end diff --git a/app/views/issues/_edit.rhtml b/app/views/issues/_edit.rhtml index 0f843e855..5bd685e47 100644 --- a/app/views/issues/_edit.rhtml +++ b/app/views/issues/_edit.rhtml @@ -32,7 +32,7 @@ :method => 'post', :update => 'preview', :with => 'Form.serialize("issue-form")', - :complete => "location.hash='preview'" + :complete => "Element.scrollTo('preview')" }, :accesskey => accesskey(:preview) %> <% end %> diff --git a/app/views/issues/new.rhtml b/app/views/issues/new.rhtml index 1e9e323fe..52c32dbbd 100644 --- a/app/views/issues/new.rhtml +++ b/app/views/issues/new.rhtml @@ -12,9 +12,8 @@ :method => 'post', :update => 'preview', :with => "Form.serialize('issue-form')", - :complete => "location.href='#preview-top'" + :complete => "Element.scrollTo('preview')" }, :accesskey => accesskey(:preview) %> <% end %> -
    diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 3392aef83..e3999bd8e 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -89,7 +89,6 @@ end %> <% end %> <% if authorize_for('issues', 'edit') %> - - -<% content_for :header_tags do %> -<%= javascript_include_tag 'calendar/calendar' %> -<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> -<%= javascript_include_tag 'calendar/calendar-setup' %> -<%= stylesheet_link_tag 'calendar' %> -<% end %> From 80a5ad7ff344b9c5047a1990f6af19f97c4bd680 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 1 Apr 2008 17:43:20 +0000 Subject: [PATCH 354/710] Fixed: Boards are not deleted when project is deleted (closes #963). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1316 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 2 +- test/unit/project_test.rb | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index a223b35f0..eaa33c969 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -33,7 +33,7 @@ class Project < ActiveRecord::Base has_many :documents, :dependent => :destroy has_many :news, :dependent => :delete_all, :include => :author has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" - has_many :boards, :order => "position ASC" + has_many :boards, :dependent => :destroy, :order => "position ASC" has_one :repository, :dependent => :destroy has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index f7da6ecb5..9af68c231 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -18,7 +18,7 @@ require File.dirname(__FILE__) + '/../test_helper' class ProjectTest < Test::Unit::TestCase - fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers + fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers, :boards def setup @ecookbook = Project.find(1) @@ -84,12 +84,15 @@ class ProjectTest < Test::Unit::TestCase assert_equal 2, @ecookbook.members.size # and 1 is locked assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size + # some boards + assert @ecookbook.boards.any? @ecookbook.destroy # make sure that the project non longer exists assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) } - # make sure all members have been removed - assert_equal 0, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size + # make sure related data was removed + assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty? + assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty? end def test_subproject_ok From e4da9d6f10edda72211b63659ab938a5ad7f0cda Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 1 Apr 2008 17:48:11 +0000 Subject: [PATCH 355/710] Prevent NoMethodError on nil class if custom_fields params is not present in IssuesController#new (#969). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1317 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 4 +++- test/functional/issues_controller_test.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 7d4212095..269e988d9 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -140,7 +140,9 @@ class IssuesController < ApplicationController requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) # Check that the user is allowed to apply the requested status @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } + @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]) diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 7d49f088d..89769ffe2 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -181,6 +181,16 @@ class IssuesControllerTest < Test::Unit::TestCase assert_equal 'Value for field 2', v.value end + def test_post_new_without_custom_fields_param + @request.session[:user_id] = 2 + post :new, :project_id => 1, + :issue => {:tracker_id => 1, + :subject => 'This is the test_new issue', + :description => 'This is the description', + :priority_id => 5} + assert_redirected_to 'issues/show' + end + def test_copy_issue @request.session[:user_id] = 2 get :new, :project_id => 1, :copy_from => 1 From 043cb37b161d712e1c6fe7b9aa41d7af356cf795 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 1 Apr 2008 19:40:40 +0000 Subject: [PATCH 356/710] Add predefined date ranges to the time report in the same way as the details view (closes #972). It nows defaults to 'All time'. This patch also fixes time report periods (columns) computation. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1318 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 135 +++++++++++---------- app/views/timelog/_date_range.rhtml | 16 +++ app/views/timelog/details.rhtml | 18 +-- app/views/timelog/report.rhtml | 26 ++-- test/functional/timelog_controller_test.rb | 18 ++- 5 files changed, 108 insertions(+), 105 deletions(-) create mode 100644 app/views/timelog/_date_range.rhtml diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 8cfe225d1..7e2b55872 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -53,16 +53,11 @@ class TimelogController < ApplicationController @criterias.uniq! @criterias = @criterias[0,3] - @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month' + @columns = (params[:columns] && %w(year month week).include?(params[:columns])) ? params[:columns] : 'month' - if params[:date_from] - begin; @date_from = params[:date_from].to_date; rescue; end - end - if params[:date_to] - begin; @date_to = params[:date_to].to_date; rescue; end - end - @date_from ||= Date.civil(Date.today.year, 1, 1) - @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1 + retrieve_date_range + @from ||= TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today + @to ||= TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today unless @criterias.empty? sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') @@ -74,7 +69,7 @@ class TimelogController < ApplicationController sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id" sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?) sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries) - sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)] + sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)] sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek" @hours = ActiveRecord::Base.connection.select_all(sql) @@ -91,22 +86,23 @@ class TimelogController < ApplicationController end @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f} - end - - @periods = [] - date_from = @date_from - # 100 columns max - while date_from < @date_to && @periods.length < 100 - case @columns - when 'year' - @periods << "#{date_from.year}" - date_from = date_from >> 12 - when 'month' - @periods << "#{date_from.year}-#{date_from.month}" - date_from = date_from >> 1 - when 'week' - @periods << "#{date_from.year}-#{date_from.cweek}" - date_from = date_from + 7 + + @periods = [] + # Date#at_beginning_of_ not supported in Rails 1.2.x + date_from = @from.to_time + # 100 columns max + while date_from <= @to.to_time && @periods.length < 100 + case @columns + when 'year' + @periods << "#{date_from.year}" + date_from = (date_from + 1.year).at_beginning_of_year + when 'month' + @periods << "#{date_from.year}-#{date_from.month}" + date_from = (date_from + 1.month).at_beginning_of_month + when 'week' + @periods << "#{date_from.year}-#{date_from.to_date.cweek}" + date_from = (date_from + 7.day).at_beginning_of_week + end end end @@ -116,52 +112,13 @@ class TimelogController < ApplicationController def details sort_init 'spent_on', 'desc' sort_update - - @free_period = false - @from, @to = nil, nil - - if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?) - case params[:period].to_s - when 'today' - @from = @to = Date.today - when 'yesterday' - @from = @to = Date.today - 1 - when 'current_week' - @from = Date.today - (Date.today.cwday - 1)%7 - @to = @from + 6 - when 'last_week' - @from = Date.today - 7 - (Date.today.cwday - 1)%7 - @to = @from + 6 - when '7_days' - @from = Date.today - 7 - @to = Date.today - when 'current_month' - @from = Date.civil(Date.today.year, Date.today.month, 1) - @to = (@from >> 1) - 1 - when 'last_month' - @from = Date.civil(Date.today.year, Date.today.month, 1) << 1 - @to = (@from >> 1) - 1 - when '30_days' - @from = Date.today - 30 - @to = Date.today - when 'current_year' - @from = Date.civil(Date.today.year, 1, 1) - @to = Date.civil(Date.today.year, 12, 31) - end - elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?)) - begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end - begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end - @free_period = true - else - # default - end - - @from, @to = @to, @from if @from && @to && @from > @to cond = ARCondition.new cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) : ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]) + retrieve_date_range + if @from if @to cond << ['spent_on BETWEEN ? AND ?', @from, @to] @@ -238,4 +195,48 @@ private rescue ActiveRecord::RecordNotFound render_404 end + + # Retreive the date range based on predefined ranges or specific from/to param dates + def retrieve_date_range + @free_period = false + @from, @to = nil, nil + + if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?) + case params[:period].to_s + when 'today' + @from = @to = Date.today + when 'yesterday' + @from = @to = Date.today - 1 + when 'current_week' + @from = Date.today - (Date.today.cwday - 1)%7 + @to = @from + 6 + when 'last_week' + @from = Date.today - 7 - (Date.today.cwday - 1)%7 + @to = @from + 6 + when '7_days' + @from = Date.today - 7 + @to = Date.today + when 'current_month' + @from = Date.civil(Date.today.year, Date.today.month, 1) + @to = (@from >> 1) - 1 + when 'last_month' + @from = Date.civil(Date.today.year, Date.today.month, 1) << 1 + @to = (@from >> 1) - 1 + when '30_days' + @from = Date.today - 30 + @to = Date.today + when 'current_year' + @from = Date.civil(Date.today.year, 1, 1) + @to = Date.civil(Date.today.year, 12, 31) + end + elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?)) + begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end + begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end + @free_period = true + else + # default + end + + @from, @to = @to, @from if @from && @to && @from > @to + end end diff --git a/app/views/timelog/_date_range.rhtml b/app/views/timelog/_date_range.rhtml new file mode 100644 index 000000000..ac46fea18 --- /dev/null +++ b/app/views/timelog/_date_range.rhtml @@ -0,0 +1,16 @@ +
    <%= l(:label_date_range) %> +

    +<%= radio_button_tag 'period_type', '1', !@free_period %> +<%= select_tag 'period', options_for_period_select(params[:period]), + :onchange => 'this.form.onsubmit();', + :onfocus => '$("period_type_1").checked = true;' %> +

    +

    +<%= radio_button_tag 'period_type', '2', @free_period %> +<%= l(:label_date_from) %> +<%= text_field_tag 'from', @from, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('from') %> +<%= l(:label_date_to) %> +<%= text_field_tag 'to', @to, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('to') %> +<%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %> +

    +
    diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/details.rhtml index 89793745e..f1e80da1f 100644 --- a/app/views/timelog/details.rhtml +++ b/app/views/timelog/details.rhtml @@ -12,23 +12,7 @@ <% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %> <%= hidden_field_tag 'project_id', params[:project_id] %> <%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %> - -
    <%= l(:label_date_range) %> -

    -<%= radio_button_tag 'period_type', '1', !@free_period %> -<%= select_tag 'period', options_for_period_select(params[:period]), - :onchange => 'this.form.onsubmit();', - :onfocus => '$("period_type_1").checked = true;' %> -

    -

    -<%= radio_button_tag 'period_type', '2', @free_period %> -<%= l(:label_date_from) %> -<%= text_field_tag 'from', @from, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('from') %> -<%= l(:label_date_to) %> -<%= text_field_tag 'to', @to, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('to') %> -<%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %> -

    -
    +<%= render :partial => 'date_range' %> <% end %>
    diff --git a/app/views/timelog/report.rhtml b/app/views/timelog/report.rhtml index 2682a5cb0..2e08e5883 100644 --- a/app/views/timelog/report.rhtml +++ b/app/views/timelog/report.rhtml @@ -5,32 +5,27 @@

    <%= l(:label_spent_time) %>

    -<% form_remote_tag(:url => {:project_id => @project}, :update => 'content') do %> +<% form_remote_tag(:url => {}, :update => 'content') do %> <% @criterias.each do |criteria| %> <%= hidden_field_tag 'criterias[]', criteria %> <% end %> -
    <%= l(:label_date_range) %> -

    - <%= l(:label_date_from) %> - <%= text_field_tag 'date_from', @date_from, :size => 10 %><%= calendar_for('date_from') %> - <%= l(:label_date_to) %> - <%= text_field_tag 'date_to', @date_to, :size => 10 %><%= calendar_for('date_to') %> - <%= l(:label_details) %> - <%= select_tag 'period', options_for_select([[l(:label_year), 'year'], - [l(:label_month), 'month'], - [l(:label_week), 'week']], @columns) %> -   - <%= submit_tag l(:button_apply) %> + <%= hidden_field_tag 'project_id', params[:project_id] %> + <%= render :partial => 'date_range' %>

    +

    <%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'], + [l(:label_month), 'month'], + [l(:label_week), 'week']], @columns), + :onchange => "this.form.onsubmit();" %> -

    <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), + <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), :onchange => "this.form.onsubmit();", :style => 'width: 200px', :disabled => (@criterias.length >= 3)) %> <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'}, :class => 'icon icon-reload' %>

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

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

    @@ -62,4 +57,3 @@ <% end %> <% end %> -<% end %> diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index fa4432295..9ca1a6cbf 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -78,17 +78,25 @@ class TimelogControllerTest < Test::Unit::TestCase assert_response :success assert_template 'report' end - + + def test_report_all_time + get :report, :project_id => 1, :criterias => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + end + def test_report_one_criteria - get :report, :project_id => 1, :period => 'week', :date_from => "2007-04-01", :date_to => "2007-04-30", :criterias => ['project'] + get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project'] assert_response :success assert_template 'report' assert_not_nil assigns(:total_hours) assert_equal "8.65", "%.2f" % assigns(:total_hours) - end + end def test_report_two_criterias - get :report, :project_id => 1, :period => 'month', :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member", "activity"] + get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"] assert_response :success assert_template 'report' assert_not_nil assigns(:total_hours) @@ -96,7 +104,7 @@ class TimelogControllerTest < Test::Unit::TestCase end def test_report_one_criteria_no_result - get :report, :project_id => 1, :period => 'week', :date_from => "1998-04-01", :date_to => "1998-04-30", :criterias => ['project'] + get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project'] assert_response :success assert_template 'report' assert_not_nil assigns(:total_hours) From 467f74510e44d2c0821fad691a71bc0292b8944c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 1 Apr 2008 22:42:10 +0000 Subject: [PATCH 357/710] Time report can be done at issue level (closes #970) + timelog views xhtml validation. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1319 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 7 +++++-- app/models/issue.rb | 4 ++++ app/views/timelog/_list.rhtml | 4 +++- app/views/timelog/details.rhtml | 2 ++ app/views/timelog/report.rhtml | 13 ++++++++----- public/stylesheets/application.css | 2 +- test/functional/timelog_controller_test.rb | 2 +- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 7e2b55872..e617d3805 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -45,7 +45,10 @@ class TimelogController < ApplicationController :label => :label_tracker}, 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", :klass => Enumeration, - :label => :label_activity} + :label => :label_activity}, + 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", + :klass => Issue, + :label => :label_issue} } @criterias = params[:criterias] || [] @@ -196,7 +199,7 @@ private render_404 end - # Retreive the date range based on predefined ranges or specific from/to param dates + # Retrieves the date range based on predefined ranges or specific from/to param dates def retrieve_date_range @free_period = false @from, @to = nil, nil diff --git a/app/models/issue.rb b/app/models/issue.rb index 1d25a4604..d6fcf53f2 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -237,4 +237,8 @@ class Issue < ActiveRecord::Base yield end end + + def to_s + "#{tracker} ##{id}: #{subject}" + end end diff --git a/app/views/timelog/_list.rhtml b/app/views/timelog/_list.rhtml index 67e3c67d5..189f4f5e8 100644 --- a/app/views/timelog/_list.rhtml +++ b/app/views/timelog/_list.rhtml @@ -1,5 +1,6 @@ + <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %> <%= sort_header_tag('user_id', :caption => l(:label_member)) %> <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %> @@ -8,6 +9,7 @@ <%= sort_header_tag('hours', :caption => l(:field_hours)) %> + <% entries.each do |entry| -%> @@ -35,5 +37,5 @@ <% end -%> - +
    <%= l(:field_comments) %>
    diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/details.rhtml index f1e80da1f..a0810fbde 100644 --- a/app/views/timelog/details.rhtml +++ b/app/views/timelog/details.rhtml @@ -28,3 +28,5 @@ <%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %>

    <% end %> + +<% html_title l(:label_spent_time), l(:label_details) %> diff --git a/app/views/timelog/report.rhtml b/app/views/timelog/report.rhtml index 2e08e5883..92d0e8782 100644 --- a/app/views/timelog/report.rhtml +++ b/app/views/timelog/report.rhtml @@ -7,12 +7,11 @@ <% form_remote_tag(:url => {}, :update => 'content') do %> <% @criterias.each do |criteria| %> - <%= hidden_field_tag 'criterias[]', criteria %> + <%= hidden_field_tag 'criterias[]', criteria, :id => nil %> <% end %> <%= hidden_field_tag 'project_id', params[:project_id] %> <%= render :partial => 'date_range' %> -

    - +

    <%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'], [l(:label_month), 'month'], [l(:label_week), 'week']], @columns), @@ -21,6 +20,7 @@ <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), :onchange => "this.form.onsubmit();", :style => 'width: 200px', + :id => nil, :disabled => (@criterias.length >= 3)) %> <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'}, :class => 'icon icon-reload' %>

    @@ -36,10 +36,10 @@ <% @criterias.each do |criteria| %> - <%= l(@available_criterias[criteria][:label]) %> + <%= l(@available_criterias[criteria][:label]) %> <% end %> <% @periods.each do |period| %> - <%= period %> + <%= period %> <% end %> @@ -57,3 +57,6 @@ <% end %> <% end %> + +<% html_title l(:label_spent_time), l(:label_report) %> + diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 3d614fe19..69e89b0ed 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -183,7 +183,7 @@ div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom div#version-summary fieldset { margin-bottom: 1em; } div#version-summary .total-hours { text-align: right; } -table#time-report td.hours { text-align: right; padding-right: 0.5em; } +table#time-report td.hours, table#time-report th.period { text-align: right; padding-right: 0.5em; } table#time-report tbody tr { font-style: italic; color: #777; } table#time-report tbody tr.last-level { font-style: normal; color: #555; } table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; } diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index 9ca1a6cbf..c55d5248c 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -80,7 +80,7 @@ class TimelogControllerTest < Test::Unit::TestCase end def test_report_all_time - get :report, :project_id => 1, :criterias => ['project'] + get :report, :project_id => 1, :criterias => ['project', 'issue'] assert_response :success assert_template 'report' assert_not_nil assigns(:total_hours) From 4cbe6b626e3b338d7c8d7dcb236d40f8f99ff393 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 2 Apr 2008 19:52:12 +0000 Subject: [PATCH 358/710] Accept the following formats for the timelog "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30. Also accept 1,5 for 1.5 hour (closes #975). Note that 1.5 is still equal to 1h30 and not 1h50 (#947). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1320 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/time_entry.rb | 18 +++++++++++++- test/unit/time_entry_test.rb | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/unit/time_entry_test.rb diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index bcf6d1223..0dce253c7 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -1,5 +1,5 @@ # redMine - project management software -# Copyright (C) 2006-2007 Jean-Philippe Lang +# 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 @@ -39,6 +39,22 @@ class TimeEntry < ActiveRecord::Base errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project) end + def hours=(h) + s = h.dup + if s.is_a?(String) + s.strip! + unless s =~ %r{^[\d\.,]+$} + # 2:30 => 2.5 + s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 } + # 2h30, 2h, 30m + s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] } + end + # 2,5 => 2.5 + s.gsub!(',', '.') + end + write_attribute :hours, s + end + # tyear, tmonth, tweek assigned where setting spent_on attributes # these attributes make time aggregations easier def spent_on=(date) diff --git a/test/unit/time_entry_test.rb b/test/unit/time_entry_test.rb new file mode 100644 index 000000000..f86e42eab --- /dev/null +++ b/test/unit/time_entry_test.rb @@ -0,0 +1,46 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class TimeEntryTest < Test::Unit::TestCase + fixtures :issues, :projects, :users, :time_entries + + def test_hours_format + assertions = { "2" => 2.0, + "21.1" => 21.1, + "2,1" => 2.1, + "7:12" => 7.2, + "10h" => 10.0, + "10 h" => 10.0, + "45m" => 0.75, + "45 m" => 0.75, + "3h15" => 3.25, + "3h 15" => 3.25, + "3 h 15" => 3.25, + "3 h 15m" => 3.25, + "3 h 15 m" => 3.25, + "3 hours" => 3.0, + "12min" => 0.2, + } + + assertions.each do |k, v| + t = TimeEntry.new(:hours => k) + assert_equal v, t.hours + end + end +end From 6348eeaf8a5124dfd7793211d673bb928f1ea64b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 2 Apr 2008 21:30:32 +0000 Subject: [PATCH 359/710] Attachment model clean up: fixed some inconsistent indentation and an inaccurate comment (closes patch #903 by Rocco Stanzione). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1321 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/attachment.rb | 97 ++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index cdcb8d231..08f440816 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -35,48 +35,48 @@ class Attachment < ActiveRecord::Base errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes end - def file=(incomming_file) - unless incomming_file.nil? - @temp_file = incomming_file - if @temp_file.size > 0 - self.filename = sanitize_filename(@temp_file.original_filename) - self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename - self.content_type = @temp_file.content_type.to_s.chomp - self.filesize = @temp_file.size - end - end - end + def file=(incoming_file) + unless incoming_file.nil? + @temp_file = incoming_file + if @temp_file.size > 0 + self.filename = sanitize_filename(@temp_file.original_filename) + self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename + self.content_type = @temp_file.content_type.to_s.chomp + self.filesize = @temp_file.size + end + end + end - def file - nil - end - - # Copy temp file to its final location - def before_save - if @temp_file && (@temp_file.size > 0) - logger.debug("saving '#{self.diskfile}'") - File.open(diskfile, "wb") do |f| - f.write(@temp_file.read) - end - self.digest = Digest::MD5.hexdigest(File.read(diskfile)) - end - # Don't save the content type if it's longer than the authorized length - if self.content_type && self.content_type.length > 255 - self.content_type = nil - end - end - - # Deletes file on the disk - def after_destroy - if self.filename? - File.delete(diskfile) if File.exist?(diskfile) - end - end - - # Returns file's location on disk - def diskfile - "#{@@storage_path}/#{self.disk_filename}" - end + def file + nil + end + + # Copy temp file to its final location + def before_save + if @temp_file && (@temp_file.size > 0) + logger.debug("saving '#{self.diskfile}'") + File.open(diskfile, "wb") do |f| + f.write(@temp_file.read) + end + self.digest = Digest::MD5.hexdigest(File.read(diskfile)) + end + # Don't save the content type if it's longer than the authorized length + if self.content_type && self.content_type.length > 255 + self.content_type = nil + end + end + + # Deletes file on the disk + def after_destroy + if self.filename? + File.delete(diskfile) if File.exist?(diskfile) + end + end + + # Returns file's location on disk + def diskfile + "#{@@storage_path}/#{self.disk_filename}" + end def increment_download increment!(:downloads) @@ -87,18 +87,17 @@ class Attachment < ActiveRecord::Base end def image? - self.filename =~ /\.(jpeg|jpg|gif|png)$/i + self.filename =~ /\.(jpe?g|gif|png)$/i end private def sanitize_filename(value) - # get only the filename, not the whole path - just_filename = value.gsub(/^.*(\\|\/)/, '') - # NOTE: File.basename doesn't work right with Windows paths on Unix - # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) + # get only the filename, not the whole path + just_filename = value.gsub(/^.*(\\|\/)/, '') + # NOTE: File.basename doesn't work right with Windows paths on Unix + # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) - # Finally, replace all non alphanumeric, underscore or periods with underscore - @filename = just_filename.gsub(/[^\w\.\-]/,'_') + # Finally, replace all non alphanumeric, hyphens or periods with underscore + @filename = just_filename.gsub(/[^\w\.\-]/,'_') end - end From 7f0aa5611948ebed0c1eaf4c3218762fb50e199e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 3 Apr 2008 13:16:51 +0000 Subject: [PATCH 360/710] Fixed: trying to preview a new issue raises an exception with postgresql (close #984). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1322 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 4 ++-- app/views/issues/new.rhtml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 269e988d9..b3f21fddf 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -344,8 +344,8 @@ class IssuesController < ApplicationController end def preview - issue = @project.issues.find_by_id(params[:id]) - @attachements = issue.attachments if issue + @issue = @project.issues.find_by_id(params[:id]) if params[:id] + @attachements = issue.attachments if @issue @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil) render :partial => 'common/preview' end diff --git a/app/views/issues/new.rhtml b/app/views/issues/new.rhtml index 7b9dde899..280e2009b 100644 --- a/app/views/issues/new.rhtml +++ b/app/views/issues/new.rhtml @@ -8,7 +8,7 @@
    <%= submit_tag l(:button_create) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'issues', :action => 'preview', :project_id => @project, :id => @issue }, + { :url => { :controller => 'issues', :action => 'preview', :project_id => @project }, :method => 'post', :update => 'preview', :with => "Form.serialize('issue-form')", From f6ce427a00650e92f414dc3682e56348d549d7ea Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 3 Apr 2008 16:38:06 +0000 Subject: [PATCH 361/710] Display the context menu above and/or to the left of the click if needed (patch by Mike Duchene, closes #960). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1323 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/javascripts/context_menu.js | 65 +++++++++++++++++++++++++++-- public/stylesheets/context_menu.css | 10 ++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js index e3f128d89..3e2d571fa 100644 --- a/public/javascripts/context_menu.js +++ b/public/javascripts/context_menu.js @@ -93,14 +93,55 @@ ContextMenu.prototype = { }, showMenu: function(e) { - $('context-menu').style['left'] = (Event.pointerX(e) + 'px'); - $('context-menu').style['top'] = (Event.pointerY(e) + 'px'); - Element.update('context-menu', ''); - new Ajax.Updater({success:'context-menu'}, this.url, + var mouse_x = Event.pointerX(e); + var mouse_y = Event.pointerY(e); + var render_x = mouse_x; + var render_y = mouse_y; + var dims; + var menu_width; + var menu_height; + var window_width; + var window_height; + var max_width; + var max_height; + + $('context-menu').style['left'] = (render_x + 'px'); + $('context-menu').style['top'] = (render_y + 'px'); + Element.update('context-menu', ''); + + new Ajax.Updater({success:'context-menu'}, this.url, {asynchronous:true, evalScripts:true, parameters:Form.serialize(Event.findElement(e, 'form')), onComplete:function(request){ + dims = $('context-menu').getDimensions(); + menu_width = dims.width; + menu_height = dims.height; + max_width = mouse_x + 2*menu_width; + max_height = mouse_y + menu_height; + + var ws = window_size(); + window_width = ws.width; + window_height = ws.height; + + /* display the menu above and/or to the left of the click if needed */ + if (max_width > window_width) { + render_x -= menu_width; + $('context-menu').addClassName('reverse-x'); + } else { + $('context-menu').removeClassName('reverse-x'); + } + if (max_height > window_height) { + render_y -= menu_height; + $('context-menu').addClassName('reverse-y'); + } else { + $('context-menu').removeClassName('reverse-y'); + } + if (render_x <= 0) render_x = 1; + if (render_y <= 0) render_y = 1; + $('context-menu').style['left'] = (render_x + 'px'); + $('context-menu').style['top'] = (render_y + 'px'); + Effect.Appear('context-menu', {duration: 0.20}); if (window.parseStylesheets) { window.parseStylesheets(); } // IE }}) @@ -159,3 +200,19 @@ function toggleIssuesSelection(el) { } } } + +function window_size() { + var w; + var h; + if (window.innerWidth) { + w = window.innerWidth; + h = window.innerHeight; + } else if (document.documentElement) { + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; + } else { + w = document.body.clientWidth; + h = document.body.clientHeight; + } + return {width: w, height: h}; +} diff --git a/public/stylesheets/context_menu.css b/public/stylesheets/context_menu.css index f68b33fe1..e5a83be0d 100644 --- a/public/stylesheets/context_menu.css +++ b/public/stylesheets/context_menu.css @@ -22,13 +22,13 @@ padding:1px; z-index:9; } -#context-menu li.folder ul { - position:absolute; - left:168px; /* IE6 */ - top:-2px; -} +#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; } #context-menu li.folder>ul { left:148px; } +#context-menu.reverse-y li.folder>ul { top:auto; bottom:0; } +#context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ } +#context-menu.reverse-x li.folder>ul { right:148px; } + #context-menu a { border:1px solid white; text-decoration:none; From 14a2b7e9b5765de3145c63e22affee06f20e33fc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 3 Apr 2008 16:50:53 +0000 Subject: [PATCH 362/710] Verify rev and rev_to params format in RepositoriesController. And turn revision arguments into integers in SubversionAdapter. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1324 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 11 ++++++++--- lib/redmine/scm/adapters/subversion_adapter.rb | 14 ++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 10c235d65..9b59b51ec 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -19,8 +19,8 @@ require 'SVG/Graph/Bar' require 'SVG/Graph/BarHorizontal' require 'digest/sha1' -class ChangesetNotFound < Exception -end +class ChangesetNotFound < Exception; end +class InvalidRevisionParam < Exception; end class RepositoriesController < ApplicationController layout 'base' @@ -135,7 +135,6 @@ class RepositoriesController < ApplicationController end def diff - @rev_to = params[:rev_to] @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) @@ -180,6 +179,8 @@ private render_404 end + REV_PARAM_RE = %r{^[a-f0-9]*$} + def find_repository @project = Project.find(params[:id]) @repository = @project.repository @@ -187,8 +188,12 @@ private @path = params[:path].join('/') unless params[:path].nil? @path ||= '' @rev = params[:rev] + @rev_to = params[:rev_to] + raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE) rescue ActiveRecord::RecordNotFound render_404 + rescue InvalidRevisionParam + show_error_not_found end def show_error_not_found diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index 1e0320e2c..efbd3ba8e 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -62,7 +62,7 @@ module Redmine # or nil if the given path doesn't exist in the repository def entries(path=nil, identifier=nil) path ||= '' - identifier = 'HEAD' unless identifier and identifier > 0 + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" entries = Entries.new cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}" cmd << credentials_string @@ -94,8 +94,8 @@ module Redmine def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) path ||= '' - identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0 - identifier_to = 1 unless identifier_to and identifier_to.to_i > 0 + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD" + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1 revisions = Revisions.new cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}" cmd << credentials_string @@ -131,11 +131,9 @@ module Redmine def diff(path, identifier_from, identifier_to=nil, type="inline") path ||= '' - if identifier_to and identifier_to.to_i > 0 - identifier_to = identifier_to.to_i - else - identifier_to = identifier_from.to_i - 1 - end + identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : '' + identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1) + cmd = "#{SVN_BIN} diff -r " cmd << "#{identifier_to}:" cmd << "#{identifier_from}" From 8d4aa6f9ade613f831ff2043713ad062293babfd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 3 Apr 2008 22:29:58 +0000 Subject: [PATCH 363/710] Fixed: single file 'View difference' links do not work because of duplicate slashes in url. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1325 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/repositories_helper.rb | 7 +++++-- app/views/repositories/revision.rhtml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 31daf1bd8..22bdec9df 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -58,8 +58,11 @@ module RepositoriesHelper end def with_leading_slash(path) - path ||= '' - path.starts_with?('/') ? path : "/#{path}" + path.to_s.starts_with?('/') ? path : "/#{path}" + end + + def without_leading_slash(path) + path.gsub(%r{^/+}, '') end def subversion_field_tags(form, repository) diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 5a7ef1fd5..f1e176669 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -49,7 +49,7 @@
    <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %> <% if change.action == "M" %> -<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %> +<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.path), :rev => @changeset.revision %> <% end %> From 4e961f44eff23b53351d550fb46c2a346688a271 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 5 Apr 2008 16:40:26 +0000 Subject: [PATCH 364/710] Various timelog report enhancements: * report can be done using days as columns (#993) * add a total column to the time report * replace upper-right links by tabs to switch between details and report * preserve date range filter when switching between details and report git-svn-id: http://redmine.rubyforge.org/svn/trunk@1326 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 27 ++++++++++------------ app/views/timelog/_date_range.rhtml | 10 ++++++++ app/views/timelog/_report_criteria.rhtml | 4 +++- app/views/timelog/details.rhtml | 1 - app/views/timelog/report.rhtml | 17 +++++++++----- public/stylesheets/application.css | 2 +- test/fixtures/time_entries.yml | 2 +- test/functional/timelog_controller_test.rb | 21 ++++++++++++----- 8 files changed, 53 insertions(+), 31 deletions(-) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index e617d3805..77337f344 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -56,24 +56,22 @@ class TimelogController < ApplicationController @criterias.uniq! @criterias = @criterias[0,3] - @columns = (params[:columns] && %w(year month week).include?(params[:columns])) ? params[:columns] : 'month' + @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' retrieve_date_range - @from ||= TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today - @to ||= TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today unless @criterias.empty? sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') - sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours" + sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours" sql << " FROM #{TimeEntry.table_name}" sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id" sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?) sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries) sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)] - sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek" + sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" @hours = ActiveRecord::Base.connection.select_all(sql) @@ -85,6 +83,8 @@ class TimelogController < ApplicationController row['month'] = "#{row['tyear']}-#{row['tmonth']}" when 'week' row['week'] = "#{row['tyear']}-#{row['tweek']}" + when 'day' + row['day'] = "#{row['spent_on']}" end end @@ -105,6 +105,9 @@ class TimelogController < ApplicationController when 'week' @periods << "#{date_from.year}-#{date_from.to_date.cweek}" date_from = (date_from + 7.day).at_beginning_of_week + when 'day' + @periods << "#{date_from.to_date}" + date_from = date_from + 1.day end end end @@ -121,16 +124,7 @@ class TimelogController < ApplicationController ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]) retrieve_date_range - - if @from - if @to - cond << ['spent_on BETWEEN ? AND ?', @from, @to] - else - cond << ['spent_on >= ?', @from] - end - elsif @to - cond << ['spent_on <= ?', @to] - end + cond << ['spent_on BETWEEN ? AND ?', @from, @to] TimeEntry.visible_by(User.current) do respond_to do |format| @@ -145,6 +139,7 @@ class TimelogController < ApplicationController :limit => @entry_pages.items_per_page, :offset => @entry_pages.current.offset) @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f + render :layout => !request.xhr? } format.csv { @@ -241,5 +236,7 @@ private end @from, @to = @to, @from if @from && @to && @from > @to + @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1 + @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) end end diff --git a/app/views/timelog/_date_range.rhtml b/app/views/timelog/_date_range.rhtml index ac46fea18..a54f7a2f0 100644 --- a/app/views/timelog/_date_range.rhtml +++ b/app/views/timelog/_date_range.rhtml @@ -14,3 +14,13 @@ <%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %>

    + +
    +<% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %> +
      +
    • <%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project }), + :class => (@controller.action_name == 'details' ? 'selected' : nil)) %>
    • +
    • <%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project}), + :class => (@controller.action_name == 'report' ? 'selected' : nil)) %>
    • +
    +
    diff --git a/app/views/timelog/_report_criteria.rhtml b/app/views/timelog/_report_criteria.rhtml index b048c874a..661e1fdb8 100644 --- a/app/views/timelog/_report_criteria.rhtml +++ b/app/views/timelog/_report_criteria.rhtml @@ -5,10 +5,12 @@ <%= '' * level %> <%= value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value) %> <%= '' * (criterias.length - level - 1) -%> + <% total = 0 -%> <% @periods.each do |period| -%> - <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) %> + <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%> <%= html_hours("%.2f" % sum) if sum > 0 %> <% end -%> + <%= html_hours("%.2f" % total) if total > 0 %> <% if criterias.length > level+1 -%> <%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %> diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/details.rhtml index a0810fbde..f02da9959 100644 --- a/app/views/timelog/details.rhtml +++ b/app/views/timelog/details.rhtml @@ -1,5 +1,4 @@
    -<%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}, :class => 'icon icon-report') %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
    diff --git a/app/views/timelog/report.rhtml b/app/views/timelog/report.rhtml index 92d0e8782..c29cadf9c 100644 --- a/app/views/timelog/report.rhtml +++ b/app/views/timelog/report.rhtml @@ -1,5 +1,4 @@
    -<%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-details') %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
    @@ -14,7 +13,8 @@

    <%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'], [l(:label_month), 'month'], - [l(:label_week), 'week']], @columns), + [l(:label_week), 'week'], + [l(:label_day_plural).titleize, 'day']], @columns), :onchange => "this.form.onsubmit();" %> <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}), @@ -22,8 +22,9 @@ :style => 'width: 200px', :id => nil, :disabled => (@criterias.length >= 3)) %> - <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'}, - :class => 'icon icon-reload' %>

    + <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns}, + :update => 'content' + }, :class => 'icon icon-reload' %>

    <% end %> <% unless @criterias.empty? %> @@ -38,9 +39,11 @@ <% @criterias.each do |criteria| %> <%= l(@available_criterias[criteria][:label]) %> <% end %> +<% columns_width = (40 / (@periods.length+1)).to_i %> <% @periods.each do |period| %> - <%= period %> + <%= period %> <% end %> + <%= l(:label_total) %> @@ -48,10 +51,12 @@ <%= l(:label_total) %> <%= '' * (@criterias.size - 1) %> + <% total = 0 -%> <% @periods.each do |period| -%> - <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)) %> + <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%> <%= html_hours("%.2f" % sum) if sum > 0 %> <% end -%> + <%= html_hours("%.2f" % total) if total > 0 %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 69e89b0ed..27c7592f8 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -183,7 +183,7 @@ div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom div#version-summary fieldset { margin-bottom: 1em; } div#version-summary .total-hours { text-align: right; } -table#time-report td.hours, table#time-report th.period { text-align: right; padding-right: 0.5em; } +table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } table#time-report tbody tr { font-style: italic; color: #777; } table#time-report tbody tr.last-level { font-style: normal; color: #555; } table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; } diff --git a/test/fixtures/time_entries.yml b/test/fixtures/time_entries.yml index 151077d2b..f6876c7b0 100644 --- a/test/fixtures/time_entries.yml +++ b/test/fixtures/time_entries.yml @@ -15,7 +15,7 @@ time_entries_001: tyear: 2007 time_entries_002: created_on: 2007-03-23 14:11:04 +01:00 - tweek: 12 + tweek: 11 tmonth: 3 project_id: 1 comments: "" diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index c55d5248c..6e3308c84 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -22,7 +22,7 @@ require 'timelog_controller' class TimelogController; def rescue_action(e) raise e end; end class TimelogControllerTest < Test::Unit::TestCase - fixtures :projects, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses + fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses def setup @controller = TimelogController.new @@ -86,7 +86,16 @@ class TimelogControllerTest < Test::Unit::TestCase assert_not_nil assigns(:total_hours) assert_equal "162.90", "%.2f" % assigns(:total_hours) end - + + def test_report_all_time_by_day + get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day' + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + assert_tag :tag => 'th', :content => '2007-03-12' + end + def test_report_one_criteria get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project'] assert_response :success @@ -122,8 +131,8 @@ class TimelogControllerTest < Test::Unit::TestCase assert_not_nil assigns(:total_hours) assert_equal "162.90", "%.2f" % assigns(:total_hours) # display all time by default - assert_nil assigns(:from) - assert_nil assigns(:to) + assert_equal '2007-03-11'.to_date, assigns(:from) + assert_equal '2007-04-22'.to_date, assigns(:to) end def test_details_at_project_level_with_date_range @@ -157,8 +166,8 @@ class TimelogControllerTest < Test::Unit::TestCase assert_not_nil assigns(:total_hours) assert_equal 154.25, assigns(:total_hours) # display all time by default - assert_nil assigns(:from) - assert_nil assigns(:to) + assert_equal '2007-03-11'.to_date, assigns(:from) + assert_equal '2007-04-22'.to_date, assigns(:to) end def test_details_csv_export From c6271d836168af455fcc9510907fb4fb330f1d62 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 5 Apr 2008 17:39:19 +0000 Subject: [PATCH 365/710] Fixed: inline image not displayed when including a wiki page (closes #1001). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1327 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/wiki_formatting/macros.rb | 2 +- test/fixtures/attachments.yml | 12 ++++++++++++ test/fixtures/wiki_contents.yml | 16 ++++++++++++++++ test/fixtures/wiki_pages.yml | 5 +++++ test/functional/wiki_controller_test.rb | 7 +++++-- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb index c0f2b222a..f27ea98b9 100644 --- a/lib/redmine/wiki_formatting/macros.rb +++ b/lib/redmine/wiki_formatting/macros.rb @@ -85,7 +85,7 @@ module Redmine @included_wiki_pages ||= [] raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) @included_wiki_pages << page.title - out = textilizable(page.content, :text) + out = textilizable(page.content, :text, :attachments => page.attachments) @included_wiki_pages.pop out else diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml index 764948755..509475c97 100644 --- a/test/fixtures/attachments.yml +++ b/test/fixtures/attachments.yml @@ -23,4 +23,16 @@ attachments_002: filesize: 28 filename: document.txt author_id: 2 +attachments_003: + created_on: 2006-07-19 21:07:27 +02:00 + downloads: 0 + content_type: image/gif + disk_filename: 060719210727_logo.gif + container_id: 4 + digest: b91e08d0cf966d5c6ff411bd8c4cc3a2 + id: 3 + container_type: WikiPage + filesize: 280 + filename: logo.gif + author_id: 2 \ No newline at end of file diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml index 6937dbd14..5d6d3f1de 100644 --- a/test/fixtures/wiki_contents.yml +++ b/test/fixtures/wiki_contents.yml @@ -15,6 +15,8 @@ wiki_contents_002: h1. Another page This is a link to a ticket: #2 + And this is an included page: + {{include(Page with an inline image)}} updated_on: 2007-03-08 00:18:07 +01:00 page_id: 2 id: 2 @@ -32,3 +34,17 @@ wiki_contents_003: version: 1 author_id: 1 comments: +wiki_contents_004: + text: |- + h1. Page with an inline image + + This is an inline image: + + !logo.gif! + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 4 + id: 4 + version: 1 + author_id: 1 + comments: + \ No newline at end of file diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml index ee260291d..f89832e44 100644 --- a/test/fixtures/wiki_pages.yml +++ b/test/fixtures/wiki_pages.yml @@ -14,4 +14,9 @@ wiki_pages_003: title: Start_page id: 3 wiki_id: 2 +wiki_pages_004: + created_on: 2007-03-08 00:18:07 +01:00 + title: Page_with_an_inline_image + id: 4 + wiki_id: 1 \ No newline at end of file diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index 0418a02b6..68e52600f 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -22,7 +22,7 @@ require 'wiki_controller' class WikiController; def rescue_action(e) raise e end; end class WikiControllerTest < Test::Unit::TestCase - fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions + fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments def setup @controller = WikiController.new @@ -43,6 +43,9 @@ class WikiControllerTest < Test::Unit::TestCase assert_response :success assert_template 'show' assert_tag :tag => 'h1', :content => /Another page/ + # Included page with an inline image + assert_tag :tag => 'p', :content => /This is an inline image/ + assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3' } end def test_show_unexistent_page_without_edit_right @@ -147,7 +150,7 @@ class WikiControllerTest < Test::Unit::TestCase assert_template 'special_page_index' pages = assigns(:pages) assert_not_nil pages - assert_equal 2, pages.size + assert_equal Project.find(1).wiki.pages.size, pages.size assert_tag :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, :content => /CookBook documentation/ end From c37abb64151e18461cb739ed4658d912a7c752e8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 5 Apr 2008 18:56:08 +0000 Subject: [PATCH 366/710] Inline images alt attribute set to the attachment description. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1328 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 6 ++++-- test/fixtures/attachments.yml | 1 + test/functional/wiki_controller_test.rb | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e30343ff5..eb8cc2795 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -207,8 +207,10 @@ module ApplicationHelper rf = Regexp.new(filename, Regexp::IGNORECASE) # search for the picture in attachments if found = attachments.detect { |att| att.filename =~ rf } - image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found.id - "!#{style}#{image_url}!" + image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found + desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") + alt = desc.blank? ? nil : "(#{desc})" + "!#{style}#{image_url}#{alt}!" else "!#{style}#{filename}!" end diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml index 509475c97..162d44720 100644 --- a/test/fixtures/attachments.yml +++ b/test/fixtures/attachments.yml @@ -34,5 +34,6 @@ attachments_003: container_type: WikiPage filesize: 280 filename: logo.gif + description: This is a logo author_id: 2 \ No newline at end of file diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index 68e52600f..bf31e6614 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -45,7 +45,8 @@ class WikiControllerTest < Test::Unit::TestCase assert_tag :tag => 'h1', :content => /Another page/ # Included page with an inline image assert_tag :tag => 'p', :content => /This is an inline image/ - assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3' } + assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3', + :alt => 'This is a logo' } end def test_show_unexistent_page_without_edit_right From 154f60edd353c2215b9e1622a2e47bdcc5f70101 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 10:35:55 +0000 Subject: [PATCH 367/710] Fix repository browsing at given revision for various scm and add tests for this. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1329 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 4 +- app/models/repository/cvs.rb | 3 +- app/models/repository/darcs.rb | 3 +- lib/redmine/scm/adapters/cvs_adapter.rb | 4 +- lib/redmine/scm/adapters/darcs_adapter.rb | 4 +- lib/redmine/scm/adapters/git_adapter.rb | 2 +- .../repositories_bazaar_controller_test.rb | 129 ++++++++++++++++++ .../repositories_cvs_controller_test.rb | 17 ++- .../repositories_darcs_controller_test.rb | 11 +- .../repositories_git_controller_test.rb | 12 +- .../repositories_mercurial_controller_test.rb | 12 +- ...repositories_subversion_controller_test.rb | 13 +- 12 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 test/functional/repositories_bazaar_controller_test.rb diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 9b59b51ec..79fb49c86 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -51,8 +51,8 @@ class RepositoriesController < ApplicationController def show # check if new revisions have been committed in the repository @repository.fetch_changesets if Setting.autofetch_changesets? - # get entries for the browse frame - @entries = @repository.entries('') + # root entries + @entries = @repository.entries('', @rev) # latest changesets @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") show_error_not_found unless @entries || @changesets.any? diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb index a78b60806..7c01a27ee 100644 --- a/app/models/repository/cvs.rb +++ b/app/models/repository/cvs.rb @@ -35,7 +35,8 @@ class Repository::Cvs < Repository end def entries(path=nil, identifier=nil) - entries=scm.entries(path, identifier) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + entries = scm.entries(path, rev.nil? ? nil : rev.committed_on) if entries entries.each() do |entry| unless entry.lastrev.nil? || entry.lastrev.identifier diff --git a/app/models/repository/darcs.rb b/app/models/repository/darcs.rb index cc608d370..c7c14a397 100644 --- a/app/models/repository/darcs.rb +++ b/app/models/repository/darcs.rb @@ -29,7 +29,8 @@ class Repository::Darcs < Repository end def entries(path=nil, identifier=nil) - entries=scm.entries(path, identifier) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + entries = scm.entries(path, patch.nil? ? nil : patch.scmid) if entries entries.each do |entry| # Search the DB for the entry's last change diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index c0f60c02a..6085bfdbe 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -72,7 +72,9 @@ module Redmine logger.debug " entries '#{path}' with identifier '#{identifier}'" path_with_project="#{url}#{with_leading_slash(path)}" entries = Entries.new - cmd = "#{CVS_BIN} -d #{root_url} rls -ed #{path_with_project}" + cmd = "#{CVS_BIN} -d #{root_url} rls -ed" + cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier + cmd << " #{path_with_project}" shellout(cmd) do |io| io.each_line(){|line| fields=line.chop.split('/',-1) diff --git a/lib/redmine/scm/adapters/darcs_adapter.rb b/lib/redmine/scm/adapters/darcs_adapter.rb index cd8610121..660b6cf8f 100644 --- a/lib/redmine/scm/adapters/darcs_adapter.rb +++ b/lib/redmine/scm/adapters/darcs_adapter.rb @@ -53,7 +53,9 @@ module Redmine path_prefix = (path.blank? ? '' : "#{path}/") path = '.' if path.blank? entries = Entries.new - cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output #{path}" + cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output" + cmd << " --match \"hash #{identifier}\"" if identifier + cmd << " #{path}" shellout(cmd) do |io| begin doc = REXML::Document.new(io) diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index f1d076360..5d315b0cc 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -79,7 +79,7 @@ module Redmine rev = Revision.new({:identifier => changeset[:commit], :scmid => changeset[:commit], :author => changeset[:author], - :time => Time.parse(changeset[:date]), + :time => (changeset[:date] ? Time.parse(changeset[:date]) : nil), :message => changeset[:description], :paths => files }) diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb new file mode 100644 index 000000000..5a473bab3 --- /dev/null +++ b/test/functional/repositories_bazaar_controller_test.rb @@ -0,0 +1,129 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'repositories_controller' + +# Re-raise errors caught by the controller. +class RepositoriesController; def rescue_action(e) raise e end; end + +class RepositoriesBazaarControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules + + # No '..' in the repository path + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository' + + def setup + @controller = RepositoriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + Repository::Bazaar.create(:project => Project.find(3), :url => REPOSITORY_PATH) + end + + if File.directory?(REPOSITORY_PATH) + def test_show + get :show, :id => 3 + assert_response :success + assert_template 'show' + assert_not_nil assigns(:entries) + assert_not_nil assigns(:changesets) + end + + def test_browse_root + get :browse, :id => 3 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal 2, assigns(:entries).size + assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'} + assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'} + end + + def test_browse_directory + get :browse, :id => 3, :path => ['directory'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry + assert_equal 'file', entry.kind + assert_equal 'directory/edit.png', entry.path + end + + def test_browse_at_given_revision + get :browse, :id => 3, :path => [], :rev => 3 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'], assigns(:entries).collect(&:name) + end + + def test_changes + get :changes, :id => 3, :path => ['doc-mkdir.txt'] + assert_response :success + assert_template 'changes' + assert_tag :tag => 'h2', :content => 'doc-mkdir.txt' + end + + def test_entry_show + get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'] + assert_response :success + assert_template 'entry' + # Line 19 + assert_tag :tag => 'th', + :content => /29/, + :attributes => { :class => /line-num/ }, + :sibling => { :tag => 'td', :content => /Show help message/ } + end + + def test_entry_download + get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'], :format => 'raw' + assert_response :success + # File content + assert @response.body.include?('Show help message') + end + + def test_diff + # Full diff of changeset 3 + get :diff, :id => 3, :rev => 3 + assert_response :success + assert_template 'diff' + # Line 22 removed + assert_tag :tag => 'th', + :content => /2/, + :sibling => { :tag => 'td', + :attributes => { :class => /diff_in/ }, + :content => /Main purpose/ } + end + + def test_annotate + get :annotate, :id => 3, :path => ['doc-mkdir.txt'] + assert_response :success + assert_template 'annotate' + # Line 2, revision 3 + assert_tag :tag => 'th', :content => /2/, + :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /3/ } }, + :sibling => { :tag => 'td', :content => /jsmith/ }, + :sibling => { :tag => 'td', :content => /Main purpose/ } + end + else + puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!" + def test_fake; assert true end + end +end diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb index 1e101f59a..d6181ad36 100644 --- a/test/functional/repositories_cvs_controller_test.rb +++ b/test/functional/repositories_cvs_controller_test.rb @@ -65,13 +65,24 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase end def test_browse_directory - get :browse, :id => 1, :path => ['sources'] + get :browse, :id => 1, :path => ['images'] assert_response :success assert_template 'browse' assert_not_nil assigns(:entries) - entry = assigns(:entries).detect {|e| e.name == 'watchers_controller.rb'} + assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name) + entry = assigns(:entries).detect {|e| e.name == 'edit.png'} + assert_not_nil entry assert_equal 'file', entry.kind - assert_equal 'sources/watchers_controller.rb', entry.path + assert_equal 'images/edit.png', entry.path + end + + def test_browse_at_given_revision + Project.find(1).repository.fetch_changesets + get :browse, :id => 1, :path => ['images'], :rev => 1 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) end def test_entry diff --git a/test/functional/repositories_darcs_controller_test.rb b/test/functional/repositories_darcs_controller_test.rb index fc77b8747..43c715924 100644 --- a/test/functional/repositories_darcs_controller_test.rb +++ b/test/functional/repositories_darcs_controller_test.rb @@ -60,13 +60,22 @@ class RepositoriesDarcsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'browse' assert_not_nil assigns(:entries) - assert_equal 2, assigns(:entries).size + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) entry = assigns(:entries).detect {|e| e.name == 'edit.png'} assert_not_nil entry assert_equal 'file', entry.kind assert_equal 'images/edit.png', entry.path end + def test_browse_at_given_revision + Project.find(3).repository.fetch_changesets + get :browse, :id => 3, :path => ['images'], :rev => 1 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + end + def test_changes get :changes, :id => 3, :path => ['images', 'edit.png'] assert_response :success diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb index f8b3cb2bb..10a4950f3 100644 --- a/test/functional/repositories_git_controller_test.rb +++ b/test/functional/repositories_git_controller_test.rb @@ -1,5 +1,5 @@ # redMine - project management software -# Copyright (C) 2006-2007 Jean-Philippe Lang +# 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 @@ -61,13 +61,21 @@ class RepositoriesGitControllerTest < Test::Unit::TestCase assert_response :success assert_template 'browse' assert_not_nil assigns(:entries) - assert_equal 2, assigns(:entries).size + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) entry = assigns(:entries).detect {|e| e.name == 'edit.png'} assert_not_nil entry assert_equal 'file', entry.kind assert_equal 'images/edit.png', entry.path end + def test_browse_at_given_revision + get :browse, :id => 3, :path => ['images'], :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518' + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + end + def test_changes get :changes, :id => 3, :path => ['images', 'edit.png'] assert_response :success diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb index 736e38c83..b09265d13 100644 --- a/test/functional/repositories_mercurial_controller_test.rb +++ b/test/functional/repositories_mercurial_controller_test.rb @@ -1,5 +1,5 @@ # redMine - project management software -# Copyright (C) 2006-2007 Jean-Philippe Lang +# 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 @@ -60,13 +60,21 @@ class RepositoriesMercurialControllerTest < Test::Unit::TestCase assert_response :success assert_template 'browse' assert_not_nil assigns(:entries) - assert_equal 2, assigns(:entries).size + assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name) entry = assigns(:entries).detect {|e| e.name == 'edit.png'} assert_not_nil entry assert_equal 'file', entry.kind assert_equal 'images/edit.png', entry.path end + def test_browse_at_given_revision + get :browse, :id => 3, :path => ['images'], :rev => 0 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['delete.png'], assigns(:entries).collect(&:name) + end + def test_changes get :changes, :id => 3, :path => ['images', 'edit.png'] assert_response :success diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index 9b21a13e8..adb69c8e9 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -1,5 +1,5 @@ # redMine - project management software -# Copyright (C) 2006-2007 Jean-Philippe Lang +# 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 @@ -58,11 +58,20 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_response :success assert_template 'browse' assert_not_nil assigns(:entries) + assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name) entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'} assert_equal 'file', entry.kind assert_equal 'subversion_test/helloworld.c', entry.path end - + + def test_browse_at_given_revision + get :browse, :id => 1, :path => ['subversion_test'], :rev => 4 + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entries) + assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name) + end + def test_entry get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c'] assert_response :success From fe74ab8c0644369975be2d2a5c70c4aefd877b9c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 10:53:18 +0000 Subject: [PATCH 368/710] Make the project files list sortable (#997). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1330 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 5 ++++- app/views/projects/list_files.rhtml | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 199b2f0c5..afd7f6569 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -204,7 +204,10 @@ class ProjectsController < ApplicationController end def list_files - @versions = @project.versions.sort.reverse + sort_init "#{Attachment.table_name}.filename", "asc" + sort_update + @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse + render :layout => !request.xhr? end # Show changelog for @project diff --git a/app/views/projects/list_files.rhtml b/app/views/projects/list_files.rhtml index ec4a3619b..f385229ae 100644 --- a/app/views/projects/list_files.rhtml +++ b/app/views/projects/list_files.rhtml @@ -9,10 +9,10 @@ - - - - + <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %> + <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %> + <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %> + <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %> <% if delete_allowed %><% end %> From a7ca01a8de609cb1b0453d615ee0a90c3da1192d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 11:07:16 +0000 Subject: [PATCH 369/710] =?UTF-8?q?Translation=20updates:=20*=20Norwegian?= =?UTF-8?q?=20(Kai=20Olav=20Fredriksen)=20*=20German=20(Thomas=20L=C3=B6be?= =?UTF-8?q?r)=20*=20Bulgarian=20(Nikolay=20Solakov)=20*=20Hebrew=20(Ficoos?= =?UTF-8?q?=20Bangaly)=20*=20Russian=20(Michael=20Pirogov)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1331 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/bg.yml | 164 +++++++++++++++++++++--------------------- lang/de.yml | 44 ++++++------ lang/he.yml | 200 ++++++++++++++++++++++++++-------------------------- lang/no.yml | 14 ++-- lang/ru.yml | 4 +- 5 files changed, 213 insertions(+), 213 deletions(-) diff --git a/lang/bg.yml b/lang/bg.yml index a49365d20..d1a60fe43 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -48,8 +48,8 @@ general_text_no: 'не' general_text_yes: 'да' general_lang_name: 'Bulgarian' general_csv_separator: ',' -general_csv_encoding: cp1251 -general_pdf_encoding: cp1251 +general_csv_encoding: UTF-8 +general_pdf_encoding: UTF-8 general_day_names: Понеделник,Вторник,Сряда,Четвъртък,Петък,Събота,Неделя general_first_day_of_week: '1' @@ -57,11 +57,11 @@ 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_register_done: Профилът е създаден успешно. +notice_account_unknown_email: Непознат e-mail. +notice_can_t_change_password: Този профил е с външен метод за оторизация. Невъзможна смяна на паролата. notice_account_lost_email_sent: Изпратен ви е e-mail с инструкции за избор на нова парола. -notice_account_activated: Акаунтът ви е активиран. Вече може да влезете. +notice_account_activated: Профилът ви е активиран. Вече може да влезете в системата. notice_successful_create: Успешно създаване. notice_successful_update: Успешно обновяване. notice_successful_delete: Успешно изтриване. @@ -78,8 +78,8 @@ error_scm_command_failed: "Грешка при опит за комуникац mail_subject_lost_password: Вашата парола (%s) mail_body_lost_password: 'За да смените паролата си, използвайте следния линк:' -mail_subject_register: Активация на акаунт (%s) -mail_body_register: 'За да активирате акаунта си използвайте следния линк:' +mail_subject_register: Активация на профил (%s) +mail_body_register: 'За да активирате профила си използвайте следния линк:' gui_validation_error: 1 грешка gui_validation_error_plural: %d грешки @@ -113,11 +113,11 @@ field_notes: Бележка field_is_closed: Затворена задача field_is_default: Статус по подразбиране field_tracker: Тракер -field_subject: Тема +field_subject: Относно field_due_date: Крайна дата field_assigned_to: Възложена на field_priority: Приоритет -field_fixed_version: Target version +field_fixed_version: Планувана версия field_user: Потребител field_role: Роля field_homepage: Начална страница @@ -138,7 +138,7 @@ field_version: Версия field_type: Тип field_host: Хост field_port: Порт -field_account: Акаунт +field_account: Профил field_base_dn: Base DN field_attr_login: Login attribute field_attr_firstname: Firstname attribute @@ -163,7 +163,7 @@ field_delay: Отместване field_assignable: Възможно е възлагане на задачи за тази роля field_redirect_existing_links: Пренасочване на съществуващи линкове field_estimated_hours: Изчислено време -field_default_value: Статус по подразбиране +field_default_value: Стойност по подразбиране setting_app_title: Заглавие setting_app_subtitle: Описание @@ -171,15 +171,15 @@ setting_welcome_text: Допълнителен текст setting_default_language: Език по подразбиране setting_login_required: Изискване за вход в системата setting_self_registration: Регистрация от потребители -setting_attachment_max_size: Максимално голям приложен файл +setting_attachment_max_size: Максимална големина на прикачен файл setting_issues_export_limit: Лимит за експорт на задачи setting_mail_from: E-mail адрес за емисии setting_host_name: Хост setting_text_formatting: Форматиране на текста setting_wiki_compression: Wiki компресиране на историята setting_feeds_limit: Лимит на Feeds -setting_autofetch_changesets: Автоматично обработване на commits в хранилището -setting_sys_api_enabled: Разрешаване на WS за управление на хранилището +setting_autofetch_changesets: Автоматично обработване на ревизиите +setting_sys_api_enabled: Разрешаване на WS за управление setting_commit_ref_keywords: Отбелязващи ключови думи setting_commit_fix_keywords: Приключващи ключови думи setting_autologin: Автоматичен вход @@ -231,7 +231,7 @@ label_password_lost: Забравена парола label_home: Начало label_my_page: Лична страница label_my_account: Профил -label_my_projects: Моите проекти +label_my_projects: Проекти, в които участвам label_administration: Администрация label_login: Вход label_logout: Изход @@ -375,8 +375,8 @@ label_f_hour_plural: %.2f часа label_time_tracking: Отделяне на време label_change_plural: Промени label_statistics: Статистики -label_commits_per_month: Commits за месец -label_commits_per_author: Commits за автор +label_commits_per_month: Ревизии по месеци +label_commits_per_author: Ревизии по автор label_view_diff: Виж разликите label_diff_inline: хоризонтално label_diff_side_by_side: вертикално @@ -389,7 +389,7 @@ label_applied_status: Промени статуса на label_loading: Зареждане... label_relation_new: Нова релация label_relation_delete: Изтриване на релация -label_relates_to: Свързана със +label_relates_to: свързана със label_duplicates: дублира label_blocks: блокира label_blocked_by: блокирана от @@ -427,10 +427,10 @@ label_updated_time: Обновена преди %s label_jump_to_a_project: Проект... button_login: Вход -button_submit: Приложи +button_submit: Прикачване button_save: Запис -button_check_all: Маркирай всички -button_uncheck_all: Изчисти всички +button_check_all: Избор на всички +button_uncheck_all: Изчистване на всички button_delete: Изтриване button_create: Създаване button_test: Тест @@ -481,9 +481,9 @@ text_length_between: От %d до %d символа. text_tracker_no_workflow: Няма дефиниран работен процес за този тракер text_unallowed_characters: Непозволени символи text_comma_separated: Позволено е изброяване (с разделител запетая). -text_issues_ref_in_commit_messages: Отбелязване и приключване на задачи от commit съобщения -text_issue_added: Публикувана е нова задача с номер %s (by %s). -text_issue_updated: Задача %s е обновена (by %s). +text_issues_ref_in_commit_messages: Отбелязване и приключване на задачи от ревизии +text_issue_added: Публикувана е нова задача с номер %s (от %s). +text_issue_updated: Задача %s е обновена (от %s). text_wiki_destroy_confirmation: Сигурни ли сте, че искате да изтриете това Wiki и цялото му съдържание? text_issue_category_destroy_question: Има задачи (%d) обвързани с тази категория. Какво ще изберете? text_issue_category_destroy_assignments: Премахване на връзките с категорията @@ -515,11 +515,11 @@ enumeration_issue_priorities: Приоритети на задачи enumeration_doc_categories: Категории документи enumeration_activities: Дейности (time tracking) label_file_plural: Файлове -label_changeset_plural: Changesets +label_changeset_plural: Ревизии field_column_names: Колони label_default_columns: По подразбиране setting_issue_list_default_columns: Показвани колони по подразбиране -setting_repositories_encodings: Кодови таблици на хранилищата +setting_repositories_encodings: Кодови таблици notice_no_issue_selected: "Няма избрани задачи." label_bulk_edit_selected_issues: Редактиране на задачи label_no_change_option: (Без промяна) @@ -536,20 +536,20 @@ label_user_mail_option_none: "Само за наблюдавани или в к setting_emails_footer: Подтекст за e-mail label_float: Дробно button_copy: Копиране -mail_body_account_information_external: Можете да използвате вашия "%s" акаунт за вход. -mail_body_account_information: Информацията за акаунта +mail_body_account_information_external: Можете да използвате вашия "%s" профил за вход. +mail_body_account_information: Информацията за профила ви setting_protocol: Протокол label_user_mail_no_self_notified: "Не искам известия за извършени от мен промени" setting_time_format: Формат на часа -label_registration_activation_by_email: активиране на акаунта по email -mail_subject_account_activation_request: Заявка за активиране на акаунт в %s +label_registration_activation_by_email: активиране на профила по email +mail_subject_account_activation_request: Заявка за активиране на профил в %s mail_body_account_activation_request: 'Има новорегистриран потребител (%s), очакващ вашето одобрение:' label_registration_automatic_activation: автоматично активиране label_registration_manual_activation: ръчно активиране -notice_account_pending: "Акаунтът Ви е създаден и очаква одобрение от администратор." +notice_account_pending: "Профилът Ви е създаден и очаква одобрение от администратор." field_time_zone: Часова зона text_caracters_minimum: Минимум %d символа. -setting_bcc_recipients: Blind carbon copy (bcc) получатели +setting_bcc_recipients: Получатели на скрито копие (bcc) button_annotate: Анотация label_issues_by: Задачи по %s field_searchable: С възможност за търсене @@ -566,54 +566,54 @@ label_general: Основни label_repository_plural: Хранилища label_associated_revisions: Асоциирани ревизии setting_user_format: Потребителски формат -text_status_changed_by_changeset: Applied in changeset %s. -label_more: More -text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' -label_scm: SCM -text_select_project_modules: 'Select modules to enable for this project:' -label_issue_added: Issue added -label_issue_updated: Issue updated -label_document_added: Document added -label_message_posted: Message added -label_file_added: File added -label_news_added: News added -project_module_boards: Boards -project_module_issue_tracking: Issue tracking +text_status_changed_by_changeset: Приложено с ревизия %s. +label_more: Още +text_issues_destroy_confirmation: 'Сигурни ли сте, че искате да изтриете избраните задачи?' +label_scm: SCM (Система за контрол на кода) +text_select_project_modules: 'Изберете активните модули за този проект:' +label_issue_added: Добавена задача +label_issue_updated: Обновена задача +label_document_added: Добавен документ +label_message_posted: Добавено съобщение +label_file_added: Добавен файл +label_news_added: Добавена новина +project_module_boards: Форуми +project_module_issue_tracking: Тракинг project_module_wiki: Wiki -project_module_files: Files -project_module_documents: Documents -project_module_repository: Repository -project_module_news: News -project_module_time_tracking: Time tracking -text_file_repository_writable: File repository writable -text_default_administrator_account_changed: Default administrator account changed -text_rmagick_available: RMagick available (optional) -button_configure: Configure -label_plugins: Plugins -label_ldap_authentication: LDAP authentication +project_module_files: Файлове +project_module_documents: Документи +project_module_repository: Хранилище +project_module_news: Новини +project_module_time_tracking: Отделяне на време +text_file_repository_writable: Възможност за писане в хранилището с файлове +text_default_administrator_account_changed: Сменен фабричния администраторски профил +text_rmagick_available: Наличен RMagick (по избор) +button_configure: Конфигуриране +label_plugins: Плъгини +label_ldap_authentication: LDAP оторизация label_downloads_abbr: D/L -label_this_month: 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 +label_this_month: текущия месец +label_last_n_days: последните %d дни +label_all_time: всички +label_this_year: текущата година +label_date_range: Период +label_last_week: последната седмица +label_yesterday: вчера +label_last_month: последния месец +label_add_another_file: Добавяне на друг файл +label_optional_description: Незадължително описание +text_destroy_time_entries_question: %.02f часа са отделени на задачите, които искате да изтриете. Какво избирате? +error_issue_not_found_in_project: 'Задачата не е намерена или не принадлежи на този проект' +text_assign_time_entries_to_project: Прехвърляне на отделеното време към проект +text_destroy_time_entries: Изтриване на отделеното време +text_reassign_time_entries: 'Прехвърляне на отделеното време към задача:' +setting_activity_days_default: Брой дни показвани на таб Дейност +label_chronological_order: Хронологичен ред +field_comments_sorting: Сортиране на коментарите +label_reverse_chronological_order: Обратен хронологичен ред +label_preferences: Предпочитания +setting_display_subprojects_issues: Показване на подпроектите в проектите по подразбиране +label_overall_activity: Цялостна дейност +setting_default_projects_public: Новите проекти са публични по подразбиране +error_scm_annotate: "Обектът не съществува или не може да бъде анотиран." +label_planning: Планиране diff --git a/lang/de.yml b/lang/de.yml index 17c6ac617..323859ae4 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -71,7 +71,7 @@ notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert. notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen. notice_email_sent: Eine E-Mail wurde an %s gesendet. notice_email_error: Beim Senden einer E-Mail ist ein Fehler aufgetreten (%s). -notice_feeds_access_key_reseted: Ihr RSS-Zugriffsschlüssel wurde zurückgesetzt. +notice_feeds_access_key_reseted: Ihr Atom-Zugriffsschlüssel wurde zurückgesetzt. notice_failed_to_save_issues: "%d von %d ausgewählten Tickets konnte(n) nicht gespeichert werden: %s." notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten." notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators." @@ -80,6 +80,7 @@ notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %s" error_scm_not_found: Eintrag und/oder Revision besteht nicht im Projektarchiv. error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %s" +error_scm_annotate: "Der Eintrag existiert nicht oder kann nicht annotiert werden." error_issue_not_found_in_project: 'Das Ticket wurde nicht gefunden oder gehört nicht zu diesem Projekt.' mail_subject_lost_password: Ihr %s Kennwort @@ -120,8 +121,8 @@ field_project: Projekt field_issue: Ticket field_status: Status field_notes: Kommentare -field_is_closed: Problem erledigt -field_is_default: Default +field_is_closed: Ticket geschlossen +field_is_default: Standardeinstellung field_tracker: Tracker field_subject: Thema field_due_date: Abgabedatum @@ -133,8 +134,8 @@ field_role: Rolle field_homepage: Projekt-Homepage field_is_public: Öffentlich field_parent: Unterprojekt von -field_is_in_chlog: Ansicht im Change-Log -field_is_in_roadmap: Ansicht in der Roadmap +field_is_in_chlog: Im Change-Log anzeigen +field_is_in_roadmap: In der Roadmap anzeigen field_login: Mitgliedsname field_mail_notification: Mailbenachrichtigung field_admin: Administrator @@ -177,6 +178,7 @@ field_column_names: Spalten field_time_zone: Zeitzone field_searchable: Durchsuchbar field_default_value: Standardwert +field_comments_sorting: Kommentare anzeigen setting_app_title: Applikations-Titel setting_app_subtitle: Applikations-Untertitel @@ -191,7 +193,8 @@ setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden setting_host_name: Hostname setting_text_formatting: Textformatierung setting_wiki_compression: Wiki-Historie komprimieren -setting_feeds_limit: Feed-Inhalt begrenzen +setting_feeds_limit: Max. Anzahl Einträge pro Atom-Feed +setting_default_projects_public: Neue Projekte sind standardmäßig öffentlich setting_autofetch_changesets: Changesets automatisch abrufen setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen setting_commit_ref_keywords: Schlüsselwörter (Beziehungen) @@ -206,6 +209,8 @@ setting_emails_footer: E-Mail-Fußzeile setting_protocol: Protokoll setting_per_page_options: Objekte pro Seite setting_user_format: Benutzer-Anzeigeformat +setting_activity_days_default: Anzahl Tage pro Seite der Projekt-Aktivität +setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen project_module_issue_tracking: Ticket-Verfolgung project_module_time_tracking: Zeiterfassung @@ -227,7 +232,7 @@ label_project_latest: Neueste Projekte label_issue: Ticket label_issue_new: Neues Ticket label_issue_plural: Tickets -label_issue_view_all: Alle Tickets ansehen +label_issue_view_all: Alle Tickets anzeigen label_issues_by: Tickets von %s label_issue_added: Ticket hinzugefügt label_issue_updated: Ticket aktualisiert @@ -277,6 +282,7 @@ label_last_updates: zuletzt aktualisiert label_last_updates_plural: %d zuletzt aktualisierten label_registered_on: Angemeldet am label_activity: Aktivität +label_overall_activity: Aktivität aller Projekte anzeigen label_new: Neu label_logged_as: Angemeldet als label_environment: Environment @@ -320,7 +326,7 @@ label_version: Version label_version_new: Neue Version label_version_plural: Versionen label_confirmation: Bestätigung -label_export_to: Export zu +label_export_to: "Auch abrufbar als:" label_read: Lesen... label_public_projects: Öffentliche Projekte label_open_issues: offen @@ -345,7 +351,7 @@ label_months_from: Monate ab label_gantt: Gantt label_internal: Intern label_last_changes: %d letzte Änderungen -label_change_view_all: Alle Änderungen ansehen +label_change_view_all: Alle Änderungen anzeigen label_personalize_page: Diese Seite anpassen label_comment: Kommentar label_comment_plural: Kommentare @@ -469,7 +475,7 @@ label_date_to: Bis label_language_based: Sprachabhängig label_sort_by: Sortiert nach %s label_send_test_email: Test-E-Mail senden -label_feeds_access_key_created_on: RSS-Zugriffsschlüssel vor %s erstellt +label_feeds_access_key_created_on: Atom-Zugriffsschlüssel vor %s erstellt label_module_plural: Module label_added_time_by: Von %s vor %s hinzugefügt label_updated_time: Vor %s aktualisiert @@ -500,6 +506,10 @@ label_ldap_authentication: LDAP-Authentifizierung label_downloads_abbr: D/L label_optional_description: Beschreibung (optional) label_add_another_file: Eine weitere Datei hinzufügen +label_preferences: Präferenzen +label_chronological_order: in zeitlicher Reihenfolge +label_reverse_chronological_order: in umgekehrter zeitlicher Reihenfolge +label_planning: Terminplanung button_login: Anmelden button_submit: OK @@ -518,7 +528,7 @@ button_lock: Sperren button_unlock: Entsperren button_download: Download button_list: Liste -button_view: Ansehen +button_view: Anzeigen button_move: Verschieben button_back: Zurück button_cancel: Abbrechen @@ -535,7 +545,7 @@ button_reset: Zurücksetzen button_rename: Umbenennen button_change_password: Kennwort ändern button_copy: Kopieren -button_annotate: Mit Anmerkungen versehen +button_annotate: Annotieren button_update: Aktualisieren button_configure: Konfigurieren @@ -608,13 +618,3 @@ default_activity_development: Entwicklung enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) -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 diff --git a/lang/he.yml b/lang/he.yml index 018281581..0cde264f7 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -76,7 +76,7 @@ notice_failed_to_save_issues: "נכשרת בשמירת %d נושא\ים ב %d נ notice_no_issue_selected: "לא נבחר אף נושא! בחר בבקשה את הנושאים שברצונך לערוך." error_scm_not_found: כניסה ו\או גירסא אינם קיימים במאגר. -error_scm_command_failed: "An error occurred when trying to access the repository: %s" +error_scm_command_failed: "ארעה שגיאה בעת ניסון גישה למאגר: %s" mail_subject_lost_password: סיסמת ה-%s שלך mail_body_lost_password: 'לשינו סיסמת ה-Redmine שלך,לחץ על הקישור הבא:' @@ -98,7 +98,7 @@ field_filesize: גודל field_downloads: הורדות field_author: כותב field_created_on: נוצר -field_updated_on: עודגן +field_updated_on: עודכן field_field_format: פורמט field_is_for_all: לכל הפרויקטים field_possible_values: ערכים אפשריים @@ -119,7 +119,7 @@ field_subject: שם נושא field_due_date: תאריך סיום field_assigned_to: מוצב ל field_priority: עדיפות -field_fixed_version: Target version +field_fixed_version: גירסאת יעד field_user: מתשמש field_role: תפקיד field_homepage: דף הבית @@ -140,7 +140,7 @@ field_version: גירסא field_type: סוג field_host: שרת field_port: פורט -field_account: חשבום +field_account: חשבון field_base_dn: בסיס DN field_attr_login: תכונת התחברות field_attr_firstname: תכונת שם פרטים @@ -182,7 +182,7 @@ setting_text_formatting: עיצוב טקסט setting_wiki_compression: כיווץ היסטורית WIKI setting_feeds_limit: גבול תוכן הזנות setting_autofetch_changesets: משיכה אוטומתי של עידכונים -setting_sys_api_enabled: Enable WS for repository management +setting_sys_api_enabled: אפשר WS לניהול המאגר setting_commit_ref_keywords: מילות מפתח מקשרות setting_commit_fix_keywords: מילות מפתח מתקנות setting_autologin: חיבור אוטומטי @@ -233,7 +233,7 @@ label_information_plural: מידע label_please_login: התחבר בבקשה label_register: הרשמה label_password_lost: אבדה הסיסמה? -label_home: דך הבית +label_home: דף הבית label_my_page: הדף שלי label_my_account: השבון שלי label_my_projects: הפרויקטים שלי @@ -259,7 +259,7 @@ label_subproject_plural: תת-פרויקטים label_min_max_length: אורך מינימאלי - מקסימאלי label_list: רשימה label_date: תאריך -label_integer: מספר שלים +label_integer: מספר שלם label_boolean: ערך בוליאני label_string: טקסט label_text: טקסט ארוך @@ -269,7 +269,7 @@ label_download: הורדה %d label_download_plural: %d הורדות label_no_data: אין מידע להציג label_change_status: שנה מצב -label_history: הידטוריה +label_history: היסטוריה label_attachment: קובץ label_attachment_new: קובץ חדש label_attachment_delete: מחק קובץ @@ -279,7 +279,7 @@ label_report_plural: דו"חות label_news: חדשות label_news_new: הוסף חדשות label_news_plural: חדשות -label_news_latest: חדשות חדשות +label_news_latest: חדשות אחרונות label_news_view_all: צפה בכל החדשות label_change_log: דו"ח שינויים label_settings: הגדרות @@ -419,7 +419,7 @@ label_reply_plural: השבות label_send_information: שלח מידע על חשבון למשתמש label_year: שנה label_month: חודש -label_week: שבו +label_week: שבוע label_date_from: מאת label_date_to: אל label_language_based: מבוסס שפה @@ -444,7 +444,7 @@ button_save: שמור button_check_all: בחר הכל button_uncheck_all: בחר כלום button_delete: מחק -button_create: צוק +button_create: צור button_test: בדוק button_edit: ערוך button_add: הוסף @@ -454,13 +454,13 @@ button_clear: נקה button_lock: נעל button_unlock: בטל נעילה button_download: הורד -button_list: קשימה +button_list: רשימה button_view: צפה button_move: הזז button_back: הקודם button_cancel: בטח button_activate: הפעל -button_sort: מין +button_sort: מיין button_log_time: זמן לוג button_rollback: חזור לגירסא זו button_watch: צפה @@ -526,94 +526,94 @@ default_activity_development: פיתוח enumeration_issue_priorities: עדיפות נושאים enumeration_doc_categories: קטגוריות מסמכים enumeration_activities: פעילויות (מעקב אחר זמנים) -label_search_titles_only: Search titles only -label_nobody: nobody -button_change_password: Change password -text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)." -label_user_mail_option_selected: "For any event on the selected projects only..." -label_user_mail_option_all: "For any event on all my projects" -label_user_mail_option_none: "Only for things I watch or I'm involved in" -setting_emails_footer: Emails footer -label_float: Float -button_copy: Copy -mail_body_account_information_external: You can use your "%s" account to log in. -mail_body_account_information: Your account information -setting_protocol: Protocol -label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" -setting_time_format: Time format -label_registration_activation_by_email: account activation by email -mail_subject_account_activation_request: %s account activation request -mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:' -label_registration_automatic_activation: automatic account activation -label_registration_manual_activation: manual account activation -notice_account_pending: "Your account was created and is now pending administrator approval." -field_time_zone: Time zone -text_caracters_minimum: Must be at least %d characters long. -setting_bcc_recipients: Blind carbon copy recipients (bcc) -button_annotate: Annotate -label_issues_by: Issues by %s -field_searchable: Searchable -label_display_per_page: 'Per page: %s' -setting_per_page_options: Objects per page options -label_age: Age -notice_default_data_loaded: Default configuration successfully loaded. -text_load_default_configuration: Load the default configuration -text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded." -error_can_t_load_default_data: "Default configuration could not be loaded: %s" -button_update: Update -label_change_properties: Change properties -label_general: General -label_repository_plural: Repositories -label_associated_revisions: Associated revisions -setting_user_format: Users display format -text_status_changed_by_changeset: Applied in changeset %s. -label_more: More -text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' +label_search_titles_only: חפש בכותרות בלבד +label_nobody: אף אחד +button_change_password: שנה סיסמא +text_user_mail_option: "בפרויקטים שלא בחרת, אתה רק תקבל התרעות על שאתה צופה או קשור אליהם (לדוגמא:נושאים שאתה היוצר שלהם או מוצבים אליך)." +label_user_mail_option_selected: "לכל אירוע בפרויקטים שבחרתי בלבד..." +label_user_mail_option_all: "לכל אירוע בכל הפרויקטים שלי" +label_user_mail_option_none: "רק לנושאים שאני צופה או קשור אליהם" +setting_emails_footer: תחתית דוא"ל +label_float: צף +button_copy: העתק +mail_body_account_information_external: אתה יכול להשתמש בחשבון "%s" כדי להתחבר +mail_body_account_information: פרטי החשבון שלך +setting_protocol: פרוטוקול +label_user_mail_no_self_notified: "אני לא רוצה שיודיעו לי על שינויים שאני מבצע" +setting_time_format: פורמט זמן +label_registration_activation_by_email: הפעל חשבון באמצעות דוא"ל +mail_subject_account_activation_request: בקשת הפעלה לחשבון %s +mail_body_account_activation_request: 'משתמש חדש (%s) נרשם. החשבון שלו מחכה לאישור שלך:' +label_registration_automatic_activation: הפעלת חשבון אוטומטית +label_registration_manual_activation: הפעלת חשבון ידנית +notice_account_pending: "החשבון שלך נוצר ועתה מחכה לאישור מנהל המערכת." +field_time_zone: איזור זמן +text_caracters_minimum: חייב להיות לפחות באורך של %d תווים. +setting_bcc_recipients: מוסתר (bcc) +button_annotate: הוסף תיאור מסגרת +label_issues_by: נושאים של %s +field_searchable: ניתן לחיפוש +label_display_per_page: 'לכל דף: %s' +setting_per_page_options: אפשרויות אוביקטים לפי דף +label_age: גיל +notice_default_data_loaded: אפשרויות ברירת מחדל מופעלות. +text_load_default_configuration: טען את אפשרויות ברירת המחדל +text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. יהיה באפשרותך לשנותו לאחר שיטען." +error_can_t_load_default_data: "אפשרויות ברירת המחדל לא הצליחו להיטען: %s" +button_update: עדכן +label_change_properties: שנה מאפיינים +label_general: כללי +label_repository_plural: מאגרים +label_associated_revisions: שינויים קשורים +setting_user_format: פורמט הצגת משתמשים +text_status_changed_by_changeset: הוחל בסדרת השינויים %s. +label_more: עוד +text_issues_destroy_confirmation: 'האם את\ה בטוח שברצונך למחוק את הנושא\ים ?' label_scm: SCM -text_select_project_modules: '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: 'בחר מודולים להחיל על פקרויקט זה:' +label_issue_added: נושא הוסף +label_issue_updated: נושא עודכן +label_document_added: מוסמך הוסף +label_message_posted: הודעה הוספה +label_file_added: קובץ הוסף +label_news_added: חדשות הוספו +project_module_boards: לוחות +project_module_issue_tracking: מעקב נושאים project_module_wiki: Wiki -project_module_files: 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 +project_module_files: קבצים +project_module_documents: מסמכים +project_module_repository: מאגר +project_module_news: חדשות +project_module_time_tracking: מעקב אחר זמנים +text_file_repository_writable: מאגר הקבצים ניתן לכתיבה +text_default_administrator_account_changed: מנהל המערכת ברירת המחדל שונה text_rmagick_available: RMagick available (optional) -button_configure: Configure -label_plugins: Plugins -label_ldap_authentication: LDAP authentication +button_configure: אפשרויות +label_plugins: פלאגינים +label_ldap_authentication: אימות 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 +label_this_month: החודש +label_last_n_days: ב-%d ימים אחרונים +label_all_time: תמיד +label_this_year: השנה +label_date_range: טווח תאריכים +label_last_week: שבוע שעבר +label_yesterday: אתמול +label_last_month: חודש שעבר +label_add_another_file: הוסף עוד קובץ +label_optional_description: תיאור רשות +text_destroy_time_entries_question: %.02f שעות דווחו על הנושים שאת\ה עומד\ת למחוק. מה ברצונך לעשות ? +error_issue_not_found_in_project: 'הנושאים לא נמצאו או אינם שיכים לפרויקט' +text_assign_time_entries_to_project: הצב שעות שדווחו לפרויקט הזה +text_destroy_time_entries: מחק שעות שדווחו +text_reassign_time_entries: 'הצב מחדש שעות שדווחו לפרויקט הזה:' +setting_activity_days_default: ימים המוצגים על פעילות הפרויקט +label_chronological_order: בסדר כרונולוגי +field_comments_sorting: הצג הערות +label_reverse_chronological_order: בסדר כרונולוגי הפוך +label_preferences: העדפות +setting_display_subprojects_issues: הצג נושאים של תת פרויקטים כברירת מחדל +label_overall_activity: פעילות כוללת +setting_default_projects_public: פרויקטים חדשים הינם פומביים כברירת מחדל +error_scm_annotate: "הכניסה לא קיימת או שלא ניתן לתאר אותה." +label_planning: תכנון diff --git a/lang/no.yml b/lang/no.yml index f2d7ac144..25f4047ab 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -80,7 +80,7 @@ notice_default_data_loaded: Standardkonfigurasjonen lastet inn. error_can_t_load_default_data: "Standardkonfigurasjonen kunne ikke lastes inn: %s" error_scm_not_found: "Elementet og/eller revisjonen eksisterer ikke i depoet." error_scm_command_failed: "En feil oppstod under tilkobling til depoet: %s" -error_scm_annotate: "Elementet eksisterer ikke, eller kan ikke annoteres." +error_scm_annotate: "Elementet eksisterer ikke, eller kan ikke noteres." error_issue_not_found_in_project: 'Saken eksisterer ikke, eller hører ikke til dette prosjektet' mail_subject_lost_password: Ditt %s passord @@ -137,7 +137,7 @@ field_parent: Underprosjekt til field_is_in_chlog: Vises i endringslogg field_is_in_roadmap: Vises i veikart field_login: Brukernavn -field_mail_notification: E-post varsling +field_mail_notification: E-post-varsling field_admin: Administrator field_last_login_on: Sist innlogget field_language: Språk @@ -352,7 +352,7 @@ label_gantt: Gantt label_internal: Intern label_last_changes: siste %d endringer label_change_view_all: Vis alle endringer -label_personalize_page: Tilrettelegg denne siden +label_personalize_page: Tilpass denne siden label_comment: Kommentar label_comment_plural: Kommentarer label_comment_add: Legg til kommentar @@ -545,7 +545,7 @@ button_reset: Nullstill button_rename: Endre navn button_change_password: Endre passord button_copy: Kopier -button_annotate: Annotér +button_annotate: Notér button_update: Oppdater button_configure: Konfigurer @@ -565,7 +565,7 @@ text_journal_deleted: slettet text_tip_task_begin_day: oppgaven starter denne dagen text_tip_task_end_day: oppgaven avsluttes denne dagen text_tip_task_begin_end_day: oppgaven starter og avsluttes denne dagen -text_project_identifier_info: 'Små bokstaver (a-z), nummer og binde-/understrek tillat.
    Identifikatoren kan ikke endres etter den er lagret.' +text_project_identifier_info: 'Små bokstaver (a-z), nummer og bindestrek tillatt.
    Identifikatoren kan ikke endres etter den er lagret.' text_caracters_maximum: %d tegn maksimum. text_caracters_minimum: Må være minst %d tegn langt. text_length_between: Lengde mellom %d og %d tegn. @@ -586,8 +586,8 @@ text_status_changed_by_changeset: Brukt i endringssett %s. text_issues_destroy_confirmation: 'Er du sikker på at du vil slette valgte sak(er) ?' text_select_project_modules: 'Velg moduler du vil aktivere for dette prosjektet:' text_default_administrator_account_changed: Standard administrator-konto er endret -text_file_repository_writable: Fil-depotet er skrivbart -text_rmagick_available: RMagick tilgjengelig (valgfritt) +text_file_repository_writable: Fil-arkivet er skrivbart +text_rmagick_available: RMagick er tilgjengelig (valgfritt) text_destroy_time_entries_question: %.02f timer er ført på sakene du er i ferd med å slette. Hva vil du gjøre ? text_destroy_time_entries: Slett førte timer text_assign_time_entries_to_project: Overfør førte timer til prosjektet diff --git a/lang/ru.yml b/lang/ru.yml index e488c35e1..310ded30d 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -619,5 +619,5 @@ label_preferences: Предпочтения setting_display_subprojects_issues: Отображение подпроектов по умолчанию label_overall_activity: Сводная активность setting_default_projects_public: Новые проекты являются публичными -error_scm_annotate: "The entry does not exist or can not be annotated." -label_planning: Planning +error_scm_annotate: "Данные отсутствуют или не могут быть подписаны." +label_planning: Планирование From db7f890030b8138fc12d5debcdc66bf1b1e3861b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 12:00:11 +0000 Subject: [PATCH 370/710] Mercurial: Get proper file action on revision (#983). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1332 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/mercurial_adapter.rb | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index ff52ab893..b92c88646 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -88,13 +88,7 @@ module Redmine value = $2 if parsing_descr && line_feeds > 1 parsing_descr = false - revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i, - :scmid => changeset[:changeset].split(':').last, - :author => changeset[:user], - :time => Time.parse(changeset[:date]), - :message => changeset[:description], - :paths => changeset[:files].to_s.split.collect{|path| {:action => 'X', :path => "/#{path}"}} - }) + revisions << build_revision_from_changeset(changeset) changeset = {} end if !parsing_descr @@ -111,13 +105,7 @@ module Redmine line_feeds += 1 if line.chomp.empty? end end - revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i, - :scmid => changeset[:changeset].split(':').last, - :author => changeset[:user], - :time => Time.parse(changeset[:date]), - :message => changeset[:description], - :paths => changeset[:files].to_s.split.collect{|path| {:action => 'X', :path => "/#{path}"}} - }) + revisions << build_revision_from_changeset(changeset) end return nil if $? && $?.exitstatus != 0 revisions @@ -171,6 +159,47 @@ 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 From beff2c54bc325e8a8f838d90aa122be823423114 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 12:22:59 +0000 Subject: [PATCH 371/710] Mercurial: display working directory files sizes unless browsing a specific revision (#999). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1333 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/repository/mercurial.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb index 27a8eaea9..b183c15a7 100644 --- a/app/models/repository/mercurial.rb +++ b/app/models/repository/mercurial.rb @@ -34,6 +34,11 @@ class Repository::Mercurial < Repository if entries entries.each do |entry| next unless entry.is_file? + # Set the filesize unless browsing a specific revision + if identifier.nil? + full_path = File.join(root_url, entry.path) + entry.size = File.stat(full_path).size if File.file?(full_path) + end # Search the DB for the entry's last change change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC") if change From 0249ae5f500063f4d08ff09a7071cb949a534316 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 13:15:09 +0000 Subject: [PATCH 372/710] Preserve status filter and page number when using lock/unlock/activate links on the users list (closes #998). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1334 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/users_controller.rb | 3 ++- app/helpers/users_helper.rb | 12 ++++++++++++ app/views/users/list.rhtml | 12 +----------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ceb70ab92..48fc6fade 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -83,7 +83,8 @@ class UsersController < ApplicationController end if @user.update_attributes(params[:user]) flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'list' + # Give a string to redirect_to otherwise it would use status param as the response code + redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page])) end end @auth_sources = AuthSource.find(:all) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 7bd137161..250ed8ce8 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -22,4 +22,16 @@ module UsersHelper [l(:status_registered), 2], [l(:status_locked), 3]], selected) end + + def change_status_link(user) + url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]} + + if user.locked? + link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock' + elsif user.registered? + link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock' + else + link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock' + end + end end diff --git a/app/views/users/list.rhtml b/app/views/users/list.rhtml index e12aa3425..d89672d19 100644 --- a/app/views/users/list.rhtml +++ b/app/views/users/list.rhtml @@ -33,17 +33,7 @@
    - + <% end -%> From 246e8f67c598664741954aa2cbb4cfeafea7ef85 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 17:29:09 +0000 Subject: [PATCH 373/710] Redmine.pm support for LDAP authentication (patch by Liwiusz Ociepa). Closes #879, #918. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1335 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm index b76622e3d..f4c5abc6b 100644 --- a/extra/svn/Redmine.pm +++ b/extra/svn/Redmine.pm @@ -90,6 +90,7 @@ use strict; use DBI; use Digest::SHA1; +use Authen::Simple::LDAP; use Apache2::Module; use Apache2::Access; @@ -140,7 +141,7 @@ sub is_public_project { my $dbh = connect_database($r); my $sth = $dbh->prepare( - "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;" + "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;" ); $sth->execute($project_id); @@ -176,17 +177,37 @@ sub is_member { my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); my $sth = $dbh->prepare( - "SELECT hashed_password FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;" + "SELECT hashed_password, auth_source_id FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;" ); $sth->execute($redmine_user, $project_id); my $ret; while (my @row = $sth->fetchrow_array) { - if ($row[0] eq $pass_digest) { - $ret = 1; - last; + unless ($row[1]) { + if ($row[0] eq $pass_digest) { + $ret = 1; + last; + } + } else { + my $sthldap = $dbh->prepare( + "SELECT host,port,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;" + ); + $sthldap->execute($row[1]); + while (my @rowldap = $sthldap->fetchrow_array) { + my $ldap = Authen::Simple::LDAP->new( + host => $rowldap[0], + port => $rowldap[1], + basedn => $rowldap[4], + binddn => $rowldap[2] ? $rowldap[2] : "", + bindpw => $rowldap[3] ? $rowldap[3] : "", + filter => "(".$rowldap[5]."=%s)" + ); + $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass)); + } + $sthldap->finish(); } } + $sth->finish(); $dbh->disconnect(); $ret; From 2bcb7820875b9ace4dad36a759cee453c45e4dec Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 6 Apr 2008 17:36:26 +0000 Subject: [PATCH 374/710] Redmine.pm for webdav authentication: * make Authen::Simple::LDAP module optional * handle TLS flag set in Redmine git-svn-id: http://redmine.rubyforge.org/svn/trunk@1336 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm index f4c5abc6b..782d0777a 100644 --- a/extra/svn/Redmine.pm +++ b/extra/svn/Redmine.pm @@ -8,8 +8,8 @@ against redmine database =head1 SYNOPSIS This module allow anonymous users to browse public project and -registred users to browse and commit their project. authentication is -done on the redmine database. +registred users to browse and commit their project. Authentication is +done against the redmine database or the LDAP configured in redmine. This method is far simpler than the one with pam_* and works with all database without an hassle but you need to have apache/mod_perl on the @@ -29,6 +29,9 @@ On debian/ubuntu you must do : aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl +If your Redmine users use LDAP authentication, you will also need +Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used). + =head1 CONFIGURATION ## if the module isn't in your perl path @@ -90,7 +93,8 @@ use strict; use DBI; use Digest::SHA1; -use Authen::Simple::LDAP; +# optional module for LDAP authentication +my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1"); use Apache2::Module; use Apache2::Access; @@ -188,21 +192,21 @@ sub is_member { $ret = 1; last; } - } else { + } elsif ($CanUseLDAPAuth) { my $sthldap = $dbh->prepare( - "SELECT host,port,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;" + "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;" ); $sthldap->execute($row[1]); while (my @rowldap = $sthldap->fetchrow_array) { my $ldap = Authen::Simple::LDAP->new( - host => $rowldap[0], - port => $rowldap[1], - basedn => $rowldap[4], - binddn => $rowldap[2] ? $rowldap[2] : "", - bindpw => $rowldap[3] ? $rowldap[3] : "", - filter => "(".$rowldap[5]."=%s)" - ); - $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass)); + host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], + port => $rowldap[1], + basedn => $rowldap[5], + binddn => $rowldap[3] ? $rowldap[3] : "", + bindpw => $rowldap[4] ? $rowldap[4] : "", + filter => "(".$rowldap[6]."=%s)" + ); + $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass)); } $sthldap->finish(); } From fc1a295d8a51f49e75155835a766e8fc5f9d5977 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 7 Apr 2008 17:03:02 +0000 Subject: [PATCH 375/710] Redmine.pm doc update git-svn-id: http://redmine.rubyforge.org/svn/trunk@1337 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm index 782d0777a..6f3ba4385 100644 --- a/extra/svn/Redmine.pm +++ b/extra/svn/Redmine.pm @@ -30,7 +30,9 @@ On debian/ubuntu you must do : aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl If your Redmine users use LDAP authentication, you will also need -Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used). +Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used): + + aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl =head1 CONFIGURATION From 5d3454853956adee21518a1fc891b26f36c3c294 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 7 Apr 2008 17:18:09 +0000 Subject: [PATCH 376/710] CSV export added to timelog report (#1009). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1338 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 5 ++- app/helpers/timelog_helper.rb | 52 ++++++++++++++++++++++ app/views/timelog/report.rhtml | 5 +++ test/functional/timelog_controller_test.rb | 13 +++++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 77337f344..3081c50dd 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -112,7 +112,10 @@ class TimelogController < ApplicationController end end - render :layout => false if request.xhr? + respond_to do |format| + format.html { render :layout => !request.xhr? } + format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') } + end end def details diff --git a/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb index 05b55907c..e0459581d 100644 --- a/app/helpers/timelog_helper.rb +++ b/app/helpers/timelog_helper.rb @@ -76,4 +76,56 @@ module TimelogHelper export.rewind export end + + def report_to_csv(criterias, periods, hours) + export = StringIO.new + CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| + # Column headers + headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) } + headers += periods + headers << l(:label_total) + csv << headers.collect {|c| to_utf8(c) } + # Content + report_criteria_to_csv(csv, criterias, periods, hours) + # Total row + row = [ l(:label_total) ] + [''] * (criterias.size - 1) + total = 0 + periods.each do |period| + sum = sum_hours(select_hours(hours, @columns, period.to_s)) + total += sum + row << (sum > 0 ? "%.2f" % sum : '') + end + row << "%.2f" %total + csv << row + end + export.rewind + export + end + + def report_criteria_to_csv(csv, criterias, periods, hours, level=0) + hours.collect {|h| h[criterias[level]]}.uniq.each do |value| + hours_for_value = select_hours(hours, criterias[level], value) + next if hours_for_value.empty? + row = [''] * level + row << to_utf8(value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value)) + row += [''] * (criterias.length - level - 1) + total = 0 + periods.each do |period| + sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) + total += sum + row << (sum > 0 ? "%.2f" % sum : '') + end + row << "%.2f" %total + csv << row + + if criterias.length > level + 1 + report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1) + end + end + end + + def to_utf8(s) + @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8') + begin; @ic.iconv(s.to_s); rescue; s.to_s; end + end end diff --git a/app/views/timelog/report.rhtml b/app/views/timelog/report.rhtml index c29cadf9c..97251bc11 100644 --- a/app/views/timelog/report.rhtml +++ b/app/views/timelog/report.rhtml @@ -60,6 +60,11 @@
    <%=l(:field_version)%><%=l(:field_filename)%><%=l(:label_date)%><%=l(:field_filesize)%><%=l(:label_downloads_abbr)%>MD5
    <%= image_tag('true.png') if user.admin? %> <%= format_time(user.created_on) %> - - <% if user.locked? -%> - <%= link_to l(:button_unlock), {:action => 'edit', :id => user, :user => {:status => User::STATUS_ACTIVE}}, :method => :post, :class => 'icon icon-unlock' %> - <% elsif user.registered? -%> - <%= link_to l(:button_activate), {:action => 'edit', :id => user, :user => {:status => User::STATUS_ACTIVE}}, :method => :post, :class => 'icon icon-unlock' %> - <% else -%> - <%= link_to l(:button_lock), {:action => 'edit', :id => user, :user => {:status => User::STATUS_LOCKED}}, :method => :post, :class => 'icon icon-lock' %> - <% end -%> - - <%= change_status_link(user) %>
    + +

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

    <% end %> <% end %> diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index 6e3308c84..4d6cb0b36 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -118,7 +118,18 @@ class TimelogControllerTest < Test::Unit::TestCase assert_template 'report' assert_not_nil assigns(:total_hours) assert_equal "0.00", "%.2f" % assigns(:total_hours) - end + end + + def test_report_csv_export + get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first + # Total row + assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last + end def test_details_at_project_level get :details, :project_id => 1 From f025b16be747bf7ab7b1cae470eca28b77f7ada9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 7 Apr 2008 17:56:32 +0000 Subject: [PATCH 377/710] Fix date range filter js behaviour and css fix for IE. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1339 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/timelog/_date_range.rhtml | 10 ++++++---- public/stylesheets/application.css | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/views/timelog/_date_range.rhtml b/app/views/timelog/_date_range.rhtml index a54f7a2f0..ed84b16cf 100644 --- a/app/views/timelog/_date_range.rhtml +++ b/app/views/timelog/_date_range.rhtml @@ -1,4 +1,4 @@ -
    <%= l(:label_date_range) %> +
    <%= l(:label_date_range) %>

    <%= radio_button_tag 'period_type', '1', !@free_period %> <%= select_tag 'period', options_for_period_select(params[:period]), @@ -7,11 +7,13 @@

    <%= radio_button_tag 'period_type', '2', @free_period %> + <%= l(:label_date_from) %> -<%= text_field_tag 'from', @from, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('from') %> +<%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %> <%= l(:label_date_to) %> -<%= text_field_tag 'to', @to, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('to') %> -<%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %> +<%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %> + +<%= submit_tag l(:button_apply), :name => nil %>

    diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 27c7592f8..7c3e0fde7 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -159,7 +159,9 @@ div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } -fieldset#filters .buttons { text-align: right; font-size: 0.9em; margin: 0 4px 0px 0; } +fieldset#filters { padding: 0.7em; } +fieldset#filters p { margin: 0.8em 0 0.8em 0; } +fieldset#filters .buttons { text-align: right; font-size: 0.9em; } div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} div#issue-changesets .changeset { padding: 4px;} From a3c89d4f697e958956e129b6bc5f564c9197a89c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 9 Apr 2008 17:45:39 +0000 Subject: [PATCH 378/710] Custom fields (list and boolean) can be used as criteria in time report (#1012). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1340 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 9 +++++++++ app/helpers/timelog_helper.rb | 8 ++++++-- app/views/timelog/_report_criteria.rhtml | 4 ++-- test/fixtures/custom_fields.yml | 2 +- test/fixtures/time_entries.yml | 2 +- test/functional/issues_controller_test.rb | 5 +++-- test/functional/timelog_controller_test.rb | 21 +++++++++++++++++++-- 7 files changed, 41 insertions(+), 10 deletions(-) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 3081c50dd..2c90093bd 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -26,6 +26,8 @@ class TimelogController < ApplicationController include SortHelper helper :issues include TimelogHelper + helper :custom_fields + include CustomFieldsHelper def report @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id", @@ -51,6 +53,13 @@ class TimelogController < ApplicationController :label => :label_issue} } + # Add list and boolean custom fields as available criterias + @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| + @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)", + :format => cf.field_format, + :label => cf.name} + end + @criterias = params[:criterias] || [] @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} @criterias.uniq! diff --git a/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb index e0459581d..db13556a1 100644 --- a/app/helpers/timelog_helper.rb +++ b/app/helpers/timelog_helper.rb @@ -77,6 +77,10 @@ module TimelogHelper export end + def format_criteria_value(criteria, value) + value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format])) + end + def report_to_csv(criterias, periods, hours) export = StringIO.new CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| @@ -103,11 +107,11 @@ module TimelogHelper end def report_criteria_to_csv(csv, criterias, periods, hours, level=0) - hours.collect {|h| h[criterias[level]]}.uniq.each do |value| + hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| hours_for_value = select_hours(hours, criterias[level], value) next if hours_for_value.empty? row = [''] * level - row << to_utf8(value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value)) + row << to_utf8(format_criteria_value(criterias[level], value)) row += [''] * (criterias.length - level - 1) total = 0 periods.each do |period| diff --git a/app/views/timelog/_report_criteria.rhtml b/app/views/timelog/_report_criteria.rhtml index 661e1fdb8..94f3d20f9 100644 --- a/app/views/timelog/_report_criteria.rhtml +++ b/app/views/timelog/_report_criteria.rhtml @@ -1,9 +1,9 @@ -<% @hours.collect {|h| h[criterias[level]]}.uniq.each do |value| %> +<% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %> <% hours_for_value = select_hours(hours, criterias[level], value) -%> <% next if hours_for_value.empty? -%> <%= '' * level %> -<%= value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value) %> +<%= format_criteria_value(criterias[level], value) %> <%= '' * (criterias.length - level - 1) -%> <% total = 0 -%> <% @periods.each do |period| -%> diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index e58d8e3dc..6be840fcc 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -3,7 +3,7 @@ custom_fields_001: name: Database min_length: 0 regexp: "" - is_for_all: false + is_for_all: true type: IssueCustomField max_length: 0 possible_values: MySQL|PostgreSQL|Oracle diff --git a/test/fixtures/time_entries.yml b/test/fixtures/time_entries.yml index f6876c7b0..4a8a4a2a4 100644 --- a/test/fixtures/time_entries.yml +++ b/test/fixtures/time_entries.yml @@ -36,7 +36,7 @@ time_entries_003: updated_on: 2007-04-21 12:20:48 +02:00 activity_id: 9 spent_on: 2007-04-21 - issue_id: 2 + issue_id: 3 id: 3 hours: 1.0 user_id: 1 diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 89769ffe2..042a8f3f2 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -450,10 +450,11 @@ class IssuesControllerTest < Test::Unit::TestCase end def test_destroy_issue_with_no_time_entries + assert_nil TimeEntry.find_by_issue_id(2) @request.session[:user_id] = 2 - post :destroy, :id => 3 + post :destroy, :id => 2 assert_redirected_to 'projects/ecookbook/issues' - assert_nil Issue.find_by_id(3) + assert_nil Issue.find_by_id(2) end def test_destroy_issues_with_time_entries diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index 4d6cb0b36..e80a67728 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -22,7 +22,7 @@ require 'timelog_controller' class TimelogController; def rescue_action(e) raise e end; end class TimelogControllerTest < Test::Unit::TestCase - fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses + fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values def setup @controller = TimelogController.new @@ -112,6 +112,23 @@ class TimelogControllerTest < Test::Unit::TestCase assert_equal "162.90", "%.2f" % assigns(:total_hours) end + def test_report_custom_field_criteria + get :report, :project_id => 1, :criterias => ['project', 'cf_1'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_not_nil assigns(:criterias) + assert_equal 2, assigns(:criterias).size + assert_equal "162.90", "%.2f" % assigns(:total_hours) + # Custom field column + assert_tag :tag => 'th', :content => 'Database' + # Custom field row + assert_tag :tag => 'td', :content => 'MySQL', + :sibling => { :tag => 'td', :attributes => { :class => 'hours' }, + :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' }, + :content => '1' }} + end + def test_report_one_criteria_no_result get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project'] assert_response :success @@ -186,6 +203,6 @@ class TimelogControllerTest < Test::Unit::TestCase assert_response :success assert_equal 'text/csv', @response.content_type assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n") - assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,2,Feature request,Add ingredients categories,1.0,\"\"\n") + assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n") end end From 85f040c5362dd89829f694a58f4e91ced7fcd33e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 9 Apr 2008 17:52:41 +0000 Subject: [PATCH 379/710] Fixed: preview fails when updating an issue (broken by r1322). Patch #1027. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1341 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index b3f21fddf..369d888ef 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -345,7 +345,7 @@ class IssuesController < ApplicationController def preview @issue = @project.issues.find_by_id(params[:id]) if params[:id] - @attachements = issue.attachments if @issue + @attachements = @issue.attachments if @issue @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil) render :partial => 'common/preview' end From 6d2a89142af235b5d0a9e30350859fb99ac665f3 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 12 Apr 2008 16:54:14 +0000 Subject: [PATCH 380/710] Add an icon to each event on the activity view. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1342 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/journal.rb | 1 + app/models/message.rb | 1 + app/models/wiki_content.rb | 1 + app/views/projects/activity.rhtml | 2 +- public/images/attachment.png | Bin 259 -> 995 bytes public/images/changeset.png | Bin 0 -> 512 bytes public/images/comments.png | Bin 0 -> 557 bytes public/images/document.png | Bin 0 -> 458 bytes public/images/message.png | Bin 0 -> 521 bytes public/images/news.png | Bin 0 -> 658 bytes public/images/ticket.png | Bin 0 -> 500 bytes public/images/ticket_checked.png | Bin 0 -> 598 bytes public/images/ticket_edit.png | Bin 0 -> 731 bytes public/images/wiki_edit.png | Bin 0 -> 626 bytes public/stylesheets/application.css | 14 +++++++++++-- test/functional/projects_controller_test.rb | 2 +- .../acts_as_event/lib/acts_as_event.rb | 19 ++++++++++++------ 17 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 public/images/changeset.png create mode 100644 public/images/comments.png create mode 100644 public/images/document.png create mode 100644 public/images/message.png create mode 100644 public/images/news.png create mode 100644 public/images/ticket.png create mode 100644 public/images/ticket_checked.png create mode 100644 public/images/ticket_edit.png create mode 100644 public/images/wiki_edit.png diff --git a/app/models/journal.rb b/app/models/journal.rb index 7c5e3d3bf..1376d349e 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -33,6 +33,7 @@ class Journal < ActiveRecord::Base acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') }, :description => :notes, :author => :user, + :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' }, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} def save diff --git a/app/models/message.rb b/app/models/message.rb index 12b1cd990..a18d126c9 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -28,6 +28,7 @@ class Message < ActiveRecord::Base :date_column => 'created_on' acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, :description => :content, + :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}} attr_protected :locked, :sticky diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index 13915c274..724354ad6 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -32,6 +32,7 @@ class WikiContent < ActiveRecord::Base acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"}, :description => :comments, :datetime => :updated_on, + :type => 'wiki-page', :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} def text=(plain) diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index 0cf7a5000..c2f2f9ebd 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -6,7 +6,7 @@

    <%= format_activity_day(day) %>

    <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> -
    <%= format_time(e.event_datetime, false) %> +
    <%= format_time(e.event_datetime, false) %> <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %>
    <% unless e.event_description.blank? -%> <%= format_activity_description(e.event_description) %>
    diff --git a/public/images/attachment.png b/public/images/attachment.png index eea26921b695b377e2a51cbbb78f638514565a32..b7ce3c445ec04fa8d16a939340d070752937354a 100644 GIT binary patch literal 995 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJdx@v7EBg%=9syGYj%hhlfI^%F z9+AZi3|t>Tn9*sC$qb+%OS+@4BLl<6e(pbstPBjy3;{kNu0cUT$;rvp)zw?KZasGF z*tv7(u3fu!@7}%t3=IEA!Dt8!_Yg2}%9RCWn35pBV3=PSg8eImfP#UZE{-7;w|dVz zavd<>aJg9M!}h-Z(YA|AG8YDv|5t44xc)%e!;WE6F>}W~^9F%yc@l3oabC?}owatW wV9zVYH`jy8ny#$1%Ac_MbKAL}A7jokHqT>Qc4xX;3ea!{Pgg&ebxsLQ09njHQvd(} delta 230 zcmaFN-ppju8Q|y6%O%Cdz`(%k>ERLtqXw0>k|_bj~qF2{rdF_7cK+`2XET6>CmA==g*(Nd-pET z060kP}1CjdcE5<}x94pxPedRJH%adGQ3itM&1 zUd`B%Ecoug0gfkAI1CI#G+L~gOf^+onk;-4o^;@2UOcIj;RKJPqXHwtkrJ*2CQBj` PLH2pN`njxgN@xNAJ*rGm diff --git a/public/images/changeset.png b/public/images/changeset.png new file mode 100644 index 0000000000000000000000000000000000000000..67de2c6ccbeac17742f56cf7391e72b2bf5033ba GIT binary patch literal 512 zcmV+b0{{JqP)CQDsH?WF>AIFt zQuJ}i;w2$ZUU#3SZ6RY0Gw;kZ&ol1~2ky^QZ(fom$=jNJZt!z7w_pH~wdQ;R)Gh%BbQFCx+Nm!4SuS-vkr`vhhrX zM*>w%e+v~?m@q~ImPAgtLkR_3U<2F8LP3W5=LJ*ZN|S5p#sf4YFr$p~Q~Z*0Ngxf2 zjk#J#<7EAlhzlrV53~GF&pIzcCN_lz9@05UeoUXiK%N z#x+4o*i_c|6_Uu1+&TIho?3@y4k-#b8Y_o94zW*B3a1ne2-Y5s0uke$$|@=}OP-i= zNYZQA=>PrZu0MfSL=b8UhD_={W4IY1{b{)U)*gc45xtL%IYLY&hF;d`@GzI&7H&D# zh;z_BX$#hqh@q?AY3sJTod2%*Yd)_>YM0#q&ixGuh+PQsneK)F0000kO-!8JjERGZgNx4o8zwI5pnrkJT^AW7B+k01lLkoCLBu@bfRhnJSkT}{ z5cwz-C?AEk;`Q7|NyO0V@Jst%?>X<@)8|?6hNplZoH}p}R=_wBd4A);huvr*D{8ta zv>weaRZy(zA{a{x)NMK$oUyoq;&Q_jA9Yid>V_!Q3{lkDazCTg+2GL8fKO$yVv7pZ zw#ZdlB3o|B^q?JFdAt+nq80!As0d}1Z|KFR(*lEh^G}{es&~_I}wY;n4d5jAs0d}gj@(+ z%6*K*29I(Myv%_UPWDD&5qS@s3~ZAJGDH vT*SlN0ce3)r#d%-F%SaNZe6;L@E^Vb!Ji3~dec0&00000NkvXXu0mjflI-*P literal 0 HcmV?d00001 diff --git a/public/images/document.png b/public/images/document.png new file mode 100644 index 0000000000000000000000000000000000000000..d00b9b2f4d99145ef74ddbb5624a1afbae422a4e GIT binary patch literal 458 zcmV;*0X6=KP)WdL(wZ7v`&G9Y1gaxNe;FfceEF*rIiH###lAS*C2Ffi(UX{-PM00(qQ zO+^RT1Pl--3*6r22LJ#732;bRa{vGf5&!@T5&_cPe*6Fc00d`2O+f$vv5yP$*NQ{I&x=sL=2S*bdywvg|lc*L6+P{8k`$Oxa>e$BQNC-|+n@aKAKBM^5y{63 z&~{MlW24U9!M#W^P1DMWdLnuaxNe_AYpcLE+8^6FgPGFGCDLjIx#sQD=;xIFl)^P$N&HU2XskI zMF-df2?{V1>|QTZ00009a7bBm000XT000XT0n*)m`~Uy|1ZP1_K>z@;j|==^1poj6 zUr9tkRCwBB{Qv(y11W%sB-KE`0>odw|C+z+`iUDafb75juvn<8Ae`*3B_YHO;sBl0 zwfgLe1Gj$v`3pqHqoX|KI-%|Nb!m$-nA*0}m(b+s{A#0eu6u?AITl&i@c~5DknBAd|lQ_|3z?3ev#A z#$u!*w))VWP&*Zn6B&V?1R{pV@BhF1`d^ZtQH+;q%c%#SzWr1X=L3o}0z(QC567-Q z2Zu7q9#=!TWDhMMoVs((xWgD$)EF}_d~6|E9cYZTvaqk~9`i&BW97*P>v z@lDk@Xl;b%^t%HQ(pa0tz@5kM+;h(T<_N&ip(~wEOB0DiJ{F5{;c!^;_xs1dg=jRo z3WY*KAQ0euKA+@K0;yC=8jr^rydICoSglqd_{QV$a4whY=|O=zI1-7hhr=Og4T=Ck z5O8*O_S@lbByBd^lf44TWKv2W^LH=<&(F_6aj;k{L?;9b7K?>5nM{(sFqO3)3}ZrBV@HFj%2boa`NCnylwer*m5<6vU%rtuL2L)a!Nd zJP*qIOD>n6?i;$@!fv-8Tdme9+l?I|%bm;R7`!3)<-bP$-VIIGvB6*%vHYw+%Da)t sWG@c=a1S2nbh=||dPK1BKbT#A0Qk`HN6lbD;Q#;t07*qoM6N<$f^gU==>Px# literal 0 HcmV?d00001 diff --git a/public/images/ticket.png b/public/images/ticket.png new file mode 100644 index 0000000000000000000000000000000000000000..244e6ca045c50a130086ac388b560a12761544b4 GIT binary patch literal 500 zcmV z!-sF!^gVb+8rtpyctW0#N6uWni0LCt_6PoOdbjll_d4>B|?abUmpo8>v>h}Zj|Ya;Eu#qwvU1IVc9khP8VrtAsT2=e83P~$#!xXbw)n}FlPSEe7Hq1uCb zR8w;xqmBrUgA^pnkB=O@-lq0DPz$ay0yh_~I_IDpzxRb(4=Iy9CT||k!08w)Pe>W4 zElmH8fF;68$GMwZ#7{4ozI(ySrR%I+xs4-G1q^UxnUV7rlf9>Rn&_6Wike0000(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-+(|@1RCwB?lRaxyK@f(Yy?$##EF`736yY>VVv#f|;;j_4idY2ELa;D`h#*Ok z%FaI^2*J)q5D`H{Gyx?6!NS5=2oj9OkeKM*J!d~&i*s@{it!8>AbC|K~0p0g8Xy+90X%Z4_nOJJZ{0Kw9Oa~mVb zm*?0ep=8Tl15yGE;*Wx9>_Fbuq!0qBm?x~hfs`W^rA-AShmu3dvVGqfl5f`t#2g4H zIgqcQzhQ?`8x&Ac6G8a7ScbgLvn3^0iC9RsbP|4T6iCtGW?vEVj}Qw`Ai5QHHhW9Y z$)RXL33cB`EIqqUY|ldUBzdk*PNL{aI5{pqyg@T-A;kXdB!gr9YOK|}`^YeUd4c7ZcS!Qw z{QQAt)?(!Kdz2g_GhaF|z|%+10gg2Q2oAHQ!JfkxQP|6s$G1C|n^}u%GxN3Z)cEuZ zBZ-Ckr{o_<^wK(Yy!u9_7Ov9YyqN~a`n4uf2X`KEvoSS3Z7Qf1t{O)F koCBkM8W`<6T6gw)03XG3+w)>+-v9sr07*qoM6N<$f|$Mh@Bjb+ literal 0 HcmV?d00001 diff --git a/public/images/ticket_edit.png b/public/images/ticket_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..291bfc764709a7e595050c1ed43b675f7af29c56 GIT binary patch literal 731 zcmV<10wn#3P)IABYLga|n=&i1C^Ll$f?5eeicnA#1rn`Vw5g!~Ai_;Six%mol@YXv zpr{DSk_4&HqEGXqMs21Urx~61zW2TFJuNiKk5=8qxp3h5o#%3&zLVh3I}!7>rV zy0cveL@eMS{2@4@V#Ifsc!DulJ&DQpf%Op4v$xe?6=D@)U81WGV*SDrfWQL`Vikx# zC;D5g5L>`a_LDBIB75he%=~zQqY~|505hGQwLEXKi?wPF;-YiIct4j@Rd<3|O#|8C zTHZhC><9zcK%qYMV7$)|Xb?l7a+FlZ5nDj##-OBu%J?oaL+^+>n(`G@l&xA93j+jh z8k0*q$O-j?Nca8_zy`4G;prD4e1YrH!W$vy!p#G|OpS~5?22&Z3S zwVIa$XW=s4>S&U~r@Q#jeV*}=UW(V%W0I-yifB=D-Gvu#7REiau<_yZq|tBhQd7T= z($ZQI{e#oW)d!XmFX`aO{z|uT!vfZWdLnuaxNe_AYpcLE+8^6FgPGFGCDLhIx{&SD=;xIFfAR@hyVZp2XskI zMF-df2?{SO2?m){00009a7bBm000XT000XT0n*)m`~Uy|1ZP1_K>z@;j|==^1poj6 z$Vo&&RCwBB{Qv(y11W$7h@U)p^77@&zkmN?N{fk!$;rtvF)^WN0IJr}(P3d>K~w$b z&!1DLP5~LndO(VRW-~K0|NQwAuI}Hzf4_eH5*8L#5`OjN*tC0>bickiC}))=sNv57 z@dG0xBhUt*Iv^Wp7*G%>^8LkT=9i}>bYu7x4L{shzq@~hF;D}t8-PlIdVsb8HT-+O z@Au!ELaNs9?p|kO{Kd;B!NDU0brJ*s)j{+CUHI?g3AW!?`IX#%K3~Dc#rW{TjbDG5 zRQ&2e3eY?Xbjh!`NB@1;&#&zD M07*qoM6N<$g0&13`~Uy| literal 0 HcmV?d00001 diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 7c3e0fde7..682e95dde 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -169,11 +169,21 @@ 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; } -div#activity dt { margin-bottom: 1px; } +div#activity dd { margin-bottom: 1em; padding-left: 18px; } +div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description { font-style: italic; } div#activity span.project:after { content: " -"; } +div#activity dt.issue { background-image: url(../images/ticket.png); } +div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); } +div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); } +div#activity dt.changeset { background-image: url(../images/changeset.png); } +div#activity dt.news { background-image: url(../images/news.png); } +div#activity dt.message { background-image: url(../images/message.png); } +div#activity dt.reply { background-image: url(../images/comments.png); } +div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); } +div#activity dt.attachment { background-image: url(../images/attachment.png); } +div#activity dt.document { background-image: url(../images/document.png); } div#roadmap fieldset.related-issues { margin-bottom: 1em; } div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; } diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 75b4673a1..eb5795152 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -144,7 +144,7 @@ class ProjectsControllerTest < Test::Unit::TestCase :content => /#{2.days.ago.to_date.day}/, :sibling => { :tag => "dl", :child => { :tag => "dt", - :attributes => { :class => 'journal' }, + :attributes => { :class => 'issue-edit' }, :child => { :tag => "a", :content => /(#{IssueStatus.find(2).name})/, } diff --git a/vendor/plugins/acts_as_event/lib/acts_as_event.rb b/vendor/plugins/acts_as_event/lib/acts_as_event.rb index a0d1822ad..d7f437a5e 100644 --- a/vendor/plugins/acts_as_event/lib/acts_as_event.rb +++ b/vendor/plugins/acts_as_event/lib/acts_as_event.rb @@ -25,11 +25,12 @@ 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[:datetime] ||= :created_on + options[:title] ||= :title + options[:description] ||= :description + options[:author] ||= :author options[:url] ||= {:controller => 'welcome'} + options[:type] ||= self.name.underscore.dasherize cattr_accessor :event_options self.event_options = options send :include, Redmine::Acts::Event::InstanceMethods @@ -41,11 +42,17 @@ module Redmine base.extend ClassMethods end - %w(datetime title description author).each do |attr| + %w(datetime title description author type).each do |attr| src = <<-END_SRC def event_#{attr} option = event_options[:#{attr}] - option.is_a?(Proc) ? option.call(self) : send(option) + if option.is_a?(Proc) + option.call(self) + elsif option.is_a?(Symbol) + send(option) + else + option + end end END_SRC class_eval src, __FILE__, __LINE__ From 7aaa538fd9cb34b88c037a7d03a5351c1e2c852e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 12 Apr 2008 17:13:17 +0000 Subject: [PATCH 381/710] Fixed: error when browsing an empty Mercurial repository (#1046). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1343 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/repository/mercurial.rb | 4 +++- lib/redmine/scm/adapters/mercurial_adapter.rb | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb index b183c15a7..18cbc9495 100644 --- a/app/models/repository/mercurial.rb +++ b/app/models/repository/mercurial.rb @@ -58,7 +58,9 @@ class Repository::Mercurial < Repository # latest revision found in database db_revision = latest_changeset ? latest_changeset.revision.to_i : -1 # latest revision in the repository - scm_revision = scm_info.lastrev.identifier.to_i + latest_revision = scm_info.lastrev + return if latest_revision.nil? + scm_revision = latest_revision.identifier.to_i if db_revision < scm_revision logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? identifier_from = db_revision + 1 diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index b92c88646..72db723ba 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -105,7 +105,8 @@ module Redmine line_feeds += 1 if line.chomp.empty? end end - revisions << build_revision_from_changeset(changeset) + # 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 From 8eb86396e195e040658b4f3b685072e9bd5cc572 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 12 Apr 2008 17:25:06 +0000 Subject: [PATCH 382/710] Fixed: Double dot displayed on ticket view when ticket not updated (closes #813). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1344 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/show.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 77d9ce640..f788d0ec8 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -13,7 +13,7 @@

    <%=h @issue.subject %>

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

    From a340d8c957bb05b60a9b58ccfdfbcfd6015ea9fa Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 13 Apr 2008 09:12:43 +0000 Subject: [PATCH 383/710] Better error message and AR errors in log for failed LDAP on-the-fly user creation (closes #932, #1042). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1345 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/account_controller.rb | 2 ++ app/models/user.rb | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index e719e8c9b..b9224c158 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -56,6 +56,8 @@ class AccountController < ApplicationController 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 diff --git a/app/models/user.rb b/app/models/user.rb index e0b1e238c..a67a08567 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,6 +18,9 @@ require "digest/sha1" class User < ActiveRecord::Base + + class OnTheFlyCreationFailure < Exception; end + # Account statuses STATUS_ANONYMOUS = 0 STATUS_ACTIVE = 1 @@ -105,15 +108,17 @@ class User < ActiveRecord::Base onthefly.language = Setting.default_language if onthefly.save user = find(:first, :conditions => ["login=?", login]) - logger.info("User '#{user.login}' created on the fly.") if logger + logger.info("User '#{user.login}' created from the LDAP") if logger + else + logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger + raise OnTheFlyCreationFailure.new end end end user.update_attribute(:last_login_on, Time.now) if user user - - rescue => text - raise text + rescue => text + raise text end # Return user's full name for display From 56683967dac2a3cd215f7495b33b2ed7dbf236f1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 13 Apr 2008 11:18:09 +0000 Subject: [PATCH 384/710] Left align filter buttons (closes #872) and reduce filters spacing. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1346 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/queries/_filters.rhtml | 12 ++++++------ public/stylesheets/application.css | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/queries/_filters.rhtml b/app/views/queries/_filters.rhtml index 458d7139e..ec9d4fef6 100644 --- a/app/views/queries/_filters.rhtml +++ b/app/views/queries/_filters.rhtml @@ -59,19 +59,19 @@ function toggle_multi_select(field) {
    - diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 682e95dde..7b73d0939 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -160,8 +160,12 @@ p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } fieldset#filters { padding: 0.7em; } -fieldset#filters p { margin: 0.8em 0 0.8em 0; } -fieldset#filters .buttons { text-align: right; font-size: 0.9em; } +fieldset#filters p { margin: 1.2em 0 0.8em 2px; } +fieldset#filters .buttons { font-size: 0.9em; } +fieldset#filters table { border-collapse: collapse; } +fieldset#filters table td { padding: 0; vertical-align: middle; } +fieldset#filters tr.filter { height: 2em; } +fieldset#filters td.add-filter { text-align: right; vertical-align: top; } div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} div#issue-changesets .changeset { padding: 4px;} From 8b9fb29d95d8d467da2cc1bdb657503b433305f4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 13 Apr 2008 12:25:22 +0000 Subject: [PATCH 385/710] =?UTF-8?q?Translation=20updates=20(closes=20#1010?= =?UTF-8?q?,=20#1017,=20#1047):=20*=20Finnish=20(Antti=20Perki=C3=B6m?= =?UTF-8?q?=C3=A4ki)=20*=20Spanish=20(Gumer=20Coronel)=20*=20Czech=20(Maxi?= =?UTF-8?q?m=20Kru=C5=A1ina)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1347 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/cs.yml | 34 +++++++-------- lang/es.yml | 120 ++++++++++++++++++++++++++-------------------------- lang/fi.yml | 6 +-- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/lang/cs.yml b/lang/cs.yml index 03702eaa2..60fcb8d11 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -10,16 +10,16 @@ actionview_datehelper_select_month_prefix: actionview_datehelper_select_year_prefix: actionview_datehelper_time_in_words_day: 1 den actionview_datehelper_time_in_words_day_plural: %d dny -actionview_datehelper_time_in_words_hour_about: asi hodinu -actionview_datehelper_time_in_words_hour_about_plural: asi %d hodin -actionview_datehelper_time_in_words_hour_about_single: asi hodinu -actionview_datehelper_time_in_words_minute: 1 minuta -actionview_datehelper_time_in_words_minute_half: půl minuty -actionview_datehelper_time_in_words_minute_less_than: méně než minutu -actionview_datehelper_time_in_words_minute_plural: %d minut -actionview_datehelper_time_in_words_minute_single: 1 minuta -actionview_datehelper_time_in_words_second_less_than: méně než sekunda -actionview_datehelper_time_in_words_second_less_than_plural: méně než %d sekund +actionview_datehelper_time_in_words_hour_about: asi hodinou +actionview_datehelper_time_in_words_hour_about_plural: asi %d hodinami +actionview_datehelper_time_in_words_hour_about_single: asi hodinou +actionview_datehelper_time_in_words_minute: 1 minutou +actionview_datehelper_time_in_words_minute_half: půl minutou +actionview_datehelper_time_in_words_minute_less_than: méně než minutou +actionview_datehelper_time_in_words_minute_plural: %d minutami +actionview_datehelper_time_in_words_minute_single: 1 minutou +actionview_datehelper_time_in_words_second_less_than: méně než sekundou +actionview_datehelper_time_in_words_second_less_than_plural: méně než %d sekundami actionview_instancetag_blank_option: Prosím vyberte activerecord_error_inclusion: není zahrnuto v seznamu @@ -98,7 +98,7 @@ mail_body_account_activation_request: Byl zaregistrován nový uživatel "%s". A gui_validation_error: 1 chyba gui_validation_error_plural: %d chyb(y) -field_name: Jméno +field_name: Název field_description: Popis field_summary: Přehled field_is_required: Povinné pole @@ -306,7 +306,7 @@ label_attribute: Atribut label_attribute_plural: Atributy label_download: %d Download label_download_plural: %d Downloads -label_no_data: Žádná data k zobrazení +label_no_data: Žádné položky label_change_status: Změnit stav label_history: Historie label_attachment: Soubor @@ -480,8 +480,8 @@ label_sort_by: Seřadit podle %s label_send_test_email: Poslat testovací email label_feeds_access_key_created_on: Přístupový klíč pro RSS byl vytvořen před %s label_module_plural: Moduly -label_added_time_by: 'Přidáno před: %s %s' -label_updated_time: 'Aktualizováno před: %s' +label_added_time_by: 'Přidáno uživatelem %s před %s' +label_updated_time: 'Aktualizováno před %s' label_jump_to_a_project: Zvolit projekt... label_file_plural: Soubory label_changeset_plural: Changesety @@ -586,7 +586,7 @@ text_no_configuration_data: "Role, fronty, stavy úkolů ani workflow nebyly zat text_load_default_configuration: Nahrát výchozí konfiguraci text_status_changed_by_changeset: Použito v changesetu %s. text_issues_destroy_confirmation: 'Opravdu si přejete odstranit všechny zvolené úkoly?' -text_select_project_modules: 'Zvolte moduly aktivní v tomto projektu:' +text_select_project_modules: 'Aktivní moduly v tomto projektu:' text_default_administrator_account_changed: Výchozí nastavení administrátorského účtu změněno text_file_repository_writable: Povolen zápis do repository text_rmagick_available: RMagick k dispozici (volitelné) @@ -620,5 +620,5 @@ default_activity_development: Vývoj enumeration_issue_priorities: Priority úkolů enumeration_doc_categories: Kategorie dokumentů enumeration_activities: Aktivity (sledování času) -error_scm_annotate: "The entry does not exist or can not be annotated." -label_planning: Planning +error_scm_annotate: "Položka neexistuje nebo nemůže být komentována." +label_planning: Plánování diff --git a/lang/es.yml b/lang/es.yml index 5d5492833..3f13716df 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -333,8 +333,8 @@ label_revision_plural: Revisiones label_added: añadido label_modified: modificado label_deleted: suprimido -label_latest_revision: La revisión más actual -label_latest_revision_plural: Las revisiones más actuales +label_latest_revision: Última revisión +label_latest_revision_plural: Últimas revisiones label_view_revisions: Ver las revisiones label_max_size: Tamaño máximo label_on: de @@ -372,7 +372,7 @@ label_view_diff: Ver diferencias label_diff_inline: en línea label_diff_side_by_side: cara a cara label_options: Opciones -label_copy_workflow_from: Copiar workflow desde +label_copy_workflow_from: Copiar flujo de trabajo desde label_permissions_report: Informe de permisos label_watched_issues: Peticiones monitorizadas label_related_issues: Peticiones relacionadas @@ -460,7 +460,7 @@ text_tip_task_begin_end_day: tarea que comienza y termina este día text_project_identifier_info: 'Letras minúsculas (a-z), números y signos de puntuación permitidos.
    Una vez guardado, el identificador no puede modificarse.' text_caracters_maximum: %d carácteres como máximo. text_length_between: Longitud entre %d y %d carácteres. -text_tracker_no_workflow: No hay ningún workflow definido para este tracker +text_tracker_no_workflow: No hay ningún flujo de trabajo definido para este tracker text_unallowed_characters: Carácteres no permitidos text_comma_separated: Múltiples valores permitidos (separados por coma). text_issues_ref_in_commit_messages: Referencia y petición de corrección en los mensajes @@ -559,64 +559,64 @@ field_searchable: Incluir en las búsquedas label_display_per_page: 'Por página: %s' setting_per_page_options: Objetos por página label_age: Edad -notice_default_data_loaded: 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 +notice_default_data_loaded: Configuración por defecto cargada correctamente. +text_load_default_configuration: Cargar la configuración por defecto +text_no_configuration_data: "Todavía no se han configurado roles, ni trackers, ni estados y flujo de trabajo asociado a peticiones. Se recomiendo encarecidamente cargar la configuración por defecto. Una vez cargada, podrá modificarla." +error_can_t_load_default_data: "No se ha podido cargar la configuración por defecto: %s" +button_update: Actualizar +label_change_properties: Cambiar propiedades label_general: General -label_repository_plural: Repositories -label_associated_revisions: Associated revisions -setting_user_format: Users display format -text_status_changed_by_changeset: Applied in changeset %s. -label_more: More -text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' +label_repository_plural: Repositorios +label_associated_revisions: Revisiones asociadas +setting_user_format: Formato de nombre de usuario +text_status_changed_by_changeset: Aplicado en los cambios %s +label_more: Más +text_issues_destroy_confirmation: '¿Seguro que quiere borrar las peticiones seleccionadas?' label_scm: SCM -text_select_project_modules: '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: 'Seleccione los módulos a activar para este proyecto:' +label_issue_added: Petición añadida +label_issue_updated: Petición actualizada +label_document_added: Documento añadido +label_message_posted: Mensaje añadido +label_file_added: Fichero añadido +label_news_added: Noticia añadida +project_module_boards: Foros +project_module_issue_tracking: Peticiones project_module_wiki: Wiki -project_module_files: 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: Ficheros +project_module_documents: Documentos +project_module_repository: Repositorio +project_module_news: Noticias +project_module_time_tracking: Control de tiempo +text_file_repository_writable: Se puede escribir en el repositorio +text_default_administrator_account_changed: Cuenta de administrador por defecto modificada +text_rmagick_available: RMagick disponible (opcional) +button_configure: Configurar label_plugins: Plugins -label_ldap_authentication: LDAP authentication +label_ldap_authentication: Autenticación 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 +label_this_month: este mes +label_last_n_days: últimos %d días +label_all_time: todo el tiempo +label_this_year: este año +label_date_range: Rango de fechas +label_last_week: última semana +label_yesterday: ayer +label_last_month: último mes +label_add_another_file: Añadir otro fichero +label_optional_description: Descripción opcional +text_destroy_time_entries_question: Existen %.02f horas asignadas a la petición que quiere borrar. ¿Qué quiere hacer ? +error_issue_not_found_in_project: 'La petición no se encuentra o no está asociada a este proyecto' +text_assign_time_entries_to_project: Asignar las horas al proyecto +text_destroy_time_entries: Borrar las horas +text_reassign_time_entries: 'Reasignar las horas a esta petición:' +setting_activity_days_default: Días a mostrar en la actividad de proyecto +label_chronological_order: En orden cronológico +field_comments_sorting: Mostrar comentarios +label_reverse_chronological_order: En orden cronológico inverso +label_preferences: Preferencias +setting_display_subprojects_issues: Mostrar peticiones de un subproyecto en el proyecto padre por defecto +label_overall_activity: Actividad global +setting_default_projects_public: Los proyectos nuevos son públicos por defecto +error_scm_annotate: "No existe la entrada o no ha podido ser anotada" +label_planning: Planificación diff --git a/lang/fi.yml b/lang/fi.yml index f345be139..0b47aee7f 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -614,6 +614,6 @@ field_comments_sorting: Näytä kommentit label_reverse_chronological_order: Käänteisessä aikajärjestyksessä label_preferences: Asetukset setting_default_projects_public: Uudet projektit ovat oletuksena julkisia -label_overall_activity: Kokonaisaktiviteetti -error_scm_annotate: "The entry does not exist or can not be annotated." -label_planning: Planning +label_overall_activity: Kokonaishistoria +error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä." +label_planning: Suunnittelu From 97fe797ad36389ea4719e90677408d9eed0cb369 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 13 Apr 2008 12:45:17 +0000 Subject: [PATCH 386/710] Replace closing html tags with html entity (#910). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1348 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 2 +- test/unit/helpers/application_helper_test.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 5ed23b8f7..7e0c71839 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -1134,7 +1134,7 @@ class RedCloth < String ALLOWED_TAGS = %w(redpre pre code) def escape_html_tags(text) - text.gsub!(%r{<((\/?)(\w+))}) {|m| ALLOWED_TAGS.include?($3) ? "<#{$1}" : "<#{$1}" } + text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' if $3}" } end end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 66499c003..f0de341c6 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -134,8 +134,9 @@ class ApplicationHelperTest < HelperTestCase def test_html_tags to_test = { - "
    content
    " => "

    <div>content</div>

    ", - "" => "

    <script>some script;</script>

    ", + "
    content
    " => "

    <div>content</div>

    ", + "
    content
    " => "

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

    ", + "" => "

    <script>some script;</script>

    ", # do not escape pre/code tags "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", From e64443571552e46fa1ecc34833e6f765b816a837 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 13 Apr 2008 13:24:03 +0000 Subject: [PATCH 387/710] Add css class for ticket changes (#1032). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1349 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_history.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml index 373758874..f29a44daf 100644 --- a/app/views/issues/_history.rhtml +++ b/app/views/issues/_history.rhtml @@ -1,5 +1,5 @@ <% for journal in journals %> -
    +

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

    From 0329094f015bcc2036fc1a2545253b8e187ee794 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 13 Apr 2008 16:22:55 +0000 Subject: [PATCH 388/710] Include macro can include a page of another project wiki using !{{include(projectname:Foo)}} (#1052). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1350 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/wiki_formatting/macros.rb | 29 +++++++++++--------- test/unit/helpers/application_helper_test.rb | 18 ++++++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb index f27ea98b9..0848aee4e 100644 --- a/lib/redmine/wiki_formatting/macros.rb +++ b/lib/redmine/wiki_formatting/macros.rb @@ -77,21 +77,24 @@ module Redmine content_tag('dl', out) end - desc "Include a wiki page. Example:\n\n !{{include(Foo)}}" + 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| - if @project && !@project.wiki.nil? - page = @project.wiki.find_page(args.first) - if page && page.content - @included_wiki_pages ||= [] - raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) - @included_wiki_pages << page.title - out = textilizable(page.content, :text, :attachments => page.attachments) - @included_wiki_pages.pop - out - else - raise "Page #{args.first} doesn't exist" - end + project = @project + title = args.first.to_s + if title =~ %r{^([^\:]+)\:(.*)$} + project_identifier, title = $1, $2 + project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier) end + raise 'Unknow project' unless project && User.current.allowed_to?(:view_wiki_pages, project) + raise 'No wiki for this project' unless !project.wiki.nil? + page = project.wiki.find_page(title) + raise "Page #{args.first} doesn't exist" unless page && page.content + @included_wiki_pages ||= [] + raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) + @included_wiki_pages << page.title + out = textilizable(page.content, :text, :attachments => page.attachments) + @included_wiki_pages.pop + out end end end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index f0de341c6..7ae6be4f3 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -168,6 +168,24 @@ class ApplicationHelperTest < HelperTestCase assert_equal '

    {{hello_world}}

    ', textilizable(text) end + def test_macro_include + @project = Project.find(1) + # include a page of the current project wiki + text = "{{include(Another page)}}" + assert textilizable(text).match(/This is a link to a ticket/) + + @project = nil + # include a page of a specific project wiki + text = "{{include(ecookbook:Another page)}}" + assert textilizable(text).match(/This is a link to a ticket/) + + text = "{{include(ecookbook:)}}" + assert textilizable(text).match(/CookBook documentation/) + + text = "{{include(unknowidentifier:somepage)}}" + assert textilizable(text).match(/Unknow project/) + end + def test_date_format_default today = Date.today Setting.date_format = '' From 741ddfb50b99b5193e1b5a0037cf406e4051df87 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 14 Apr 2008 19:13:22 +0000 Subject: [PATCH 389/710] Hide export links in print media css. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1351 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/stylesheets/application.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 7b73d0939..1df5b213c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -588,7 +588,7 @@ vertical-align: middle; /***** Media print specific styles *****/ @media print { - #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; } + #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } #main { background: #fff; } #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; } } From 277bee62eb15a4a32d720deb26d881922bae7ef6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 14 Apr 2008 19:21:58 +0000 Subject: [PATCH 390/710] Issue list shows all tickets when going back to a global query inside a project (closes #1055). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1352 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 369d888ef..0f1d84265 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -413,6 +413,7 @@ private else @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) + @query.project = @project end end end From 7061f73708336881660c763aa535c9b327610c70 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 15 Apr 2008 20:27:10 +0000 Subject: [PATCH 391/710] ApplicationHelperTest fix. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1353 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/unit/helpers/application_helper_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 7ae6be4f3..fa2109131 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -20,7 +20,7 @@ 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 + fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules def setup super From 1a4f81163d2c66e9827d674d32066865ef7e4bd0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 16 Apr 2008 17:27:53 +0000 Subject: [PATCH 392/710] Redirected user to where he is coming from after logging hours (#1062). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1354 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 2 +- app/helpers/application_helper.rb | 4 ++++ app/views/timelog/edit.rhtml | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 2c90093bd..29c2635d6 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -172,7 +172,7 @@ class TimelogController < ApplicationController @time_entry.attributes = params[:time_entry] if request.post? and @time_entry.save flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'details', :project_id => @time_entry.project + redirect_to(params[:back_url] || {:action => 'details', :project_id => @time_entry.project}) return end @activities = Enumeration::get_values('ACTI') diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index eb8cc2795..47a251053 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -427,6 +427,10 @@ module ApplicationHelper form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) end + def back_url_hidden_field_tag + hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER']) + end + def check_all_links(form_name) link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + " | " + diff --git a/app/views/timelog/edit.rhtml b/app/views/timelog/edit.rhtml index e221038a0..f9dae8a99 100644 --- a/app/views/timelog/edit.rhtml +++ b/app/views/timelog/edit.rhtml @@ -2,6 +2,7 @@ <% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %> <%= error_messages_for 'time_entry' %> +<%= back_url_hidden_field_tag %>

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

    From 76b8d3eff26b007791ebb58dd98546e0faa3938d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 24 Apr 2008 17:28:55 +0000 Subject: [PATCH 393/710] CVS duplicate key violation fix (#996, #1098). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1355 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/repository/cvs.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb index 7c01a27ee..c2d8be977 100644 --- a/app/models/repository/cvs.rb +++ b/app/models/repository/cvs.rb @@ -138,12 +138,18 @@ class Repository::Cvs < Repository end # Renumber new changesets in chronological order - c = changesets.find(:first, :order => 'committed_on DESC, id DESC', :conditions => "revision NOT LIKE '_%'") - next_rev = c.nil? ? 1 : (c.revision.to_i + 1) changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset| - changeset.update_attribute :revision, next_rev - next_rev += 1 + changeset.update_attribute :revision, next_revision_number end end # transaction end + + private + + # Returns the next revision number to assign to a CVS changeset + def next_revision_number + # Need to retrieve existing revision numbers to sort them as integers + @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0) + @current_revision_number += 1 + end end From 2b412e9e0795391b28ed991654e6add67f32d628 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 24 Apr 2008 17:35:04 +0000 Subject: [PATCH 394/710] Fixed: trying to preview a new issue raises an exception (closes #984). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1356 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 0f1d84265..84b95741e 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -344,7 +344,7 @@ class IssuesController < ApplicationController end def preview - @issue = @project.issues.find_by_id(params[:id]) if params[:id] + @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? @attachements = @issue.attachments if @issue @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil) render :partial => 'common/preview' From 596de073f95ff0fdcd7dcdf71bd91838be75756e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 24 Apr 2008 18:35:56 +0000 Subject: [PATCH 395/710] Commits per author graph: remove email adress in usernames (#1066). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1357 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 79fb49c86..198a9766c 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -260,6 +260,9 @@ private commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10 + # Remove email adress in usernames + fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') } + graph = SVG::Graph::BarHorizontal.new( :height => 300, :width => 500, From a9f86444fc5acd7502b72ea905f88952fc3c1434 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Apr 2008 09:49:16 +0000 Subject: [PATCH 396/710] Fixed: Modules selection not remembered when new project creation fails (#1109). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1358 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index afd7f6569..8bdcea8d7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -66,20 +66,20 @@ class ProjectsController < ApplicationController :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", :order => 'name') @project = Project.new(params[:project]) - @project.enabled_module_names = Redmine::AccessControl.available_project_modules 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 - @project.enabled_module_names = params[:enabled_modules] flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'admin', :action => 'projects' - end + end end end From 76b92fb999750402a448af0226c871bcef0ec2d9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Apr 2008 10:20:48 +0000 Subject: [PATCH 397/710] Warn user that subprojects are also deleted when deleting a project (#1111) and add a checkbox to confirm the deletion. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1359 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/projects/destroy.rhtml | 16 +++++++++------- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/da.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 3 ++- lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/no.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/uk.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + public/stylesheets/application.css | 4 ++-- 27 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/views/projects/destroy.rhtml b/app/views/projects/destroy.rhtml index 4531cb845..a1913c115 100644 --- a/app/views/projects/destroy.rhtml +++ b/app/views/projects/destroy.rhtml @@ -1,14 +1,16 @@

    <%=l(:label_confirmation)%>

    -
    -
    -

    <%=h @project_to_destroy.name %>
    -<%=l(:text_project_destroy_confirmation)%>

    +
    +

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

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

    -
    -
    \ No newline at end of file +
    diff --git a/lang/bg.yml b/lang/bg.yml index d1a60fe43..b341d989f 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -617,3 +617,4 @@ label_overall_activity: Цялостна дейност setting_default_projects_public: Новите проекти са публични по подразбиране error_scm_annotate: "Обектът не съществува или не може да бъде анотиран." label_planning: Планиране +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/cs.yml b/lang/cs.yml index 60fcb8d11..250c602c2 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -622,3 +622,4 @@ enumeration_doc_categories: Kategorie dokumentů enumeration_activities: Aktivity (sledování času) error_scm_annotate: "Položka neexistuje nebo nemůže být komentována." label_planning: Plánování +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/da.yml b/lang/da.yml index b0a6764bb..ff2ed982d 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -619,3 +619,4 @@ label_overall_activity: Overordnet aktivitet setting_default_projects_public: Nye projekter er offentlige som default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planlægning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/de.yml b/lang/de.yml index 323859ae4..77184cf88 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -618,3 +618,4 @@ default_activity_development: Entwicklung enumeration_issue_priorities: Ticket-Prioritäten enumeration_doc_categories: Dokumentenkategorien enumeration_activities: Aktivitäten (Zeiterfassung) +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/en.yml b/lang/en.yml index 8264ba908..e39aec301 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -557,6 +557,7 @@ text_select_mail_notifications: Select actions for which email notifications sho text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 means no restriction text_project_destroy_confirmation: Are you sure you want to delete this project and related data ? +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' text_workflow_edit: Select a role and a tracker to edit the workflow text_are_you_sure: Are you sure ? text_journal_changed: changed from %s to %s diff --git a/lang/es.yml b/lang/es.yml index 3f13716df..7ce4a7a8a 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -620,3 +620,4 @@ label_overall_activity: Actividad global setting_default_projects_public: Los proyectos nuevos son públicos por defecto error_scm_annotate: "No existe la entrada o no ha podido ser anotada" label_planning: Planificación +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/fi.yml b/lang/fi.yml index 0b47aee7f..68b6c20d7 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -617,3 +617,4 @@ setting_default_projects_public: Uudet projektit ovat oletuksena julkisia label_overall_activity: Kokonaishistoria error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä." label_planning: Suunnittelu +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/fr.yml b/lang/fr.yml index 602b3c8e1..cbdda4f3d 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -556,7 +556,8 @@ status_locked: vérouillé text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée text_regexp_info: ex. ^[A-Z0-9]+$ text_min_max_length_info: 0 pour aucune restriction -text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et tout ce qui lui est rattaché ? +text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et toutes ses données ? +text_subprojects_destroy_warning: 'Ses sous-projets: %s seront également supprimés.' text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow text_are_you_sure: Etes-vous sûr ? text_journal_changed: changé de %s à %s diff --git a/lang/he.yml b/lang/he.yml index 0cde264f7..a611c8c39 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -617,3 +617,4 @@ label_overall_activity: פעילות כוללת setting_default_projects_public: פרויקטים חדשים הינם פומביים כברירת מחדל error_scm_annotate: "הכניסה לא קיימת או שלא ניתן לתאר אותה." label_planning: תכנון +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/it.yml b/lang/it.yml index 7e9345b30..3d1dea09e 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -617,3 +617,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/ja.yml b/lang/ja.yml index 44d0c3ebd..680d29836 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -618,3 +618,4 @@ label_overall_activity: 全ての活動 setting_default_projects_public: デフォルトで新しいプロジェクトは公開にする error_scm_annotate: "エントリが存在しない、もしくはアノテートできません。" label_planning: 計画 +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/ko.yml b/lang/ko.yml index a8b105855..4281f3881 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -617,3 +617,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/lt.yml b/lang/lt.yml index 5db73d300..df7cd960b 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -618,3 +618,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/nl.yml b/lang/nl.yml index 6494eae07..e487a7a6d 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -618,3 +618,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/no.yml b/lang/no.yml index 25f4047ab..9ee42600b 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -618,3 +618,4 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/pl.yml b/lang/pl.yml index fdb8afaa2..81f03a62f 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -617,3 +617,4 @@ label_overall_activity: Ogólna aktywność setting_default_projects_public: Nowe projekty są domyślnie publiczne error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 218cbab51..9facd8d19 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -617,3 +617,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/pt.yml b/lang/pt.yml index e82176c56..6f51c8ed2 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -617,3 +617,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/ro.yml b/lang/ro.yml index 4d26ab103..59edfeb70 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -617,3 +617,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/ru.yml b/lang/ru.yml index 310ded30d..f69009847 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -621,3 +621,4 @@ label_overall_activity: Сводная активность setting_default_projects_public: Новые проекты являются публичными error_scm_annotate: "Данные отсутствуют или не могут быть подписаны." label_planning: Планирование +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/sr.yml b/lang/sr.yml index fa4ecd8de..d9869c362 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -618,3 +618,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/sv.yml b/lang/sv.yml index e1ac6b4bc..c0f691230 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -618,3 +618,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/uk.yml b/lang/uk.yml index 8fc418e67..a52a05603 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -619,3 +619,4 @@ label_overall_activity: Overall activity setting_default_projects_public: New projects are public by default error_scm_annotate: "The entry does not exist or can not be annotated." label_planning: Planning +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index e16fbe591..a0c7fafb3 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -618,3 +618,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/zh.yml b/lang/zh.yml index bff45a2ec..12fb8cb3e 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -618,3 +618,4 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) +text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 1df5b213c..26f66f0b8 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -259,7 +259,7 @@ p.other-formats { text-align: right; font-size:0.9em; color: #666; } a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } /***** Flash & error messages ****/ -#errorExplanation, div.flash, .nodata { +#errorExplanation, div.flash, .nodata, .warning { padding: 4px 4px 4px 30px; margin-bottom: 12px; font-size: 1.1em; @@ -282,7 +282,7 @@ div.flash.notice { color: #005f00; } -.nodata { +.nodata, .warning { text-align: center; background-color: #FFEBC1; border-color: #FDBF3B; From 1d570a40ff1eb51c2a2c7d807c742083c6cfbde6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Apr 2008 10:54:46 +0000 Subject: [PATCH 398/710] Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues (#1105). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1360 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/issue.rb | 2 ++ test/unit/issue_test.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/models/issue.rb b/app/models/issue.rb index d6fcf53f2..d6eab02fe 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -153,6 +153,8 @@ class Issue < ActiveRecord::Base # Close duplicates if the issue was closed if @issue_before_change && !@issue_before_change.closed? && self.closed? duplicates.each do |duplicate| + # Reload is need in case the duplicate was updated by a previous duplicate + duplicate.reload # Don't re-close it if it's already closed next if duplicate.closed? # Same user and notes diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index 7712b764e..3ceba1851 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -48,6 +48,8 @@ class IssueTest < Test::Unit::TestCase IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES) # And 3 is a dupe of 2 IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES) + # And 3 is a dupe of 1 (circular duplicates) + IssueRelation.create(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES) assert issue1.reload.duplicates.include?(issue2) From a6311a960370af3606ac3a18ce1d45facf318e22 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Apr 2008 11:59:51 +0000 Subject: [PATCH 399/710] Estimated time recognizes improved time formats (#1092). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1361 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/issue.rb | 6 +++- app/models/time_entry.rb | 14 +------- lib/redmine.rb | 1 + lib/redmine/core_ext.rb | 1 + lib/redmine/core_ext/string.rb | 5 +++ lib/redmine/core_ext/string/conversions.rb | 40 ++++++++++++++++++++++ test/unit/issue_test.rb | 7 ++++ 7 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 lib/redmine/core_ext.rb create mode 100644 lib/redmine/core_ext/string.rb create mode 100644 lib/redmine/core_ext/string/conversions.rb diff --git a/app/models/issue.rb b/app/models/issue.rb index d6eab02fe..8082e43b7 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -93,7 +93,11 @@ class Issue < ActiveRecord::Base self.priority = nil write_attribute(:priority_id, pid) end - + + def estimated_hours=(h) + write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) + end + def validate if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? errors.add :due_date, :activerecord_error_not_a_date diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index 0dce253c7..ddaff2b60 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -40,19 +40,7 @@ class TimeEntry < ActiveRecord::Base end def hours=(h) - s = h.dup - if s.is_a?(String) - s.strip! - unless s =~ %r{^[\d\.,]+$} - # 2:30 => 2.5 - s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 } - # 2h30, 2h, 30m - s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] } - end - # 2,5 => 2.5 - s.gsub!(',', '.') - end - write_attribute :hours, s + write_attribute :hours, (h.is_a?(String) ? h.to_hours : h) end # tyear, tmonth, tweek assigned where setting spent_on attributes diff --git a/lib/redmine.rb b/lib/redmine.rb index 5443eef4a..2697e8f5f 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -1,6 +1,7 @@ require 'redmine/access_control' require 'redmine/menu_manager' require 'redmine/mime_type' +require 'redmine/core_ext' require 'redmine/themes' require 'redmine/plugin' diff --git a/lib/redmine/core_ext.rb b/lib/redmine/core_ext.rb new file mode 100644 index 000000000..573313e74 --- /dev/null +++ b/lib/redmine/core_ext.rb @@ -0,0 +1 @@ +Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) } diff --git a/lib/redmine/core_ext/string.rb b/lib/redmine/core_ext/string.rb new file mode 100644 index 000000000..ce2646fb9 --- /dev/null +++ b/lib/redmine/core_ext/string.rb @@ -0,0 +1,5 @@ +require File.dirname(__FILE__) + '/string/conversions' + +class String #:nodoc: + include Redmine::CoreExtensions::String::Conversions +end diff --git a/lib/redmine/core_ext/string/conversions.rb b/lib/redmine/core_ext/string/conversions.rb new file mode 100644 index 000000000..7444445b0 --- /dev/null +++ b/lib/redmine/core_ext/string/conversions.rb @@ -0,0 +1,40 @@ +# redMine - project management software +# Copyright (C) 2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine #:nodoc: + module CoreExtensions #:nodoc: + module String #:nodoc: + # Custom string conversions + module Conversions + # Parses hours format and returns a float + def to_hours + s = self.dup + s.strip! + unless s =~ %r{^[\d\.,]+$} + # 2:30 => 2.5 + s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 } + # 2h30, 2h, 30m => 2.5, 2, 0.5 + s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] } + end + # 2,5 => 2.5 + s.gsub!(',', '.') + s.to_f + end + end + end + end +end diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index 3ceba1851..36ba1fb45 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -20,6 +20,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 + def test_create + issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30') + assert issue.save + issue.reload + assert_equal 1.5, issue.estimated_hours + end + def test_category_based_assignment issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1) assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to From 64474802f9dd6ad97aaf00e795fa1489c0af0042 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Apr 2008 16:55:24 +0000 Subject: [PATCH 400/710] Fixes custom field filters behaviour (#1078). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1362 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 6 +++--- test/unit/query_test.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index c54c143e2..641c0d17b 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -301,7 +301,7 @@ class Query < ActiveRecord::Base # custom field db_table = CustomValue.table_name db_field = 'value' - sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND " + sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE " else # regular field db_table = Issue.table_name @@ -320,9 +320,9 @@ 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" + sql = sql + "#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} = ''" when "*" - sql = sql + "#{db_table}.#{db_field} IS NOT NULL" + sql = sql + "#{db_table}.#{db_field} IS NOT NULL AND #{db_table}.#{db_field} <> ''" when ">=" sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}" when "<=" diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 3a9d19112..d291018fb 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -23,7 +23,7 @@ class QueryTest < Test::Unit::TestCase def test_query_with_multiple_custom_fields query = Query.find(1) assert query.valid? - assert query.statement.include?("custom_values.value IN ('MySQL')") + assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')") issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement assert_equal 1, issues.length assert_equal Issue.find(3), issues.first From ffbdc6b25bdce8abe83ee165b7a2ad6c7171f74b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Apr 2008 17:56:26 +0000 Subject: [PATCH 401/710] Postgresql 8.3 compatibility fix (#834). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1363 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/changeset.rb | 4 ++++ test/unit/changeset_test.rb | 12 ++++++------ test/unit/repository_bazaar_test.rb | 4 ++-- test/unit/repository_darcs_test.rb | 4 ++-- test/unit/repository_mercurial_test.rb | 4 ++-- test/unit/repository_subversion_test.rb | 4 ++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index ce9ea28ca..3e95ce111 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -35,6 +35,10 @@ class Changeset < ActiveRecord::Base validates_uniqueness_of :revision, :scope => :repository_id validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true + def revision=(r) + write_attribute :revision, (r.nil? ? nil : r.to_s) + end + def comments=(comment) write_attribute(:comments, comment.strip) end diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb index 2442a8b8c..bbfe6952d 100644 --- a/test/unit/changeset_test.rb +++ b/test/unit/changeset_test.rb @@ -41,22 +41,22 @@ class ChangesetTest < Test::Unit::TestCase end def test_previous - changeset = Changeset.find_by_revision(3) - assert_equal Changeset.find_by_revision(2), changeset.previous + changeset = Changeset.find_by_revision('3') + assert_equal Changeset.find_by_revision('2'), changeset.previous end def test_previous_nil - changeset = Changeset.find_by_revision(1) + changeset = Changeset.find_by_revision('1') assert_nil changeset.previous end def test_next - changeset = Changeset.find_by_revision(2) - assert_equal Changeset.find_by_revision(3), changeset.next + changeset = Changeset.find_by_revision('2') + assert_equal Changeset.find_by_revision('3'), changeset.next end def test_next_nil - changeset = Changeset.find_by_revision(4) + changeset = Changeset.find_by_revision('4') assert_nil changeset.next end end diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb index 15fcc8672..b7a3cf98e 100644 --- a/test/unit/repository_bazaar_test.rb +++ b/test/unit/repository_bazaar_test.rb @@ -36,13 +36,13 @@ class RepositoryBazaarTest < Test::Unit::TestCase assert_equal 4, @repository.changesets.count assert_equal 9, @repository.changes.count - assert_equal 'Initial import', @repository.changesets.find_by_revision(1).comments + assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments end def test_fetch_changesets_incremental @repository.fetch_changesets # Remove changesets with revision > 5 - @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy) + @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} @repository.reload assert_equal 2, @repository.changesets.count diff --git a/test/unit/repository_darcs_test.rb b/test/unit/repository_darcs_test.rb index 1228976f1..1c8c1b8dd 100644 --- a/test/unit/repository_darcs_test.rb +++ b/test/unit/repository_darcs_test.rb @@ -35,13 +35,13 @@ class RepositoryDarcsTest < Test::Unit::TestCase assert_equal 6, @repository.changesets.count assert_equal 13, @repository.changes.count - assert_equal "Initial commit.", @repository.changesets.find_by_revision(1).comments + assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments end def test_fetch_changesets_incremental @repository.fetch_changesets # Remove changesets with revision > 3 - @repository.changesets.find(:all, :conditions => 'revision > 3').each(&:destroy) + @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3} @repository.reload assert_equal 3, @repository.changesets.count diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb index e6cfdf9b2..21ddf1e3a 100644 --- a/test/unit/repository_mercurial_test.rb +++ b/test/unit/repository_mercurial_test.rb @@ -35,13 +35,13 @@ class RepositoryMercurialTest < Test::Unit::TestCase assert_equal 6, @repository.changesets.count assert_equal 11, @repository.changes.count - assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision(0).comments + assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments end def test_fetch_changesets_incremental @repository.fetch_changesets # Remove changesets with revision > 2 - @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy) + @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} @repository.reload assert_equal 3, @repository.changesets.count diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb index 879feece8..7a1c9df4a 100644 --- a/test/unit/repository_subversion_test.rb +++ b/test/unit/repository_subversion_test.rb @@ -35,13 +35,13 @@ class RepositorySubversionTest < Test::Unit::TestCase assert_equal 8, @repository.changesets.count assert_equal 16, @repository.changes.count - assert_equal 'Initial import.', @repository.changesets.find_by_revision(1).comments + assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments end def test_fetch_changesets_incremental @repository.fetch_changesets # Remove changesets with revision > 5 - @repository.changesets.find(:all, :conditions => 'revision > 5').each(&:destroy) + @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5} @repository.reload assert_equal 5, @repository.changesets.count From 6a3236daea38f0cf8b62ccf9f1212eb0f0395b08 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 27 Apr 2008 08:20:53 +0000 Subject: [PATCH 402/710] Include subprojects versions on calendar and gantt (#1116). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1364 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 6 ++++-- app/models/project.rb | 8 +++++--- app/views/common/_calendar.rhtml | 7 +++++-- app/views/projects/gantt.rhtml | 10 +++++++--- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8bdcea8d7..b71ec1ecd 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -341,8 +341,9 @@ class ProjectsController < ApplicationController :include => [:tracker, :status, :assigned_to, :priority, :project], :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] ) unless @selected_tracker_ids.empty? + events += Version.find(:all, :include => :project, + :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) end - events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) @calendar.events = events render :layout => false if request.xhr? @@ -386,8 +387,9 @@ class ProjectsController < ApplicationController :include => [:tracker, :status, :assigned_to, :priority, :project], :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] ) unless @selected_tracker_ids.empty? + @events += Version.find(:all, :include => :project, + :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to]) end - @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to]) @events.sort! {|x,y| x.start_date <=> y.start_date } if params[:format]=='pdf' diff --git a/app/models/project.rb b/app/models/project.rb index eaa33c969..964469649 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -75,12 +75,14 @@ class Project < ActiveRecord::Base conditions = nil if include_subprojects && !active_children.empty? ids = [id] + active_children.collect {|c| c.id} - conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"] + conditions = ["#{Project.table_name}.id IN (#{ids.join(',')})"] end - conditions ||= ["#{Issue.table_name}.project_id = ?", id] + conditions ||= ["#{Project.table_name}.id = ?", id] # Quick and dirty fix for Rails 2 compatibility Issue.send(:with_scope, :find => { :conditions => conditions }) do - yield + Version.send(:with_scope, :find => { :conditions => conditions }) do + yield + end end end diff --git a/app/views/common/_calendar.rhtml b/app/views/common/_calendar.rhtml index 7534a1223..1095cd501 100644 --- a/app/views/common/_calendar.rhtml +++ b/app/views/common/_calendar.rhtml @@ -19,12 +19,15 @@ while day <= calendar.enddt %> elsif day == i.due_date image_tag('arrow_to.png') end %> - <%= h("#{i.project.name} -") unless @project && @project == i.project %> + <%= h("#{i.project} -") unless @project && @project == i.project %> <%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %> <%= render_issue_tooltip i %>
    <% else %> - <%= link_to_version i, :class => "icon icon-package" %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i%> + <% end %> <% end %> diff --git a/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml index 05bd4b9bc..d941d2777 100644 --- a/app/views/projects/gantt.rhtml +++ b/app/views/projects/gantt.rhtml @@ -70,10 +70,13 @@ top = headers_height + 8 @events.each do |i| %>
    <% if i.is_a? Issue %> - <%= h("#{i.project.name} -") unless @project && @project == i.project %> + <%= h("#{i.project} -") unless @project && @project == i.project %> <%= link_to_issue i %>: <%=h i.subject %> <% else %> - <%= link_to_version i, :class => "icon icon-package" %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i %> + <% end %>
    <% top = top + 20 @@ -197,7 +200,8 @@ top = headers_height + 10 %>
     
    - <%= i.name %> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%=h i %>
    <% end %> <% top = top + 20 From a73f68a185df00db1aaa153f576f106ee752c54d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 27 Apr 2008 10:12:15 +0000 Subject: [PATCH 403/710] Fixed: Links to repository directories don't work (#1119). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1365 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 9 ++++++++- lib/redmine/scm/adapters/abstract_adapter.rb | 13 +++++++++++-- lib/redmine/scm/adapters/bazaar_adapter.rb | 12 ------------ lib/redmine/scm/adapters/cvs_adapter.rb | 9 --------- lib/redmine/scm/adapters/darcs_adapter.rb | 7 ------- lib/redmine/scm/adapters/git_adapter.rb | 8 -------- lib/redmine/scm/adapters/mercurial_adapter.rb | 8 -------- lib/redmine/scm/adapters/subversion_adapter.rb | 7 ------- .../repositories_bazaar_controller_test.rb | 8 ++++++++ test/functional/repositories_cvs_controller_test.rb | 8 ++++++++ test/functional/repositories_git_controller_test.rb | 8 ++++++++ .../repositories_mercurial_controller_test.rb | 10 +++++++++- .../repositories_subversion_controller_test.rb | 8 ++++++++ 13 files changed, 60 insertions(+), 55 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 198a9766c..64eb05793 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -65,7 +65,8 @@ class RepositoriesController < ApplicationController if request.xhr? @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) else - show_error_not_found unless @entries + show_error_not_found and return unless @entries + render :action => 'browse' end rescue Redmine::Scm::Adapters::CommandFailed => e show_error_command_failed(e.message) @@ -95,6 +96,12 @@ class RepositoriesController < ApplicationController end def entry + @entry = @repository.scm.entry(@path, @rev) + show_error_not_found and return unless @entry + + # If the entry is a dir, show the browser + browse and return if @entry.is_dir? + @content = @repository.scm.cat(@path, @rev) show_error_not_found and return unless @content if 'raw' == params[:format] || @content.is_binary_data? diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 41edf00ad..2c254d48d 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -59,8 +59,17 @@ module Redmine # Returns the entry identified by path and revision identifier # or nil if entry doesn't exist in the repository def entry(path=nil, identifier=nil) - e = entries(path, identifier) - e ? e.first : nil + parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?} + search_path = parts[0..-2].join('/') + search_name = parts[-1] + if search_path.blank? && search_name.blank? + # Root entry + Entry.new(:path => '', :kind => 'dir') + else + # Search for the entry in the parent directory + es = entries(search_path, identifier) + es ? es.detect {|e| e.name == search_name} : nil + end end # Returns an Entries collection diff --git a/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb index 11a44b7cf..2225a627c 100644 --- a/lib/redmine/scm/adapters/bazaar_adapter.rb +++ b/lib/redmine/scm/adapters/bazaar_adapter.rb @@ -44,18 +44,6 @@ module Redmine return nil end - # Returns the entry identified by path and revision identifier - # or nil if entry doesn't exist in the repository - def entry(path=nil, identifier=nil) - path ||= '' - parts = path.split(%r{[\/\\]}).select {|p| !p.blank?} - if parts.size > 0 - parent = parts[0..-2].join('/') - entries = entries(parent, identifier) - entries ? entries.detect {|e| e.name == parts.last} : nil - end - end - # Returns an Entries collection # or nil if the given path doesn't exist in the repository def entries(path=nil, identifier=nil) diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index 6085bfdbe..37920b599 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -55,15 +55,6 @@ module Redmine def get_previous_revision(revision) CvsRevisionHelper.new(revision).prevRev end - - # Returns the entry identified by path and revision identifier - # or nil if entry doesn't exist in the repository - # this method returns all revisions from one single SCM-Entry - def entry(path=nil, identifier="HEAD") - e = entries(path, identifier) - logger.debug(" #{e.first.inspect}") if e - e ? e.first : nil - end # Returns an Entries collection # or nil if the given path doesn't exist in the repository diff --git a/lib/redmine/scm/adapters/darcs_adapter.rb b/lib/redmine/scm/adapters/darcs_adapter.rb index 660b6cf8f..a1d1867b1 100644 --- a/lib/redmine/scm/adapters/darcs_adapter.rb +++ b/lib/redmine/scm/adapters/darcs_adapter.rb @@ -40,13 +40,6 @@ module Redmine rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil end - # Returns the entry identified by path and revision identifier - # or nil if entry doesn't exist in the repository - def entry(path=nil, identifier=nil) - e = entries(path, identifier) - e ? e.first : nil - end - # Returns an Entries collection # or nil if the given path doesn't exist in the repository def entries(path=nil, identifier=nil) diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index 5d315b0cc..77604f283 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -132,14 +132,6 @@ module Redmine entries.sort_by_name end - def entry(path=nil, identifier=nil) - path ||= '' - search_path = path.split('/')[0..-2].join('/') - entry_name = path.split('/').last - e = entries(search_path, identifier) - e ? e.detect{|entry| entry.name == entry_name} : nil - end - def revisions(path, identifier_from, identifier_to, options={}) revisions = Revisions.new cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw " diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 72db723ba..6f42dda06 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -59,14 +59,6 @@ module Redmine return nil if $? && $?.exitstatus != 0 entries.sort_by_name end - - def entry(path=nil, identifier=nil) - path ||= '' - search_path = path.split('/')[0..-2].join('/') - entry_name = path.split('/').last - e = entries(search_path, identifier) - e ? e.detect{|entry| entry.name == entry_name} : nil - end def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) revisions = Revisions.new diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index efbd3ba8e..40c7eb3f1 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -51,13 +51,6 @@ module Redmine return nil end - # Returns the entry identified by path and revision identifier - # or nil if entry doesn't exist in the repository - def entry(path=nil, identifier=nil) - e = entries(path, identifier) - e ? e.first : nil - end - # Returns an Entries collection # or nil if the given path doesn't exist in the repository def entries(path=nil, identifier=nil) diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb index 5a473bab3..acb6c1d21 100644 --- a/test/functional/repositories_bazaar_controller_test.rb +++ b/test/functional/repositories_bazaar_controller_test.rb @@ -99,6 +99,14 @@ class RepositoriesBazaarControllerTest < Test::Unit::TestCase assert @response.body.include?('Show help message') end + def test_directory_entry + get :entry, :id => 3, :path => ['directory'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entry) + assert_equal 'directory', assigns(:entry).name + end + def test_diff # Full diff of changeset 3 get :diff, :id => 3, :rev => 3 diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb index d6181ad36..e12bb53ac 100644 --- a/test/functional/repositories_cvs_controller_test.rb +++ b/test/functional/repositories_cvs_controller_test.rb @@ -101,6 +101,14 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :format => 'raw' assert_response :success end + + def test_directory_entry + get :entry, :id => 1, :path => ['sources'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end def test_diff Project.find(1).repository.fetch_changesets diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb index 10a4950f3..339e22897 100644 --- a/test/functional/repositories_git_controller_test.rb +++ b/test/functional/repositories_git_controller_test.rb @@ -101,6 +101,14 @@ class RepositoriesGitControllerTest < Test::Unit::TestCase assert @response.body.include?('WITHOUT ANY WARRANTY') end + def test_directory_entry + get :entry, :id => 3, :path => ['sources'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + def test_diff # Full diff of changeset 2f9c0091 get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7' diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb index b09265d13..cb870aa32 100644 --- a/test/functional/repositories_mercurial_controller_test.rb +++ b/test/functional/repositories_mercurial_controller_test.rb @@ -99,7 +99,15 @@ class RepositoriesMercurialControllerTest < Test::Unit::TestCase # File content assert @response.body.include?('WITHOUT ANY WARRANTY') end - + + def test_directory_entry + get :entry, :id => 3, :path => ['sources'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entry) + assert_equal 'sources', assigns(:entry).name + end + def test_diff # Full diff of changeset 4 get :diff, :id => 3, :rev => 4 diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index adb69c8e9..dd56947fc 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -89,6 +89,14 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_response :success end + def test_directory_entry + get :entry, :id => 1, :path => ['subversion_test', 'folder'] + assert_response :success + assert_template 'browse' + assert_not_nil assigns(:entry) + assert_equal 'folder', assigns(:entry).name + end + def test_diff get :diff, :id => 1, :rev => 3 assert_response :success From c72b53a8fa3c01ff0a80b34317fef80d97ee9c97 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 27 Apr 2008 16:34:42 +0000 Subject: [PATCH 404/710] Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present. If you don't want to upgrade rails gem, freeze Redmine by running the following command in your Redmine directory: rake rails:freeze:edge TAG=rel_2-0-2 git-svn-id: http://redmine.rubyforge.org/svn/trunk@1366 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- config/environment.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/environment.rb b/config/environment.rb index 2d581168b..7878eca47 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -4,6 +4,9 @@ # you don't control web/app server and can't set it the proper way # ENV['RAILS_ENV'] ||= 'production' +# Specifies gem version of Rails to use when vendor/rails is not present +RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION + # Bootstrap the Rails environment, frameworks, and default configuration require File.join(File.dirname(__FILE__), 'boot') From 67e7758185c6344f95eb9d071b869cdcb0ef9ed5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 28 Apr 2008 08:52:35 +0000 Subject: [PATCH 405/710] Translation updates (closes #1123, #1124): * Spanish (Gumer Coronel) * Norvegian (Kai Olav Fredriksen) git-svn-id: http://redmine.rubyforge.org/svn/trunk@1367 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/es.yml | 6 +++--- lang/no.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lang/es.yml b/lang/es.yml index 7ce4a7a8a..c6eef021a 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -432,7 +432,7 @@ button_move: Mover button_back: Atrás button_cancel: Cancelar button_activate: Activar -button_sort: Clasificar +button_sort: Ordenar button_log_time: Tiempo dedicado button_rollback: Volver a esta versión button_watch: Monitorizar @@ -448,7 +448,7 @@ status_locked: bloqueado text_select_mail_notifications: Seleccionar los eventos a notificar text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 para ninguna restricción -text_project_destroy_confirmation: ¿ Estás seguro de querer eliminar el proyecto ? +text_project_destroy_confirmation: ¿Estás seguro de querer eliminar el proyecto? text_workflow_edit: Seleccionar un flujo de trabajo para actualizar text_are_you_sure: ¿ Estás seguro ? text_journal_changed: cambiado de %s a %s @@ -620,4 +620,4 @@ label_overall_activity: Actividad global setting_default_projects_public: Los proyectos nuevos son públicos por defecto error_scm_annotate: "No existe la entrada o no ha podido ser anotada" label_planning: Planificación -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' +text_subprojects_destroy_warning: 'Sus subprojectos: %s también se eliminarán' diff --git a/lang/no.yml b/lang/no.yml index 9ee42600b..22b8c10af 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -165,7 +165,7 @@ field_url: URL field_start_page: Startside field_subproject: Underprosjekt field_hours: Timer -field_activity: Activitet +field_activity: Aktivitet field_spent_on: Dato field_identifier: Identifikasjon field_is_filter: Brukes som filter @@ -282,7 +282,7 @@ label_last_updates: Sist oppdatert label_last_updates_plural: %d siste oppdaterte label_registered_on: Registrert label_activity: Aktivitet -label_overall_activity: Total aktivitet +label_overall_activity: All aktivitet label_new: Ny label_logged_as: Innlogget som label_environment: Miljø @@ -557,6 +557,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_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 @@ -618,4 +619,3 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' From e55c1d82e633c692324c9b0289ffeeb15b11e383 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 28 Apr 2008 09:25:51 +0000 Subject: [PATCH 406/710] Notify project members when a message is posted if they want to receive notifications for everything on the project (#1079). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1368 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/message_observer.rb | 2 ++ test/functional/messages_controller_test.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/models/message_observer.rb b/app/models/message_observer.rb index c26805c1b..043988172 100644 --- a/app/models/message_observer.rb +++ b/app/models/message_observer.rb @@ -21,6 +21,8 @@ class MessageObserver < ActiveRecord::Observer recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?} # send notification to the board watchers recipients += message.board.watcher_recipients + # send notification to project members who want to be notified + recipients += message.board.project.recipients recipients = recipients.compact.uniq Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted') end diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb index dcfe0caa7..1fe8d086a 100644 --- a/test/functional/messages_controller_test.rb +++ b/test/functional/messages_controller_test.rb @@ -54,6 +54,9 @@ class MessagesControllerTest < Test::Unit::TestCase def test_post_new @request.session[:user_id] = 2 + ActionMailer::Base.deliveries.clear + Setting.notified_events << 'message_posted' + post :new, :board_id => 1, :message => { :subject => 'Test created message', :content => 'Message body'} @@ -63,6 +66,15 @@ class MessagesControllerTest < Test::Unit::TestCase assert_equal 'Message body', message.content assert_equal 2, message.author_id assert_equal 1, message.board_id + + mail = ActionMailer::Base.deliveries.last + assert_kind_of TMail::Mail, mail + assert_equal "[#{message.board.project.name} - #{message.board.name}] Test created message", mail.subject + assert mail.body.include?('Message body') + # author + assert mail.bcc.include?('jsmith@somenet.foo') + # project member + assert mail.bcc.include?('dlopper@somenet.foo') end def test_get_edit From 05d39eeb35c83b26bdad8ebddb55db1fdb89de2b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 28 Apr 2008 10:26:05 +0000 Subject: [PATCH 407/710] Changelog update. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1369 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/CHANGELOG | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 9ced8b4c0..b39185151 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -5,6 +5,38 @@ Copyright (C) 2006-2008 Jean-Philippe Lang http://www.redmine.org/ +== 2008-04-28 v0.7.0 + +* Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present +* Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list. +* Add predefined date ranges to the time report +* Time report can be done at issue level +* Various timelog report enhancements +* Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30 +* Display the context menu above and/or to the left of the click if needed +* Make the admin project files list sortable +* Mercurial: display working directory files sizes unless browsing a specific revision +* Preserve status filter and page number when using lock/unlock/activate links on the users list +* Redmine.pm support for LDAP authentication +* Better error message and AR errors in log for failed LDAP on-the-fly user creation +* Redirected user to where he is coming from after logging hours +* Warn user that subprojects are also deleted when deleting a project +* Include subprojects versions on calendar and gantt +* Notify project members when a message is posted if they want to receive notifications +* Fixed: Feed content limit setting has no effect +* Fixed: Priorities not ordered when displayed as a filter in issue list +* Fixed: can not display attached images inline in message replies +* Fixed: Boards are not deleted when project is deleted +* Fixed: trying to preview a new issue raises an exception with postgresql +* Fixed: single file 'View difference' links do not work because of duplicate slashes in url +* Fixed: inline image not displayed when including a wiki page +* Fixed: CVS duplicate key violation +* Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues +* Fixed: custom field filters behaviour +* Fixed: Postgresql 8.3 compatibility +* Fixed: Links to repository directories don't work + + == 2008-03-29 v0.7.0-rc1 * Overall activity view and feed added, link is available on the project list From 5f58e2ced2fd2329978e70744a4470a7dea42484 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 29 Apr 2008 09:57:47 +0000 Subject: [PATCH 408/710] Fixed: Search for target version of "none" fails with postgres 8.3 (#1134). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1379 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 12 ++++-- test/fixtures/custom_fields.yml | 1 + test/unit/query_test.rb | 74 +++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index 641c0d17b..d9a720812 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -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 @@ -296,11 +296,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 +322,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 "<=" diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index 6be840fcc..3a9e79a29 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -4,6 +4,7 @@ custom_fields_001: min_length: 0 regexp: "" is_for_all: true + is_filter: true type: IssueCustomField max_length: 0 possible_values: MySQL|PostgreSQL|Oracle diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index d291018fb..e143e6fc2 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -29,6 +29,80 @@ class QueryTest < Test::Unit::TestCase 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 = ''") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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 <> ''") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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 >=") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + end + + def test_operator_in_less_than + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('due_date', ' [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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%'") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + 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%'") + issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + end + def test_default_columns q = Query.new assert !q.columns.empty? From 4403e300ff2de9a442c83066065471aed80cb17a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 30 Apr 2008 08:47:14 +0000 Subject: [PATCH 409/710] Thai translation added (Gampol Thitinilnithi, #1136). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1383 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/th.yml | 623 ++++++++++++++++++ .../javascripts/calendar/lang/calendar-th.js | 127 ++++ .../jstoolbar/lang/jstoolbar-th.js | 14 + 3 files changed, 764 insertions(+) create mode 100644 lang/th.yml create mode 100644 public/javascripts/calendar/lang/calendar-th.js create mode 100644 public/javascripts/jstoolbar/lang/jstoolbar-th.js diff --git a/lang/th.yml b/lang/th.yml new file mode 100644 index 000000000..2c506c664 --- /dev/null +++ b/lang/th.yml @@ -0,0 +1,623 @@ +_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_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) และขีด (-) เท่านั้น.
    เมื่อจัดเก็บแล้ว, ชื่อเฉพาะไม่สามารถเปลี่ยนแปลงได้' +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: กิจกรรม (ใช้ในการติดตามเวลา) diff --git a/public/javascripts/calendar/lang/calendar-th.js b/public/javascripts/calendar/lang/calendar-th.js new file mode 100644 index 000000000..dc4809e52 --- /dev/null +++ b/public/javascripts/calendar/lang/calendar-th.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar EN language +// Author: Gampol Thitinilnithi, +// 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/public/javascripts/jstoolbar/lang/jstoolbar-th.js b/public/javascripts/jstoolbar/lang/jstoolbar-th.js new file mode 100644 index 000000000..2e2f2b88e --- /dev/null +++ b/public/javascripts/jstoolbar/lang/jstoolbar-th.js @@ -0,0 +1,14 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'หนา'; +jsToolBar.strings['Italic'] = 'เอียง'; +jsToolBar.strings['Underline'] = 'ขีดเส้นใต้'; +jsToolBar.strings['Deleted'] = 'ขีดฆ่า'; +jsToolBar.strings['Code'] = 'โค๊ดโปรแกรม'; +jsToolBar.strings['Heading 1'] = 'หัวข้อ 1'; +jsToolBar.strings['Heading 2'] = 'หัวข้อ 2'; +jsToolBar.strings['Heading 3'] = 'หัวข้อ 3'; +jsToolBar.strings['Unordered list'] = 'รายการ'; +jsToolBar.strings['Ordered list'] = 'ลำดับเลข'; +jsToolBar.strings['Preformatted text'] = 'รูปแบบข้อความคงที่'; +jsToolBar.strings['Wiki link'] = 'เชื่อมโยงไปหน้า Wiki อื่น'; +jsToolBar.strings['Image'] = 'รูปภาพ'; From 4525a7429aecbdf44a62fc3c8adb972727ba19ad Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 30 Apr 2008 08:51:12 +0000 Subject: [PATCH 410/710] Fixed: Home, Logout, Login links are absolute (#1122, #1145). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1384 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/redmine.rb b/lib/redmine.rb index 2697e8f5f..cef2615df 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -94,7 +94,7 @@ Redmine::AccessControl.map do |map| 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? } @@ -102,10 +102,10 @@ Redmine::MenuManager.map :top_menu do |menu| 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| From f1ae453688aef224d5d8dd9e715811a7517e1bb2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 30 Apr 2008 09:09:28 +0000 Subject: [PATCH 411/710] Fixed: Updating tickets add a time log with zero hours (#1147). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1385 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/core_ext/string/conversions.rb | 2 +- test/functional/issues_controller_test.rb | 25 ++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/redmine/core_ext/string/conversions.rb b/lib/redmine/core_ext/string/conversions.rb index 7444445b0..41149f5ea 100644 --- a/lib/redmine/core_ext/string/conversions.rb +++ b/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/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 042a8f3f2..a6d2ca6e3 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -169,13 +169,15 @@ class IssuesControllerTest < Test::Unit::TestCase :issue => {:tracker_id => 1, :subject => 'This is the test_new issue', :description => 'This is the description', - :priority_id => 5}, + :priority_id => 5, + :estimated_hours => ''}, :custom_fields => {'2' => 'Value for field 2'} assert_redirected_to 'issues/show' issue = Issue.find_by_subject('This is the test_new issue') assert_not_nil issue assert_equal 2, issue.author_id + assert_nil issue.estimated_hours v = issue.custom_values.find_by_custom_field_id(2) assert_not_nil v assert_equal 'Value for field 2', v.value @@ -254,10 +256,13 @@ class IssuesControllerTest < Test::Unit::TestCase 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 +293,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) From b2abe48592b3b2425d63bfbe2dc38e051bec272a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 30 Apr 2008 12:14:15 +0000 Subject: [PATCH 412/710] Use GET instead of POST on roadmap (#718), gantt and calendar forms. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1388 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/projects/calendar.rhtml | 2 +- app/views/projects/gantt.rhtml | 4 ++-- app/views/projects/roadmap.rhtml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/calendar.rhtml b/app/views/projects/calendar.rhtml index 743721cb3..048d8a5df 100644 --- a/app/views/projects/calendar.rhtml +++ b/app/views/projects/calendar.rhtml @@ -23,7 +23,7 @@ <% content_for :sidebar do %>

    <%= l(:label_calendar) %>

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

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

    diff --git a/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml index d941d2777..f398bace7 100644 --- a/app/views/projects/gantt.rhtml +++ b/app/views/projects/gantt.rhtml @@ -235,7 +235,7 @@ if Date.today >= @date_from and Date.today <= @date_to %> <% content_for :sidebar do %>

    <%= l(:label_gantt) %>

    - <% 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| %>
    <% end %> @@ -243,7 +243,7 @@ if Date.today >= @date_from and Date.today <= @date_to %>
    <%= hidden_field_tag 'with_subprojects', 0 %> <% end %> -

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

    +

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

    <% end %> <% end %> diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml index d9329d109..ba0bb3ebd 100644 --- a/app/views/projects/roadmap.rhtml +++ b/app/views/projects/roadmap.rhtml @@ -30,7 +30,7 @@ <% end %> <% content_for :sidebar do %> -<% form_tag do %> +<% form_tag({}, :method => :get) do %>

    <%= l(:label_roadmap) %>

    <% @trackers.each do |tracker| %>
    - +
    <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %> <% field = filter[0] options = filter[1] %> - id="tr_<%= field %>"> - id="tr_<%= field %>" class="filter"> + - -
    +
    <%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %> + <%= select_tag "operators[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %> +
    + <%= l(:label_filter_add) %>: <%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/\_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact), :onchange => "add_filter();", :class => "select-small" %>
    #{result}
    ", textilizable(text).gsub(/[\t\n]/, '') } end + def test_wiki_horizontal_rule + assert_equal '
    ', textilizable('---') + assert_equal '

    Dashes: ---

    ', textilizable('Dashes: ---') + end + def test_macro_hello_world text = "{{hello_world}}" assert textilizable(text).match(/Hello world!/) From 2d11265ec5e1696769bbd0f115cc14d2461c588f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 1 May 2008 12:56:59 +0000 Subject: [PATCH 414/710] Fixed: private subprojects names are revealed on the project overview (#1152). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1399 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 2 +- test/fixtures/members.yml | 6 ++++++ test/fixtures/projects.yml | 14 +++++++++++++- test/functional/projects_controller_test.rb | 15 +++++++++++++++ test/unit/project_test.rb | 2 +- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b71ec1ecd..34ce734a5 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -87,7 +87,7 @@ class ProjectsController < ApplicationController def show @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} - @subprojects = @project.active_children + @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 diff --git a/test/fixtures/members.yml b/test/fixtures/members.yml index 2c9209131..32c65c673 100644 --- a/test/fixtures/members.yml +++ b/test/fixtures/members.yml @@ -24,4 +24,10 @@ members_004: role_id: 2 # Locked user user_id: 5 +members_005: + id: 5 + created_on: 2006-07-19 19:35:33 +02:00 + project_id: 5 + role_id: 1 + user_id: 2 \ No newline at end of file diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index ad5cf4aa2..8e1b3fe1d 100644 --- a/test/fixtures/projects.yml +++ b/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/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index eb5795152..5af7b5572 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -63,6 +63,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 diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index 9af68c231..f24e7d44f 100644 --- a/test/unit/project_test.rb +++ b/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 From f705b889d9d20086094939b1e7060bf49bd01f2b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 1 May 2008 13:01:23 +0000 Subject: [PATCH 415/710] Hide the 'Latest projects' box on the welcome screen if there are no projects (#1156). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1400 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/welcome/index.rhtml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/welcome/index.rhtml b/app/views/welcome/index.rhtml index 5da5a1ed3..8c25897bd 100644 --- a/app/views/welcome/index.rhtml +++ b/app/views/welcome/index.rhtml @@ -12,6 +12,7 @@
    + <% if @projects.any? %>

    <%=l(:label_project_latest)%>

      @@ -22,7 +23,8 @@ <% end %>
    -
    +
    + <% end %>
    <% content_for :header_tags do %> From 7f8d959171c571eaf1a49951a2da02ccffa6f808 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 1 May 2008 13:50:39 +0000 Subject: [PATCH 416/710] Show the project hierarchy in the drop down list for new membership on user administration screen. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1401 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/users_helper.rb | 14 ++++++++++++++ app/views/users/_memberships.rhtml | 12 ++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 250ed8ce8..0fe1755eb 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -23,6 +23,20 @@ module UsersHelper [l(:status_locked), 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) url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]} diff --git a/app/views/users/_memberships.rhtml b/app/views/users/_memberships.rhtml index 2499ba387..44d74ef7b 100644 --- a/app/views/users/_memberships.rhtml +++ b/app/views/users/_memberships.rhtml @@ -13,17 +13,17 @@

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


    <% form_tag({ :action => 'edit_membership', :id => @user }) do %> - - +<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %> +<%= l(:label_role) %>: +<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %> <%= submit_tag l(:button_add) %> <% end %>

    +<% end %>
    \ No newline at end of file From 7a969dafacf06f9079a925f69b62dbad81c3aba4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 2 May 2008 15:16:17 +0000 Subject: [PATCH 417/710] Escape HTML comment tags (#1160). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1403 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 2 +- test/unit/helpers/application_helper_test.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 7e0c71839..7729ced46 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -1134,7 +1134,7 @@ class RedCloth < String ALLOWED_TAGS = %w(redpre pre code) def escape_html_tags(text) - text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' if $3}" } + text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" } end end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 182cd3aec..e8b5883ce 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -141,6 +141,8 @@ class ApplicationHelperTest < HelperTestCase "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", "
    \nline 1\nline2
    " => "
    \nline 1\nline2
    ", "
    content
    " => "
    <div>content</div>
    ", + "HTML comment: " => "

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

    ", + "
    -

    <%=l(:label_information_plural)%>

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

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

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

    diff --git a/app/views/users/_general.rhtml b/app/views/users/_general.rhtml new file mode 100644 index 000000000..80615ff6c --- /dev/null +++ b/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/app/views/users/_memberships.rhtml b/app/views/users/_memberships.rhtml index 44d74ef7b..94b49159e 100644 --- a/app/views/users/_memberships.rhtml +++ b/app/views/users/_memberships.rhtml @@ -1,21 +1,33 @@ -
    -

    <%= l(:label_project_plural) %>

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

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

    -<% end %> +<% if @memberships.any? %> + + + + + + + + <% @memberships.each do |membership| %> + <% next if membership.new_record? %> + + + + + + +<% end; reset_cycle %> +
    <%= l(:label_project) %><%= l(:label_role) %>
    <%=h membership.project %> + <% 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 %> + + <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %> +
    +<% else %> +

    <%= l(:label_no_data) %>

    <% end %> <% if @projects.any? %> -


    <% form_tag({ :action => 'edit_membership', :id => @user }) do %> @@ -26,4 +38,3 @@ <% end %>

    <% end %> -
    \ No newline at end of file diff --git a/app/views/users/edit.rhtml b/app/views/users/edit.rhtml index 0da99d0d2..a4d0b2e5b 100644 --- a/app/views/users/edit.rhtml +++ b/app/views/users/edit.rhtml @@ -1,8 +1,23 @@ -

    <%=l(:label_user)%>

    +

    <%=l(:label_user)%>: <%=h @user.login %>

    -<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %> -<%= render :partial => 'form', :locals => { :f => f } %> -<%= submit_tag l(:button_save) %> -<% end %> +<% selected_tab = params[:tab] ? params[:tab].to_s : user_settings_tabs.first[:name] %> -<%= render :partial => 'memberships' %> \ No newline at end of file +
    +
      +<% user_settings_tabs.each do |tab| -%> +
    • <%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %>
    • +<% end -%> +
    +
    + +<% 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)) -%> From c65d696f41a1657afaf543a8410500e7dc9ec531 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 2 May 2008 15:35:41 +0000 Subject: [PATCH 419/710] Fixed: error when using upcase language name in coderay (#1162) (Windows specific). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1405 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/wiki_formatting.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 553f77132..8866e8cde 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -56,7 +56,7 @@ module Redmine content = @pre_list[$1.to_i] if content.match(/\s?(.+)/m) content = "" + - CodeRay.scan($2, $1).html(:escape => false, :line_numbers => :inline) + CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline) end content end From 251b1f75e9407d28ef8e952266a9299293a9f040 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 2 May 2008 16:48:09 +0000 Subject: [PATCH 420/710] Fixed: error on Trac import when :due attribute is nil (#1164). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1406 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 7fe1f09ac..48a774eef 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -92,7 +92,7 @@ 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 From 35321d938bfbad15f0cfd3fb32f10b68e7e6cac9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 3 May 2008 10:41:28 +0000 Subject: [PATCH 421/710] Translations updates (closes #1128, #1129, #1135, #1148, #1163). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1409 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/fi.yml | 6 +++--- lang/no.yml | 2 +- lang/pl.yml | 8 ++++---- lang/zh-tw.yml | 4 ++-- lang/zh.yml | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lang/fi.yml b/lang/fi.yml index 68b6c20d7..1a614a16c 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -307,8 +307,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ä @@ -617,4 +617,4 @@ 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 alaprojekti(t): %s tullaan myös poistamaan.' diff --git a/lang/no.yml b/lang/no.yml index 22b8c10af..ccfa5c99f 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -557,7 +557,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 diff --git a/lang/pl.yml b/lang/pl.yml index 81f03a62f..cf101811a 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -114,7 +114,7 @@ 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 @@ -331,7 +331,7 @@ label_modification_plural: %d modyfikacja label_revision: Zmiana label_revision_plural: Zmiany label_added: dodane -label_modified: zmodufikowane +label_modified: zmodyfikowane label_deleted: usunięte label_latest_revision: Ostatnia zmiana label_latest_revision_plural: Ostatnie zmiany @@ -469,7 +469,7 @@ 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 +483,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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index a0c7fafb3..d91c9708d 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -210,7 +210,7 @@ setting_protocol: 協定 setting_per_page_options: 每頁顯示個數選項 setting_user_format: 使用者顯示格式 setting_activity_days_default: 專案活動顯示天數 -setting_display_subprojects_issues: 預設於主控專案中顯示從屬專案的項目 +setting_display_subprojects_issues: 預設於父專案中顯示子專案的項目 project_module_issue_tracking: 項目追蹤 project_module_time_tracking: 工時追蹤 @@ -557,6 +557,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 @@ -618,4 +619,3 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' diff --git a/lang/zh.yml b/lang/zh.yml index 12fb8cb3e..c8b809764 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -554,9 +554,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 @@ -618,4 +619,3 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) -text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.' From 9e5624c362ad2a38c52d1b36a4f01bd051114486 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 3 May 2008 15:09:06 +0000 Subject: [PATCH 422/710] Prevent "can't convert nil into String" error when :sort_order param is not present. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1410 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/sort_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index f16ff3c7d..9ca5c11bd 100644 --- a/app/helpers/sort_helper.rb +++ b/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. From 556df99456bf579057213157eb593c6a0dea0676 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 4 May 2008 10:20:16 +0000 Subject: [PATCH 423/710] Doc update for 0.7.1 release. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1412 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/CHANGELOG | 15 +++++++++++++++ doc/INSTALL | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/doc/CHANGELOG b/doc/CHANGELOG index b39185151..3a39061c6 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -5,6 +5,21 @@ Copyright (C) 2006-2008 Jean-Philippe Lang http://www.redmine.org/ +== 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/doc/INSTALL b/doc/INSTALL index 7a00b9367..872543d95 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -1,7 +1,7 @@ == Redmine installation Redmine - project management software -Copyright (C) 2006-2007 Jean-Philippe Lang +Copyright (C) 2006-2008 Jean-Philippe Lang http://www.redmine.org/ From 04766697357b29521515e7c71ae5139e04dd5ba2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 4 May 2008 15:05:38 +0000 Subject: [PATCH 424/710] Wiki page protection (#851, patch #1146 by Mateo Murphy with slight changes). New permission added: protect wiki pages. Once a page is protected, it can be edited/renamed/deleted only by users who have this permission. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1415 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/wiki_controller.rb | 22 +++++++- app/models/wiki_page.rb | 5 ++ app/views/wiki/show.rhtml | 8 ++- db/migrate/093_add_wiki_pages_protected.rb | 9 ++++ lib/redmine.rb | 1 + public/images/locked.png | Bin 566 -> 1127 bytes public/images/unlock.png | Bin 643 -> 618 bytes test/fixtures/roles.yml | 2 + test/fixtures/wiki_pages.yml | 4 ++ test/functional/wiki_controller_test.rb | 56 +++++++++++++++++++++ 10 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 db/migrate/093_add_wiki_pages_protected.rb diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 53c5ec53b..44113ebf3 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -21,7 +21,7 @@ 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 +48,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 +84,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 +95,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 +131,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 @@ -152,6 +162,7 @@ class WikiController < ApplicationController def preview page = @wiki.find_page(params[:page]) + return render_403 unless editable?(page) @attachements = page.attachments if page @text = params[:content][:text] render :partial => 'common/preview' @@ -159,12 +170,14 @@ class WikiController < ApplicationController 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 +191,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/app/models/wiki_page.rb b/app/models/wiki_page.rb index 8ce71cb80..95750f37b 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -105,6 +105,11 @@ 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 end class WikiDiff diff --git a/app/views/wiki/show.rhtml b/app/views/wiki/show.rhtml index e4413d090..8092525bd 100644 --- a/app/views/wiki/show.rhtml +++ b/app/views/wiki/show.rhtml @@ -1,8 +1,12 @@
    +<% 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') %>
    @@ -22,9 +26,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') %>

    <%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", :id => 'attach_files_link' %>

    <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> diff --git a/db/migrate/093_add_wiki_pages_protected.rb b/db/migrate/093_add_wiki_pages_protected.rb new file mode 100644 index 000000000..49720fbb7 --- /dev/null +++ b/db/migrate/093_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/lib/redmine.rb b/lib/redmine.rb index cef2615df..b21f5716d 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -76,6 +76,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| diff --git a/public/images/locked.png b/public/images/locked.png index c2789e35cbe70bd4d7bbf999158c5b6f4db451c2..82d629961912b414224d483158997d0318a74acb 100644 GIT binary patch literal 1127 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!UWsc&iE~kEVo7Fxofw`4|k%G2?p@G5Sw|;Fvo$Mu^zOL*ySXua_lyl#A zr2&OF3p^r=85p=efH0%e8j~47L6&q!Uq=Rpjs4tz5?L7-m>B|mLR=@+rp<26zP~;D z-j2Md2QyzB$$NRK^yTTwEnBv{xzzOWdgtfclfT|y{QchI4|jKbytnhyyA{{)5BFWWcJ0fPLtmaA`u6nDmuE-5K0o^H<*{$CPJDlT`un@H-`}18 z_3q4%cNczty8h?Or9WS;{P}d}&!>BTzdii_|3A=SqhK@y1}X&3KV>=sv|GC*$S)X} z8i@o9$|sj!0BWoBba4!+xK(=Yq)?NA0LujnmQ5Z?iH2HQo815ZXJ$McvM}_^!sWl_ zw$=P|`m=FP@$Sad^^X!Ijq?~^b4wZAV!o1VzHhF!cf>>0KCN)C7y&J{r;2?yB_B$9 zdT!Fu5|CSHxY97OsbDz+BZKYRLtn3bcfOp#Yu*1hg-JbCYEi$GeGe$yJYD@<);T3K F0RZk%`04-v delta 522 zcmV+l0`>jp2(|=}H-B?sZ7v`-AZlT5b}k??FfcbDGBG+bG&(XdAS*C2Ffh8@)=cO^mj48E@Lf+QK_vhC4tttPtIkki-ds-OptvSY&G5@VGt$Qiw*0sHiDTHGo z=dD7udny0DN^?>eLNO5UtwQIuTIa1w|GiT8#%tE4Qt!QE_q}8P#&h?^bML)t|IT~& z&U^R9d+)}3=YO?p|Nr;z#)RIjWAD9lt$QK8y}j?}*6-fE=gyqgwS>l{Yqgwf#>U3Z z&d%1>)(TKcx&QzG0d!JMQvg8b*k%9#0I5ktK~#9!P0rgEfj|@n;7;Vwq%fI;oFY+F zLOC_P|3hhN*5tp~YyIEe7n|cqB#cGwFBlg^E;U`mIDgNJTwqWZGH?U9JTHjSq-pki zy+7ZquGi!i-=idnwa2W2k$b*`VHCxtMd5hzeLn~s27C9Qd!DB-SgqC@v~_k2Ae=7{ zvTbuoVG6AYLI9Wsg)uZJ$O2EK*H?h54uGZ)>(6*Wlv-`M)2-G&N}d$~0Hf^`00000 MNkvXXt^-0~f~wp8kpKVy diff --git a/public/images/unlock.png b/public/images/unlock.png index e0d414978ad0a8d17ba340276191bce671fc43f4..f15fead727a16a9c79e86a60b37ae2dbd2b27724 100644 GIT binary patch delta 593 zcmV-X0V>IRB3Hx0Bm(`E+8>CAZ=lCb09J>FgPGF zGCD9fIxsRID=;xIFvIOmh5!Hn2XskIMF-dg1P(I~8*zDV00009a7bBm000W`000W` z0Ya=am;e9(1ZP1_K>z@;j|==^1poj64p2-~MgRZ*`PtR|;(z4%-{ADr&hXv1-?wY| z-PiGPcl2Ut_1@e2+1d0_|eVu<-+#m!u#Cd`fhUgXkhYUW%#{nU$R!;6v zRr%A`_2UByet-RQb^LE|`)hIcV`}@}-S+Cp>B@Ed>FW50kNkpq`+9%;x4QXrclvK` z_G)wZX z!otJZpaLGAUfw>wzMg*m0T2OOJ9`I5CubK|H+P7Dk+F%XnYo3fm9-5-KtoeYTSr$< z-@p)RhDww)8=JCAqjIBI|nBh8#fOxAKdYb fOhAH}g@FM8N3$56Q!blL00000NkvXXu0mjfJ!Uk9 delta 618 zcmWlVZ%7ki0LI_TiI*Ck_J^Z{HYo6ja_=x@GisZz@<^Gs54j>-r;|CR)kc3faHt~; zF8af)53WLDU!qsZxcXpM1hydR=nuYG)Z2%kxB7As#gQr3dU&1>&+pswe7B93Ufi7C zmI@q9HJ|8>q+){u_E0R*3wmOS?RHnAJrRl7T~24C9lAZP2G5Rq`&K7(I-@&1699m+ zRBKzH>}~1Bs+!6n7xT4w-Rb6#EEvdL z$U-$ESO_|JmSUCUp<7^8S+FcCN3$a2Ow07^FdKJp98e@jmMUTFmqcFG6%q4tn#OSs z5(Gt+Q}cqN@S^Sz3CPo&WtnCnV`y;$i3mBM#_ z1Hk)bAzCbIYl|;*`hA=JWc^4G*n%x^)2=m@!K&HWk|t2HZrQ546pA*Q&yR9*W50a2 zBY{0LC%dwX#?wci&V1{rzFzn4!wvm`{m9h)pMx{u!F#jI!%eq(oi*nl_jo4it{i@W zytP%akkoy5{_M4Z>FMT&K?k$yTt6D{cMS8s@n@cwU6*ZMrlHR(j@FMyQe#^t%a4nH zx$?a~YA2cKo4L=wnHsZ%XE*Fl)b^k1Zoj%%{%6JfO+M<%FB~c^0Ih-cmMMSt$Uo=z B6W;&; diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index 1ede6fca9..e7b142268 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -29,6 +29,7 @@ roles_001: - :manage_documents - :view_wiki_pages - :edit_wiki_pages + - :protect_wiki_pages - :delete_wiki_pages - :rename_wiki_pages - :add_messages @@ -69,6 +70,7 @@ roles_002: - :manage_documents - :view_wiki_pages - :edit_wiki_pages + - :protect_wiki_pages - :delete_wiki_pages - :add_messages - :manage_boards diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml index f89832e44..ef7242430 100644 --- a/test/fixtures/wiki_pages.yml +++ b/test/fixtures/wiki_pages.yml @@ -4,19 +4,23 @@ wiki_pages_001: title: CookBook_documentation id: 1 wiki_id: 1 + protected: true wiki_pages_002: created_on: 2007-03-08 00:18:07 +01:00 title: Another_page id: 2 wiki_id: 1 + protected: false wiki_pages_003: created_on: 2007-03-08 00:18:07 +01:00 title: Start_page id: 3 wiki_id: 2 + protected: false 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 \ No newline at end of file diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index bf31e6614..8688c2e03 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -160,4 +160,60 @@ class WikiControllerTest < Test::Unit::TestCase 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 From c4560c4f3be39dd4cb9505c3e08ebcca0dad1799 Mon Sep 17 00:00:00 2001 From: Liwiusz Ociepa Date: Tue, 13 May 2008 09:41:19 +0000 Subject: [PATCH 425/710] Merge changes from branch swistak. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1425 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 136 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm index 6f3ba4385..8003cabc0 100644 --- a/extra/svn/Redmine.pm +++ b/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 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 - ## for postgres - # PerlSetVar dsn DBI:Pg:dbname=databasename;host=my.db.server + RedmineDSN "DBI:mysql:database=databasename;host=my.db.server" + ## for postgres (there is memory leak in libpq+ssl) + # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server;sslmode=disable" - PerlSetVar db_user redmine - PerlSetVar db_pass password + 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 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); @@ -182,9 +265,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 +304,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 +331,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; From 77e87eb0bad7afb83ba569ef42d9b0dda9aa44e9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 13 May 2008 16:56:34 +0000 Subject: [PATCH 426/710] Fixed: some labels overflow the form box (#1228). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1426 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/stylesheets/application.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 26f66f0b8..18e1de621 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -223,6 +223,8 @@ height: 1%; clear:left; } +html>body .tabular p {overflow:auto;} + .tabular label{ font-weight: bold; float: left; From 75d25b7e61c16b732e340073af6d928ced3f8ae3 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 13 May 2008 17:51:55 +0000 Subject: [PATCH 427/710] Fixed: Update issue form: comment field from log time end out of screen (#1227). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1427 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/_edit.rhtml | 2 +- public/stylesheets/application.css | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/issues/_edit.rhtml b/app/views/issues/_edit.rhtml index 2e00ab520..49f198a63 100644 --- a/app/views/issues/_edit.rhtml +++ b/app/views/issues/_edit.rhtml @@ -21,9 +21,9 @@

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

    -

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

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

    +

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

    <% end %> <% end %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 18e1de621..8e4bf995c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -241,6 +241,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; } From 835cc7ed7c6b9257d7e5e2877f95f26edc013b5b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 13 May 2008 17:54:51 +0000 Subject: [PATCH 428/710] Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant (#1207). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1428 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/git_adapter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index 77604f283..a9ab505a8 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -27,8 +27,8 @@ 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 + cmd="#{GIT_BIN} --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?) + cmd="#{GIT_BIN} --git-dir #{target('')} log -1 master -- #{shell_quote path}" if rev=='latest' or rev.nil? rev=[] i=0 From 4c6b9d9ce506253cae52bfc0d716bd67ecebb29f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 14 May 2008 17:23:40 +0000 Subject: [PATCH 429/710] Fixed: Check All / Uncheck All in Email Settings doesn't work (#1180). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1429 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/settings/_notifications.rhtml | 4 ++-- public/javascripts/application.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/settings/_notifications.rhtml b/app/views/settings/_notifications.rhtml index ac3213853..1a472d606 100644 --- a/app/views/settings/_notifications.rhtml +++ b/app/views/settings/_notifications.rhtml @@ -9,13 +9,13 @@ <%= hidden_field_tag 'settings[bcc_recipients]', 0 %>

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

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

    +

    <%= check_all_links('notified_events') %>

    <%= l(:setting_emails_footer) %> diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 4e8849842..f3d771a10 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -2,10 +2,10 @@ 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; } } } From 06e44b8e643797c8d5a11478ef6605f49c7220e0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 14 May 2008 17:26:13 +0000 Subject: [PATCH 430/710] Do not toggle assignable check box when using un/check all permissions links on role form. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1430 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/roles/_form.rhtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/roles/_form.rhtml b/app/views/roles/_form.rhtml index 58dc2af41..4aad45471 100644 --- a/app/views/roles/_form.rhtml +++ b/app/views/roles/_form.rhtml @@ -12,7 +12,7 @@ <% end %>

    <%= l(:label_permissions) %>

    -
    +
    <% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %> <% perms_by_module.keys.sort.each do |mod| %>
    <%= mod.blank? ? l(:label_project) : mod.humanize %> @@ -24,6 +24,6 @@ <% end %>
    <% end %> -
    <%= check_all_links 'role_form' %> +
    <%= check_all_links 'permissions' %> <%= hidden_field_tag 'role[permissions][]', '' %>
    From 7ee38a95a0052ddc544137f32fcf9114e5ffabb9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 14 May 2008 18:01:13 +0000 Subject: [PATCH 431/710] Fixed: Calendar and Gantt show private subprojects even if current user is not a member of them (#1217). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1431 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 7 +++--- test/fixtures/issues.yml | 18 ++++++++++++++- test/functional/projects_controller_test.rb | 25 +++++++++++++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 964469649..8c32c8562 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -73,9 +73,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 @@ -93,6 +93,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? diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml index 4f42d93c4..48195a7b7 100644 --- a/test/fixtures/issues.yml +++ b/test/fixtures/issues.yml @@ -71,4 +71,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/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 5af7b5572..bebe96f29 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -29,6 +29,7 @@ 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 @@ -237,11 +238,21 @@ 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 @@ -251,13 +262,23 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_not_nil assigns(:events) 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 From 9e225cc63ff889d3dc6f2f904d8e0c33850073b6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 14 May 2008 18:19:37 +0000 Subject: [PATCH 432/710] Fixed: private subprojects are listed on the issues view (#1217). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1432 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 17 +++++-------- test/functional/issues_controller_test.rb | 31 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index d9a720812..f25b5c401 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -265,7 +265,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 +277,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 = [] @@ -365,8 +364,6 @@ 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 end diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index a6d2ca6e3..c4389fedd 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -53,13 +53,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 From 439c697237224a5dde8fbe87a136239bfa0f99b6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 17 May 2008 11:03:43 +0000 Subject: [PATCH 433/710] Fixed: possible error when attachment's filename is non-ASCII (#747, #1243, #1089). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1433 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/attachment.rb | 15 ++++++++++++++- test/unit/attachment_test.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/unit/attachment_test.rb diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 08f440816..f44fe8b4d 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -40,7 +40,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 @@ -100,4 +100,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/test/unit/attachment_test.rb b/test/unit/attachment_test.rb new file mode 100644 index 000000000..99f7c29f9 --- /dev/null +++ b/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 From 1907c31138d065aea93f53b8e7565e06a44e49f8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 18 May 2008 08:24:31 +0000 Subject: [PATCH 434/710] Fixed: bold, italics, underline not working within parentheses (#1225). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1434 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 4 ++-- test/unit/helpers/application_helper_test.rb | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 7729ced46..f94c95738 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -376,13 +376,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}) diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index e8b5883ce..8bd745124 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -162,6 +162,13 @@ class ApplicationHelperTest < HelperTestCase to_test.each { |text, result| assert_equal "#{result}
    ", textilizable(text).gsub(/[\t\n]/, '') } end + def test_text_formatting + to_test = {'*_+bold, italic and underline+_*' => 'bold, italic and underline', + '(_text within parentheses_)' => '(text within parentheses)' + } + to_test.each { |text, result| assert_equal "

    #{result}

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

    Dashes: ---

    ', textilizable('Dashes: ---') From 073818f8bc46580e118a7550f1faaa55b3be13d9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 18 May 2008 16:15:22 +0000 Subject: [PATCH 435/710] Ability to search all projects or the projects the user belongs to (#791). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1435 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/search_controller.rb | 74 +++++++++---------- app/helpers/search_helper.rb | 7 ++ app/models/changeset.rb | 2 +- app/models/document.rb | 2 +- app/models/issue.rb | 2 +- app/models/journal.rb | 2 +- app/models/message.rb | 4 +- app/models/news.rb | 2 +- app/models/project.rb | 6 +- app/models/wiki_page.rb | 2 +- app/views/search/index.rhtml | 18 +++-- public/stylesheets/application.css | 2 +- test/functional/search_controller_test.rb | 14 +++- .../lib/acts_as_searchable.rb | 18 ++++- 14 files changed, 96 insertions(+), 59 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f15653b63..d93c63808 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -29,6 +29,16 @@ class SearchController < ApplicationController @all_words = params[:all_words] || (params[:submit] ? false : true) @titles_only = !params[:titles_only].nil? + projects_to_search = + case params[:projects] + when 'all' + nil + when 'my_projects' + User.current.memberships.collect(&:project) + 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"] @@ -62,37 +72,27 @@ class SearchController < ApplicationController like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} @results = [] limit = 10 - if @project - @scope.each do |s| - @results += s.singularize.camelcase.constantize.search(like_tokens, @project, - :all_words => @all_words, - :titles_only => @titles_only, - :limit => (limit+1), - :offset => offset, - :before => params[:previous].nil?) - end - @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} - if params[:previous].nil? - @pagination_previous_date = @results[0].event_datetime if offset && @results[0] - if @results.size > limit - @pagination_next_date = @results[limit-1].event_datetime - @results = @results[0, limit] - end - else - @pagination_next_date = @results[-1].event_datetime if offset && @results[-1] - if @results.size > limit - @pagination_previous_date = @results[-(limit)].event_datetime - @results = @results[-(limit), limit] - end + @scope.each do |s| + @results += 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?) + 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/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ed2f40b69..6b5a2cede 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -35,4 +35,11 @@ module SearchHelper end result end + + def project_select_tag + options = [[l(:label_project_all), 'all']] + options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? + options << [@project.name, ''] unless @project.nil? + select_tag('projects', options_for_select(options, params[:projects].to_s)) if options.size > 1 + end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 3e95ce111..9a05e6a68 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -27,7 +27,7 @@ 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' diff --git a/app/models/document.rb b/app/models/document.rb index 7a432b46b..9e2818fc7 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -20,7 +20,7 @@ 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}} diff --git a/app/models/issue.rb b/app/models/issue.rb index 8082e43b7..0618b0f0a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -36,7 +36,7 @@ class Issue < ActiveRecord::Base has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all acts_as_watchable - acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue} + acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue} acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} diff --git a/app/models/journal.rb b/app/models/journal.rb index 1376d349e..edf261e6d 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -26,7 +26,7 @@ class Journal < ActiveRecord::Base attr_accessor :indice acts_as_searchable :columns => 'notes', - :include => :issue, + :include => {:issue => :project}, :project_key => "#{Issue.table_name}.project_id", :date_column => "#{Issue.table_name}.created_on" diff --git a/app/models/message.rb b/app/models/message.rb index a18d126c9..f57b90985 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -23,9 +23,9 @@ 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'}, diff --git a/app/models/news.rb b/app/models/news.rb index 3d8c4d661..71e2a2d5e 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -24,7 +24,7 @@ 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}} # returns latest news for projects visible by user diff --git a/app/models/project.rb b/app/models/project.rb index 8c32c8562..2f2937fd9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -46,7 +46,7 @@ class Project < ActiveRecord::Base acts_as_tree :order => "name", :counter_cache => true - acts_as_searchable :columns => ['name', 'description'], :project_key => 'id' + 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}} @@ -202,6 +202,10 @@ class Project < ActiveRecord::Base @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq end + def project + self + end + def <=>(project) name.downcase <=> project.name.downcase end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 95750f37b..65fc1f68c 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -29,7 +29,7 @@ class WikiPage < ActiveRecord::Base :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 diff --git a/app/views/search/index.rhtml b/app/views/search/index.rhtml index 29c604a21..0be97a504 100644 --- a/app/views/search/index.rhtml +++ b/app/views/search/index.rhtml @@ -4,23 +4,25 @@ <% form_tag({}, :method => :get) do %>

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

    -<%= submit_tag l(:button_submit), :name => 'submit' %> +

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

    + +

    <%= submit_tag l(:button_submit), :name => 'submit' %>

    <% end %>
    <% if @results %>

    <%= l(:label_result_plural) %>

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

      • <%= 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 %>
        <%= highlight_tokens(e.event_description, @tokens) %>
        <%= format_time(e.event_datetime) %>

      • <% end %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 8e4bf995c..bdcf5615c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -177,7 +177,7 @@ div#activity dd { margin-bottom: 1em; padding-left: 18px; } div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description { font-style: italic; } -div#activity span.project:after { content: " -"; } +div#activity span.project:after, #search-results 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); } diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index 49004c7e6..b02f07793 100644 --- a/test/functional/search_controller_test.rb +++ b/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,15 @@ 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)) + end + def test_search_without_searchable_custom_fields CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}" diff --git a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb index dff76b913..1eb64c30f 100644 --- a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -49,6 +49,9 @@ module Redmine 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,8 +65,12 @@ module Redmine end module ClassMethods - def search(tokens, project, options={}) + # Search the model for the given tokens + # projects argument can be either nil (will search all projects), a project or an array of projects + def search(tokens, projects, 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') @@ -92,12 +99,17 @@ module Redmine end find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] - results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do + 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 = with_scope(:find => {:conditions => project_conditions.join(' AND ')}) 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} + results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc} end results.uniq! end From 82f204d8ac48744b7d4458fcf742882716bc0733 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 18 May 2008 16:33:50 +0000 Subject: [PATCH 436/710] Adds icons on search results. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1436 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/search/index.rhtml | 10 +++++----- public/stylesheets/application.css | 28 +++++++++++++++------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/views/search/index.rhtml b/app/views/search/index.rhtml index 0be97a504..44d024b60 100644 --- a/app/views/search/index.rhtml +++ b/app/views/search/index.rhtml @@ -20,13 +20,13 @@ <% if @results %>

        <%= l(:label_result_plural) %>

        -
          +
          <% @results.each do |e| %> -
        • <%= 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 %>
          - <%= highlight_tokens(e.event_description, @tokens) %>
          - <%= format_time(e.event_datetime) %>

        • +
          <%= 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 %>
          +
          <%= highlight_tokens(e.event_description, @tokens) %>
          + <%= format_time(e.event_datetime) %>
          <% end %> -
        + <% end %>

        diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index bdcf5615c..9a0201378 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -172,22 +172,24 @@ 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 dl, #search-results { margin-left: 2em; } div#activity dd { margin-bottom: 1em; padding-left: 18px; } -div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt, #search-results dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } div#activity dt .time { color: #777; font-size: 80%; } -div#activity dd .description { font-style: italic; } +div#activity dd .description, #search-results dd .description { font-style: italic; } div#activity span.project:after, #search-results 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); } +#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px;} + +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.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); } div#roadmap fieldset.related-issues { margin-bottom: 1em; } div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; } From 94dbf641ffd7f479208b135b310585898058e20b Mon Sep 17 00:00:00 2001 From: Liwiusz Ociepa Date: Mon, 19 May 2008 12:41:40 +0000 Subject: [PATCH 437/710] Memory leak (postgres -> zlib + ssl) has been fixed by apache developers. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1438 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- extra/svn/Redmine.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm index 8003cabc0..e046b949a 100644 --- a/extra/svn/Redmine.pm +++ b/extra/svn/Redmine.pm @@ -52,8 +52,8 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used): ## for mysql RedmineDSN "DBI:mysql:database=databasename;host=my.db.server" - ## for postgres (there is memory leak in libpq+ssl) - # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server;sslmode=disable" + ## for postgres + # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server" RedmineDbUser "redmine" RedmineDbPass "password" From b0be3b95aab8c01a7561431579c83cde07f3109f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 20 May 2008 20:31:04 +0000 Subject: [PATCH 438/710] Ability to search a project and its subprojects (#1264). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1439 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/search_controller.rb | 4 +++- app/helpers/search_helper.rb | 3 ++- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/da.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/no.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/th.yml | 1 + lang/uk.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/functional/search_controller_test.rb | 8 ++++++++ 29 files changed, 39 insertions(+), 2 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index d93c63808..d4ef01bf8 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -30,11 +30,13 @@ class SearchController < ApplicationController @titles_only = !params[:titles_only].nil? projects_to_search = - case params[:projects] + 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 diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 6b5a2cede..d6a2fb949 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -39,7 +39,8 @@ module SearchHelper 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('projects', options_for_select(options, params[:projects].to_s)) if options.size > 1 + select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 end end diff --git a/lang/bg.yml b/lang/bg.yml index b341d989f..224177d94 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/cs.yml b/lang/cs.yml index 250c602c2..2126553fc 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -623,3 +623,4 @@ 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 diff --git a/lang/da.yml b/lang/da.yml index ff2ed982d..3b5ca07d3 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -620,3 +620,4 @@ 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 diff --git a/lang/de.yml b/lang/de.yml index 77184cf88..273bad277 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -619,3 +619,4 @@ 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 diff --git a/lang/en.yml b/lang/en.yml index e39aec301..892a63a1b 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -291,6 +291,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 diff --git a/lang/es.yml b/lang/es.yml index c6eef021a..26009d716 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -621,3 +621,4 @@ 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 diff --git a/lang/fi.yml b/lang/fi.yml index 1a614a16c..908ef1283 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -618,3 +618,4 @@ label_overall_activity: Kokonaishistoria error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä." label_planning: Suunnittelu text_subprojects_destroy_warning: 'Tämän alaprojekti(t): %s tullaan myös poistamaan.' +label_and_its_subprojects: %s and its subprojects diff --git a/lang/fr.yml b/lang/fr.yml index cbdda4f3d..8827edd5e 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -291,6 +291,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 diff --git a/lang/he.yml b/lang/he.yml index a611c8c39..71bc5e5b3 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/it.yml b/lang/it.yml index 3d1dea09e..1771e1bda 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/ja.yml b/lang/ja.yml index 680d29836..84f27a2eb 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -619,3 +619,4 @@ 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 diff --git a/lang/ko.yml b/lang/ko.yml index 4281f3881..ee94c4025 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/lt.yml b/lang/lt.yml index df7cd960b..a58835186 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -619,3 +619,4 @@ 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 diff --git a/lang/nl.yml b/lang/nl.yml index e487a7a6d..01755137a 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -619,3 +619,4 @@ 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 diff --git a/lang/no.yml b/lang/no.yml index ccfa5c99f..388e51dd7 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -619,3 +619,4 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) +label_and_its_subprojects: %s and its subprojects diff --git a/lang/pl.yml b/lang/pl.yml index cf101811a..bc65b6253 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 9facd8d19..b1f8d2055 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/pt.yml b/lang/pt.yml index 6f51c8ed2..9d0586775 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/ro.yml b/lang/ro.yml index 59edfeb70..01127f5c5 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -618,3 +618,4 @@ 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 diff --git a/lang/ru.yml b/lang/ru.yml index f69009847..4f0a4bb7b 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/sr.yml b/lang/sr.yml index d9869c362..263bd1187 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -619,3 +619,4 @@ 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 diff --git a/lang/sv.yml b/lang/sv.yml index c0f691230..5b0c57acf 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -619,3 +619,4 @@ 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 diff --git a/lang/th.yml b/lang/th.yml index 2c506c664..643ee01c6 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -621,3 +621,4 @@ default_activity_development: พัฒนา enumeration_issue_priorities: ความสำคัญของปัญหา enumeration_doc_categories: ประเภทเอกสาร enumeration_activities: กิจกรรม (ใช้ในการติดตามเวลา) +label_and_its_subprojects: %s and its subprojects diff --git a/lang/uk.yml b/lang/uk.yml index a52a05603..e61fc5b38 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -620,3 +620,4 @@ 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index d91c9708d..b52446917 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -619,3 +619,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +label_and_its_subprojects: %s and its subprojects diff --git a/lang/zh.yml b/lang/zh.yml index c8b809764..808dd1b39 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -619,3 +619,4 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) +label_and_its_subprojects: %s and its subprojects diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index b02f07793..1c505620b 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -37,6 +37,14 @@ class SearchControllerTest < Test::Unit::TestCase assert assigns(:results).include?(Changeset.find(101)) 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}" From 03308028c9520fa6f4f6e153799031b153a11dfc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 20 May 2008 20:45:53 +0000 Subject: [PATCH 439/710] Slight changes to the search results. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1440 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/search/index.rhtml | 2 +- public/images/projects.png | Bin 690 -> 811 bytes public/stylesheets/application.css | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/search/index.rhtml b/app/views/search/index.rhtml index 44d024b60..b5cea4645 100644 --- a/app/views/search/index.rhtml +++ b/app/views/search/index.rhtml @@ -23,7 +23,7 @@
        <% @results.each do |e| %>
        <%= 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 %>
        -
        <%= highlight_tokens(e.event_description, @tokens) %>
        +
        <%= highlight_tokens(e.event_description, @tokens) %> <%= format_time(e.event_datetime) %>
        <% end %>
        diff --git a/public/images/projects.png b/public/images/projects.png index 244c896f0eddc1e19a2cdae29acda561a20be169..073c7219db8a53000c1d6c2af503499b43716334 100644 GIT binary patch delta 799 zcmV+)1K|9!1*-;-8Gi-<001BJ|6u?C0`f^jK~y-6g;GmMlwlM--}n9hKaS2LCv7xT za7HQ2YNjkqBR54vA-ISZJt&Bzmx~BmSPN;&U)wM9h@IYMwGCS&M|Ti)sg{P#(=9mEE09k;}%WLZ24h(llL!Je` zQnyXWh@_I}3BA7)8I(?BQgeu%wsG>( z^6MM`(tlsld(}uPNhSl43^EziNGjeoy!)Ob&(2oe7ASW}k^tU( z*678{;p0~}CvE`%YiMW?p-?Da)3iC3Wfc*T7k~#q4gdkbg3Irc%Gdgt;!>^g-Ul># zHzY|?)a7!;84;nat&O#}w@cx0SW#6~F$_Z{B0)rqF=jEwEY7(pgfJCF(cNyh9t;MJ d))>}S>pR*#O3<-TAv*v7002ovPDHLkV1fh=b~gY3 literal 690 zcmWkqT}TvB6uw()x|$>v1SYgwyXYeA&a9@41i9mGxm>r0NaIGt#Z7k_pYd#Vmj`2!Cf$&+e3CjwBme+RBl`5W7g z=)M$fj{=aHEZgoa0>BmNJd=!L09Y9@qDl)2#mgiolB^J9r631`yh`$r6b(u)@T$#g zHmz8sXwtGl$`+5XfH0-llw=}>Hx!1HvNXi3iW!Itki?n~BOfPel!6%*XCbXCjEFF; z*cM{2;RKC!vY=6{q_fawRcBPTpbI!FV3WrtS3o?kC=3*71=6x^Xc9JMoKtj-kxWXn zC}h!aixLo~eqggbsi854G74lG43#@MQy3I)j$3aEia%xGpL}Uq~ z!Lf=a>YQFs3`wCUtbMQlXl-@4`qlHA7ye^`fG6h<`Lf<%pxhmKlzQwb^-SC= xnRwug|#MCI+;{yL)R<%{$8QO5}0Eu`<>^0pp_8;0PE2#hg diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 9a0201378..2467fd3cf 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -179,6 +179,7 @@ div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description, #search-results dd .description { font-style: italic; } div#activity span.project:after, #search-results span.project:after { content: " -"; } #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px;} +#search-results dd span.description { display:block; } dt.issue { background-image: url(../images/ticket.png); } dt.issue-edit { background-image: url(../images/ticket_edit.png); } @@ -190,6 +191,7 @@ 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; } From e5bc1d370a12aba5273d32ee8bf45f39aaa916cc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 20 May 2008 20:49:06 +0000 Subject: [PATCH 440/710] Use display: block; for activity item descriptions. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1441 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/projects/activity.rhtml | 4 +--- public/stylesheets/application.css | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index c2f2f9ebd..d4b47f7be 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -8,9 +8,7 @@ <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
        <%= format_time(e.event_datetime, false) %> <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %>
        -
        <% unless e.event_description.blank? -%> - <%= format_activity_description(e.event_description) %>
        - <% end %> +
        <%= format_activity_description(e.event_description) %> <%= e.event_author if e.respond_to?(:event_author) %>
        <% end -%> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 2467fd3cf..e5bb4a7ab 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -179,7 +179,7 @@ div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description, #search-results dd .description { font-style: italic; } div#activity span.project:after, #search-results span.project:after { content: " -"; } #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px;} -#search-results dd span.description { display:block; } +div#activity dd span.description, #search-results dd span.description { display:block; } dt.issue { background-image: url(../images/ticket.png); } dt.issue-edit { background-image: url(../images/ticket_edit.png); } From 62125cad33f8ffb47e774698603ca1772cfdea43 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 22 May 2008 17:02:29 +0000 Subject: [PATCH 441/710] Fixed: encoding problem when adding non-ASCII issue category in the new issue page (#1286). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1442 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/javascripts/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/javascripts/application.js b/public/javascripts/application.js index f3d771a10..a8b6c0e46 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -56,7 +56,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; } } From 82c12d09e94c502358b38166c5b239eb4ffcfff9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 22 May 2008 18:08:54 +0000 Subject: [PATCH 442/710] Fixed: non member or anonymous permissions change is effective only after an application restart (#1280). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1443 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/user.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index a67a08567..573261b77 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -258,13 +258,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 From e02e047dd4a8f171225bcc4ca904474d9f89adcc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 22 May 2008 18:26:43 +0000 Subject: [PATCH 443/710] Fixed: TypeError in WikiController#destroy_attachment (#1281). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1444 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/attachment.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index f44fe8b4d..45bbd5428 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -68,9 +68,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 From 9d4e71adf35656c5a06d782b24502fa121ff10b2 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 23 May 2008 16:29:40 +0000 Subject: [PATCH 444/710] Fixed: error when previewing a new wiki page (#1292) introduced in r1415. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1445 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/wiki_controller.rb | 3 ++- test/functional/wiki_controller_test.rb | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 44113ebf3..8655cfed7 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -162,7 +162,8 @@ class WikiController < ApplicationController def preview page = @wiki.find_page(params[:page]) - return render_403 unless editable?(page) + # page is nil when previewing a new page + return render_403 unless page.nil? || editable?(page) @attachements = page.attachments if page @text = params[:content][:text] render :partial => 'common/preview' diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index 8688c2e03..f1ae7a9c2 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -86,6 +86,17 @@ 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 From 193b2450f47a8e6eb9f6ff6fc5a6895e469b78eb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 24 May 2008 17:58:34 +0000 Subject: [PATCH 445/710] Fixed: View differences for individual file of a changeset fails if the subversion repository URL doesn't point to the repository root (#1209, #1262, #1275). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1446 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/change.rb | 4 +++ app/models/repository.rb | 5 ++++ app/models/repository/subversion.rb | 15 +++++++++++ app/views/repositories/revision.rhtml | 2 +- test/fixtures/changes.yml | 7 +++++ ...repositories_subversion_controller_test.rb | 26 +++++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/app/models/change.rb b/app/models/change.rb index d14f435a4..385fe5acb 100644 --- a/app/models/change.rb +++ b/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/app/models/repository.rb b/app/models/repository.rb index 8b1f8d0af..1ea77f24f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -64,6 +64,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 diff --git a/app/models/repository/subversion.rb b/app/models/repository/subversion.rb index 0c2239c43..3981d6f4c 100644 --- a/app/models/repository/subversion.rb +++ b/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/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index f1e176669..2fdf58faf 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -49,7 +49,7 @@
        <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %> <% if change.action == "M" %> -<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.path), :rev => @changeset.revision %> +<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.relative_path), :rev => @changeset.revision %> <% end %> diff --git a/test/fixtures/changes.yml b/test/fixtures/changes.yml index 30acbd02d..56d936296 100644 --- a/test/fixtures/changes.yml +++ b/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/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index dd56947fc..bc3f261a0 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -97,6 +97,32 @@ 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', :content => %r{/test/some/path/in/the/repo} }, + :child => { :tag => 'td', + :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 From 0e5edccaf2ef94372f2fb56e57c363134b69bc04 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 24 May 2008 18:37:06 +0000 Subject: [PATCH 446/710] Fixed: Issue number display clipped on 'my issues' (#1291). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1447 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/stylesheets/application.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index e5bb4a7ab..4ae1c70f1 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -80,7 +80,7 @@ a.issue.closed, .issue.closed a { text-decoration: line-through; } /***** Tables *****/ table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } -table.list td { overflow: hidden; vertical-align: top;} +table.list td { vertical-align: top; } table.list td.id { width: 2%; text-align: center;} table.list td.checkbox { width: 15px; padding: 0px;} From 7f134a8c67955f30ef79c4f0c7f36f8049ea1145 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 24 May 2008 18:42:53 +0000 Subject: [PATCH 447/710] Prevent admin users from locking their own account (#1276). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1448 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/users_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 6976d021b..32b29526a 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -44,7 +44,7 @@ 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 From 39fc8f38b8695820101149228f03de9638e19228 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 11:22:53 +0000 Subject: [PATCH 448/710] Prevent admin users from making themselves non-administrator (#1276). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1449 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/users/_form.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/_form.rhtml b/app/views/users/_form.rhtml index 6ca167a59..09a798468 100644 --- a/app/views/users/_form.rhtml +++ b/app/views/users/_form.rhtml @@ -12,7 +12,7 @@

        <%= custom_field_tag_with_label @custom_value %>

        <% end if @custom_values%> -

        <%= f.check_box :admin %>

        +

        <%= f.check_box :admin, :disabled => (@user == User.current) %>

        From e8d092e46ad8c77419792cc5b418251a1c0a09c6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 11:43:20 +0000 Subject: [PATCH 449/710] Fixed: using '*' as keyword for repository referencing keywords doesn't work when issue id is at the beginning of a line (#1253). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1451 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/changeset.rb | 2 +- test/unit/changeset_test.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 9a05e6a68..41f5ed86a 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -75,7 +75,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 diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb index bbfe6952d..6cc53d852 100644 --- a/test/unit/changeset_test.rb +++ b/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') From fbafff83630b767f7da735d64ed0feb3be001fe0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 12:18:49 +0000 Subject: [PATCH 450/710] =?UTF-8?q?Hungarian=20translation=20added=20(#119?= =?UTF-8?q?8=20by=20G=C3=A1bor=20Tak=C3=A1cs).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1452 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/hu.yml | 622 ++++++++++++++++++ .../javascripts/calendar/lang/calendar-hu.js | 127 ++++ .../jstoolbar/lang/jstoolbar-hu.js | 14 + 3 files changed, 763 insertions(+) create mode 100644 lang/hu.yml create mode 100644 public/javascripts/calendar/lang/calendar-hu.js create mode 100644 public/javascripts/jstoolbar/lang/jstoolbar-hu.js diff --git a/lang/hu.yml b/lang/hu.yml new file mode 100644 index 000000000..428c7bf5c --- /dev/null +++ b/lang/hu.yml @@ -0,0 +1,622 @@ +_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: 'Hungarian (Magyar)' +general_csv_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 and its subprojects +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: Új státuszok engedélyezve +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ések +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: Esedékes +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 %s ezelőtt lérehozva +label_module_plural: Modulok +label_added_time_by: %s által %s ezelőtt hozzáadva +label_updated_time: %s ezelőtt módosítva +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.
        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) diff --git a/public/javascripts/calendar/lang/calendar-hu.js b/public/javascripts/calendar/lang/calendar-hu.js new file mode 100644 index 000000000..0e219c123 --- /dev/null +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-hu.js b/public/javascripts/jstoolbar/lang/jstoolbar-hu.js new file mode 100644 index 000000000..e586a123b --- /dev/null +++ b/public/javascripts/jstoolbar/lang/jstoolbar-hu.js @@ -0,0 +1,14 @@ +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['Preformatted text'] = 'Előreformázott szöveg'; +jsToolBar.strings['Wiki link'] = 'Link egy Wiki oldalra'; +jsToolBar.strings['Image'] = 'Kép'; From 02757f5892af308c0cdf905e3d504a5ce3ad84de Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 12:42:56 +0000 Subject: [PATCH 451/710] Translations updates. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1453 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/hu.yml | 14 +- lang/lt.yml | 83 +- lang/no.yml | 2 +- lang/pl.yml | 26 +- lang/pt-br.yml | 796 +++++++++--------- lang/zh-tw.yml | 2 +- lang/zh.yml | 14 +- .../calendar/lang/calendar-pt-br.js | 34 +- .../jstoolbar/lang/jstoolbar-pt-br.js | 28 +- 9 files changed, 502 insertions(+), 497 deletions(-) diff --git a/lang/hu.yml b/lang/hu.yml index 428c7bf5c..c3feaf16d 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -46,7 +46,7 @@ general_text_No: 'Nem' general_text_Yes: 'Igen' general_text_no: 'nem' general_text_yes: 'igen' -general_lang_name: 'Hungarian (Magyar)' +general_lang_name: 'Magyar' general_csv_separator: ',' general_csv_encoding: ISO-8859-2 general_pdf_encoding: ISO-8859-2 @@ -291,7 +291,7 @@ 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 and its subprojects +label_and_its_subprojects: %s és alprojektjei label_min_max_length: Min - Max hossz label_list: Lista label_date: Dátum @@ -337,7 +337,7 @@ label_closed_issues_plural: lezárt label_total: Összesen label_permissions: Jogosultságok label_current_status: Jelenlegi státusz -label_new_statuses_allowed: Új státuszok engedélyezve +label_new_statuses_allowed: Státusz változtatások engedélyei label_all: mind label_none: nincs label_nobody: senki @@ -406,7 +406,7 @@ label_sort_higher: Eggyel feljebb label_sort_lower: Eggyel lejjebb label_sort_lowest: Az aljára label_roadmap: Életút -label_roadmap_due_in: Esedékes +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 @@ -476,10 +476,10 @@ 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 %s ezelőtt lérehozva +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 által %s ezelőtt hozzáadva -label_updated_time: %s ezelőtt módosítva +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 diff --git a/lang/lt.yml b/lang/lt.yml index a58835186..bc2faaa00 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -554,7 +554,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,45 +578,46 @@ 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_and_its_subprojects: %s and its subprojects +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 + diff --git a/lang/no.yml b/lang/no.yml index 388e51dd7..13fcd24f5 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -291,6 +291,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 @@ -619,4 +620,3 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) -label_and_its_subprojects: %s and its subprojects diff --git a/lang/pl.yml b/lang/pl.yml index bc65b6253..c83173ca2 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -70,7 +70,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 @@ -119,7 +119,7 @@ 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 +172,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 +328,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: 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 +366,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,7 +463,7 @@ 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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index b1f8d2055..9714df2ca 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -1,140 +1,140 @@ _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_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 +142,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 +262,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 +436,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 +450,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.
        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.
        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,108 +514,108 @@ 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index b52446917..1bdc7d715 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -291,6 +291,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: 日期 @@ -619,4 +620,3 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) -label_and_its_subprojects: %s and its subprojects diff --git a/lang/zh.yml b/lang/zh.yml index 808dd1b39..ac5eb456d 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -198,7 +198,7 @@ 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: 时间格式 @@ -257,9 +257,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 +291,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: 日期 @@ -573,7 +574,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 及其所有内容吗? @@ -618,5 +619,4 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 -enumeration_activities: 活动(时间跟踪) -label_and_its_subprojects: %s and its subprojects +enumeration_activities: 活动(时间跟踪) \ No newline at end of file diff --git a/public/javascripts/calendar/lang/calendar-pt-br.js b/public/javascripts/calendar/lang/calendar-pt-br.js index 5d4d014ce..bf7734ab3 100644 --- a/public/javascripts/calendar/lang/calendar-pt-br.js +++ b/public/javascripts/calendar/lang/calendar-pt-br.js @@ -2,7 +2,8 @@ // Calendar pt_BR language // Author: Adalberto Machado, -// Encoding: any +// Review: Alexandre da Silva, +// 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/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js b/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js index cd36a4b55..8fc58ba67 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js @@ -1,14 +1,16 @@ +// Translated by: Alexandre da Silva + 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['Preformatted text'] = 'Texto pré-formatado'; +jsToolBar.strings['Wiki link'] = 'Link para uma página Wiki'; +jsToolBar.strings['Image'] = 'Imagem'; From 03f0236a6ee995363c54f958d66a0abbf0d3a827 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 12:50:33 +0000 Subject: [PATCH 452/710] Prevents NoMethodError on @available_filters.has_key? in query.rb (#1178). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1454 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 2 +- test/unit/query_test.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/query.rb b/app/models/query.rb index f25b5c401..c19bb8d7e 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -227,7 +227,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 diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index e143e6fc2..147bfbea3 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -116,6 +116,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) From a92749ef93bbcc4e9a1116bd58d750de3054f78a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 13:26:21 +0000 Subject: [PATCH 453/710] Gantt chart: display issues that don't have a due date if they are assigned to a version with a date (#184). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1455 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 7 +++++++ app/models/issue.rb | 6 ++++++ app/views/projects/gantt.rfpdf | 4 ++-- app/views/projects/gantt.rhtml | 4 ++-- test/fixtures/issues.yml | 6 +++++- test/fixtures/versions.yml | 2 +- test/functional/projects_controller_test.rb | 11 ++++++++++- test/functional/versions_controller_test.rb | 6 +++--- 8 files changed, 36 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 34ce734a5..07b29fa25 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -382,11 +382,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 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 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/app/models/issue.rb b/app/models/issue.rb index 0618b0f0a..633253db7 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -230,6 +230,12 @@ class Issue < ActiveRecord::Base relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)} 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 (start_date && due_date) ? due_date - start_date : 0 end diff --git a/app/views/projects/gantt.rfpdf b/app/views/projects/gantt.rfpdf index a293906ba..e94fc5814 100644 --- a/app/views/projects/gantt.rfpdf +++ b/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/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml index f398bace7..071e0f324 100644 --- a/app/views/projects/gantt.rhtml +++ b/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 ) diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml index 48195a7b7..c037624ae 100644 --- a/test/fixtures/issues.yml +++ b/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 diff --git a/test/fixtures/versions.yml b/test/fixtures/versions.yml index bf08660d5..62c5e6f99 100644 --- a/test/fixtures/versions.yml +++ b/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/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index bebe96f29..82b5bdeb6 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -259,7 +259,16 @@ class ProjectsControllerTest < Test::Unit::TestCase 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_should_not_show_private_subprojects diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb index 3477c5edd..3a118701a 100644 --- a/test/functional/versions_controller_test.rb +++ b/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 From c7d83be6131a386928c48f093a7426505c028dfc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 13:30:54 +0000 Subject: [PATCH 454/710] Test for striked through wiki link (#199). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1456 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/unit/helpers/application_helper_test.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 8bd745124..25559ebc1 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -125,6 +125,8 @@ class ApplicationHelperTest < HelperTestCase '[[onlinestore:Start page]]' => 'Start page', '[[onlinestore:Start page|Text]]' => 'Text', '[[onlinestore:Unknown page]]' => 'Unknown page', + # striked through link + '-[[Another page|Page]]-' => 'Page', # escaping '![[Another page|Page]]' => '[[Another page|Page]]', } From dfcc8e1492dc4a30b1cddf600aed105f3284528d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 13:37:29 +0000 Subject: [PATCH 455/710] Change projects homepage limit to 255 chars (#663, #1095). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1457 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 2 +- app/views/projects/_form.rhtml | 2 +- db/migrate/094_change_projects_homepage_limit.rb | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 db/migrate/094_change_projects_homepage_limit.rb diff --git a/app/models/project.rb b/app/models/project.rb index 2f2937fd9..e560f1dac 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -57,7 +57,7 @@ class Project < ActiveRecord::Base 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\-]*$/ diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index 32e4dcd44..774e73977 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -13,7 +13,7 @@ <% unless @project.identifier_frozen? %>
        <%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %> <% end %>

        -

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

        +

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

        <%= f.check_box :is_public %>

        <%= wikitoolbar_for 'project_description' %> diff --git a/db/migrate/094_change_projects_homepage_limit.rb b/db/migrate/094_change_projects_homepage_limit.rb new file mode 100644 index 000000000..98374aa4e --- /dev/null +++ b/db/migrate/094_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 From d99dc4070a925fabc050ae61c21c64548730a636 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 15:09:42 +0000 Subject: [PATCH 456/710] Remove edit step from Status context menu (#1197). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1458 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/context_menu.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index f42f254e8..04c21ceae 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -6,7 +6,7 @@ <%= l(:field_status) %>
          <% @statuses.each do |s| -%> -
        • <%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}}, +
        • <%= 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)) %>
        • <% end -%>
        From 0c8105277070c20590d22a0d4b8fc1f09ce981d9 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 17:31:50 +0000 Subject: [PATCH 457/710] Adds a rake task to send reminders. An email is sent to each user with a list of the issues due in the next days, if any. See @rake -D redmine:send_reminders@ for options. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1459 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/mailer.rb | 31 +++++++++++++++++ app/views/mailer/reminder.text.html.rhtml | 9 +++++ app/views/mailer/reminder.text.plain.rhtml | 7 ++++ lang/bg.yml | 2 ++ lang/cs.yml | 2 ++ lang/da.yml | 2 ++ lang/de.yml | 2 ++ lang/en.yml | 2 ++ lang/es.yml | 2 ++ lang/fi.yml | 2 ++ lang/fr.yml | 2 ++ lang/he.yml | 2 ++ lang/hu.yml | 2 ++ lang/it.yml | 2 ++ lang/ja.yml | 2 ++ lang/ko.yml | 2 ++ lang/lt.yml | 2 ++ lang/nl.yml | 2 ++ lang/no.yml | 2 ++ lang/pl.yml | 2 ++ lang/pt-br.yml | 2 ++ lang/pt.yml | 2 ++ lang/ro.yml | 2 ++ lang/ru.yml | 2 ++ lang/sr.yml | 2 ++ lang/sv.yml | 2 ++ lang/th.yml | 2 ++ lang/uk.yml | 2 ++ lang/zh-tw.yml | 2 ++ lang/zh.yml | 4 ++- lib/tasks/reminder.rake | 39 ++++++++++++++++++++++ test/fixtures/issues.yml | 2 +- test/unit/mailer_test.rb | 9 +++++ 33 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 app/views/mailer/reminder.text.html.rhtml create mode 100644 app/views/mailer/reminder.text.plain.rhtml create mode 100644 lib/tasks/reminder.rake diff --git a/app/models/mailer.rb b/app/models/mailer.rb index 6fc879a15..aae374268 100644 --- a/app/models/mailer.rb +++ b/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,28 @@ 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 <= ? AND #{Issue.table_name}.assigned_to_id IS NOT NULL", false, days.day.from_now.to_date] + 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/app/views/mailer/reminder.text.html.rhtml b/app/views/mailer/reminder.text.html.rhtml new file mode 100644 index 000000000..1e33fbe43 --- /dev/null +++ b/app/views/mailer/reminder.text.html.rhtml @@ -0,0 +1,9 @@ +

        <%= l(:mail_body_reminder, @issues.size, @days) %>

        + +
          +<% @issues.each do |issue| -%> +
        • <%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %>
        • +<% end -%> +
        + +

        <%= link_to l(:label_issue_view_all), @issues_url %>

        diff --git a/app/views/mailer/reminder.text.plain.rhtml b/app/views/mailer/reminder.text.plain.rhtml new file mode 100644 index 000000000..7e6a2e585 --- /dev/null +++ b/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/lang/bg.yml b/lang/bg.yml index 224177d94..10198bce9 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/cs.yml b/lang/cs.yml index 2126553fc..f90068b3b 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -624,3 +624,5 @@ 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" diff --git a/lang/da.yml b/lang/da.yml index 3b5ca07d3..ca4ddeb46 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -621,3 +621,5 @@ 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" diff --git a/lang/de.yml b/lang/de.yml index 273bad277..fa33e1c74 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -620,3 +620,5 @@ 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" diff --git a/lang/en.yml b/lang/en.yml index 892a63a1b..f9e07d211 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -91,6 +91,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 diff --git a/lang/es.yml b/lang/es.yml index 26009d716..3d8c0a931 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -622,3 +622,5 @@ 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" diff --git a/lang/fi.yml b/lang/fi.yml index 908ef1283..60428bb49 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -619,3 +619,5 @@ error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä." label_planning: Suunnittelu text_subprojects_destroy_warning: 'Tämän alaprojekti(t): %s tullaan myös poistamaan.' 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" diff --git a/lang/fr.yml b/lang/fr.yml index 8827edd5e..8b002bbf6 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -91,6 +91,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 diff --git a/lang/he.yml b/lang/he.yml index 71bc5e5b3..f972ef62a 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/hu.yml b/lang/hu.yml index c3feaf16d..f6d312ce0 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -620,3 +620,5 @@ 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 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" diff --git a/lang/it.yml b/lang/it.yml index 1771e1bda..a8646644a 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/ja.yml b/lang/ja.yml index 84f27a2eb..d119cf55e 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -620,3 +620,5 @@ 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" diff --git a/lang/ko.yml b/lang/ko.yml index ee94c4025..98dde9e06 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/lt.yml b/lang/lt.yml index bc2faaa00..6b791d87b 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -621,3 +621,5 @@ 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 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" diff --git a/lang/nl.yml b/lang/nl.yml index 01755137a..c36eaecd5 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -620,3 +620,5 @@ 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" diff --git a/lang/no.yml b/lang/no.yml index 13fcd24f5..5e788dc50 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -620,3 +620,5 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) +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" diff --git a/lang/pl.yml b/lang/pl.yml index c83173ca2..4476c2160 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -619,3 +619,5 @@ error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacj 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" diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 9714df2ca..9b3d23151 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/pt.yml b/lang/pt.yml index 9d0586775..c56b76a0b 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/ro.yml b/lang/ro.yml index 01127f5c5..30464569b 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -619,3 +619,5 @@ 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" diff --git a/lang/ru.yml b/lang/ru.yml index 4f0a4bb7b..f4fd92037 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -623,3 +623,5 @@ 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" diff --git a/lang/sr.yml b/lang/sr.yml index 263bd1187..53e4f9f2c 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -620,3 +620,5 @@ 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" diff --git a/lang/sv.yml b/lang/sv.yml index 5b0c57acf..248f3dcba 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -620,3 +620,5 @@ 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" diff --git a/lang/th.yml b/lang/th.yml index 643ee01c6..444316aa1 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -622,3 +622,5 @@ 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" diff --git a/lang/uk.yml b/lang/uk.yml index e61fc5b38..3cc60ef48 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -621,3 +621,5 @@ 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" diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 1bdc7d715..3926113b2 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -620,3 +620,5 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +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" diff --git a/lang/zh.yml b/lang/zh.yml index ac5eb456d..ffd7cd811 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -619,4 +619,6 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 -enumeration_activities: 活动(时间跟踪) \ No newline at end of file +enumeration_activities: 活动(时间跟踪)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" +mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" diff --git a/lib/tasks/reminder.rake b/lib/tasks/reminder.rake new file mode 100644 index 000000000..73844fb79 --- /dev/null +++ b/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/test/fixtures/issues.yml b/test/fixtures/issues.yml index c037624ae..9d3287c6f 100644 --- a/test/fixtures/issues.yml +++ b/test/fixtures/issues.yml @@ -42,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) %> diff --git a/test/unit/mailer_test.rb b/test/unit/mailer_test.rb index 64648b94c..8cf6c1423 100644 --- a/test/unit/mailer_test.rb +++ b/test/unit/mailer_test.rb @@ -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 From b3e5f1a1c39822923160e376e99c05d370429cfd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 25 May 2008 20:49:07 +0000 Subject: [PATCH 458/710] Fixed zh lang file. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1460 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/zh.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/zh.yml b/lang/zh.yml index ffd7cd811..55516e033 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -619,6 +619,6 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 -enumeration_activities: 活动(时间跟踪)mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:" +enumeration_activities: 活动(时间跟踪) 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:" From d6daeca40ad2aa180ac807875861b987b11ba3eb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 26 May 2008 17:12:11 +0000 Subject: [PATCH 459/710] Fixed: IssueController#edit doesn't set default Activity as default (#1302). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1461 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 1 + app/models/time_entry.rb | 6 ++++++ test/fixtures/enumerations.yml | 5 +++++ test/functional/timelog_controller_test.rb | 12 +++++++++++- 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 84b95741e..ca3309c46 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -102,6 +102,7 @@ class IssuesController < ApplicationController @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' } diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index ddaff2b60..4bd2d33b8 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -29,6 +29,12 @@ class TimeEntry < ActiveRecord::Base validates_numericality_of :hours, :allow_nil => true validates_length_of :comments, :maximum => 255 + def after_initialize + if new_record? + self.activity ||= Enumeration.default('ACTI') + end + end + def before_validation self.project = issue.project if issue && project.nil? end diff --git a/test/fixtures/enumerations.yml b/test/fixtures/enumerations.yml index c90a997ee..5e2615476 100644 --- a/test/fixtures/enumerations.yml +++ b/test/fixtures/enumerations.yml @@ -39,4 +39,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/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index e80a67728..479f4f44f 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -30,7 +30,17 @@ 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', From 54138d2b1771d31b14aa3007974451888fd3a8c4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 26 May 2008 19:12:29 +0000 Subject: [PATCH 460/710] Fixed roadmap links to versions (#1297). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1462 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/projects/roadmap.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml index ba0bb3ebd..a51b22fa9 100644 --- a/app/views/projects/roadmap.rhtml +++ b/app/views/projects/roadmap.rhtml @@ -43,7 +43,7 @@

        <%= l(:label_version_plural) %>

        <% @versions.each do |version| %> -<%= link_to version.name, :anchor => version.name %>
        +<%= link_to version.name, "##{version.name}" %>
        <% end %> <% end %> From aa87a73e413070799f450f496ad7a9d57a84a5a0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 26 May 2008 19:39:51 +0000 Subject: [PATCH 461/710] No multiline text for textile links. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1463 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 2 +- lib/redmine/wiki_formatting.rb | 2 +- test/unit/helpers/application_helper_test.rb | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index f94c95738..fb6a053c6 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -764,7 +764,7 @@ class RedCloth < String ([\s\[{(]|[#{PUNCT}])? # $pre " # start (#{C}) # $atts - ([^"]+?) # $text + ([^"\n]+?) # $text \s? (?:\(([^)]+?)\)(?="))? # $title ": diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 8866e8cde..3c1eac020 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -45,7 +45,7 @@ module Redmine # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. # http://code.whytheluckystiff.net/redcloth/changeset/128 def hard_break( text ) - text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
        " ) if hard_breaks + text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
        \n" ) if hard_breaks end # Patch to add code highlighting support to RedCloth diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 25559ebc1..ffee28b00 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -58,7 +58,9 @@ class ApplicationHelperTest < HelperTestCase to_test = { 'This is a "link":http://foo.bar' => 'This is a link', 'This is an intern "link":/foo/bar' => 'This is an intern link', - '"link (Link title)":http://foo.bar' => 'link' + '"link (Link title)":http://foo.bar' => 'link', + # 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
        \nand another on a second line\":test" } to_test.each { |text, result| assert_equal "

        #{result}

        ", textilizable(text) } end From 744d8669260b63a94f5cd41c6b79b8396dd47df3 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 26 May 2008 20:10:32 +0000 Subject: [PATCH 462/710] Moved ProjectsController#list to ProjectsController#index. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1464 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 7 +------ app/views/projects/{list.rhtml => index.rhtml} | 0 test/functional/projects_controller_test.rb | 8 +------- 3 files changed, 2 insertions(+), 13 deletions(-) rename app/views/projects/{list.rhtml => index.rhtml} (100%) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 07b29fa25..320def41b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -44,13 +44,8 @@ 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 diff --git a/app/views/projects/list.rhtml b/app/views/projects/index.rhtml similarity index 100% rename from app/views/projects/list.rhtml rename to app/views/projects/index.rhtml diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 82b5bdeb6..d4ce97dc2 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -35,13 +35,7 @@ class ProjectsControllerTest < Test::Unit::TestCase 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)) From eb0e218603fb334f3281515f15b23b246c6b351d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 27 May 2008 16:49:18 +0000 Subject: [PATCH 463/710] Adds new projects atom feed (#1290). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1465 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 12 ++++++++++-- app/views/projects/index.rhtml | 9 +++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 320def41b..d15c6bc2a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -49,8 +49,16 @@ class ProjectsController < ApplicationController 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.each_key {|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 diff --git a/app/views/projects/index.rhtml b/app/views/projects/index.rhtml index b8bb62ebb..e12b05979 100644 --- a/app/views/projects/index.rhtml +++ b/app/views/projects/index.rhtml @@ -17,9 +17,14 @@ <% end %> <% if User.current.logged? %> -
        +

        <%= l(:label_my_projects) %> -

        +

        <% end %> +

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

        + <% html_title(l(:label_project_plural)) -%> From 39216f327c632fd9c2ed74294b0753e610235d68 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 May 2008 17:42:10 +0000 Subject: [PATCH 464/710] Fixed: can not access old projects created with a numeric identifier (#1322). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1473 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/project.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index e560f1dac..f05ccb2af 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -144,7 +144,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? From e1a86106d0c5317d75eef170c1d676f9c3d464f4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 May 2008 18:48:00 +0000 Subject: [PATCH 465/710] Fixed: GitAdapter#get_rev should use current branch instead of hardwiring @master@ (#1319). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1475 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/git_adapter.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index a9ab505a8..9dfbd17a8 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/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_BIN} --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?) - cmd="#{GIT_BIN} --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| From 6ebdf848a583add999e9f27a5d1a0bf4009d478d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 May 2008 19:12:17 +0000 Subject: [PATCH 466/710] Fixed: Unable to use angular braces after include word in c code highlighting (#1230). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1476 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb b/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb index f6d71ade2..e5d87b233 100644 --- a/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb +++ b/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 From ffea0446992f91ae45ff3c9d47a89d24d23298a1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 29 May 2008 19:31:21 +0000 Subject: [PATCH 467/710] Trac importer: migrate attachments descriptions (#1326). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1477 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 48a774eef..5341fa14a 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -126,6 +126,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) @@ -408,6 +412,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 +461,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 From 4311ecbc04e3256bd8d7c9a8668dff32349b0159 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 30 May 2008 15:34:53 +0000 Subject: [PATCH 468/710] Fixed: Incorrect weekend definition in Hebrew calendar locale (#1328). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1478 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/javascripts/calendar/lang/calendar-he.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/javascripts/calendar/lang/calendar-he.js b/public/javascripts/calendar/lang/calendar-he.js index bd92e0073..9d4c87db0 100644 --- a/public/javascripts/calendar/lang/calendar-he.js +++ b/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"] = "היום"; From 88dea1a06d833067e5a6d9668c4b6829e027a9f5 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 30 May 2008 16:35:36 +0000 Subject: [PATCH 469/710] Adds multi-levels blockquotes support by using > at the beginning of lines. Textile is preserved inside quoted text. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1479 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 25 +++++++++++ lib/redmine/wiki_formatting.rb | 2 +- public/images/jstoolbar/bt_bq.png | Bin 0 -> 503 bytes public/images/jstoolbar/bt_bq_remove.png | Bin 0 -> 501 bytes public/javascripts/jstoolbar/jstoolbar.js | 33 ++++++++++++++- .../jstoolbar/lang/jstoolbar-bg.js | 2 + .../jstoolbar/lang/jstoolbar-cs.js | 2 + .../jstoolbar/lang/jstoolbar-da.js | 2 + .../jstoolbar/lang/jstoolbar-de.js | 2 + .../jstoolbar/lang/jstoolbar-en.js | 2 + .../jstoolbar/lang/jstoolbar-es.js | 2 + .../jstoolbar/lang/jstoolbar-fi.js | 2 + .../jstoolbar/lang/jstoolbar-fr.js | 2 + .../jstoolbar/lang/jstoolbar-he.js | 2 + .../jstoolbar/lang/jstoolbar-hu.js | 2 + .../jstoolbar/lang/jstoolbar-it.js | 2 + .../jstoolbar/lang/jstoolbar-ja.js | 2 + .../jstoolbar/lang/jstoolbar-ko.js | 2 + .../jstoolbar/lang/jstoolbar-lt.js | 2 + .../jstoolbar/lang/jstoolbar-nl.js | 2 + .../jstoolbar/lang/jstoolbar-no.js | 2 + .../jstoolbar/lang/jstoolbar-pl.js | 2 + .../jstoolbar/lang/jstoolbar-pt-br.js | 2 + .../jstoolbar/lang/jstoolbar-pt.js | 2 + .../jstoolbar/lang/jstoolbar-ro.js | 2 + .../jstoolbar/lang/jstoolbar-ru.js | 2 + .../jstoolbar/lang/jstoolbar-sr.js | 2 + .../jstoolbar/lang/jstoolbar-sv.js | 2 + .../jstoolbar/lang/jstoolbar-th.js | 2 + .../jstoolbar/lang/jstoolbar-uk.js | 2 + .../jstoolbar/lang/jstoolbar-zh-tw.js | 2 + .../jstoolbar/lang/jstoolbar-zh.js | 2 + public/stylesheets/application.css | 2 + public/stylesheets/jstoolbar.css | 6 +++ test/unit/helpers/application_helper_test.rb | 40 ++++++++++++++++++ 35 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 public/images/jstoolbar/bt_bq.png create mode 100644 public/images/jstoolbar/bt_bq_remove.png diff --git a/lib/redcloth.rb b/lib/redcloth.rb index fb6a053c6..344fd6c78 100644 --- a/lib/redcloth.rb +++ b/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 @@ -576,6 +578,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 ? '
        ' * (l-indent) : '
        ' * (indent-l)) + "\n\n") + indent = l + end + quotes << (content + "\n") + end + quotes << ("\n" + '' * indent + "\n\n") + quotes + end + end CODE_RE = /(\W) @ diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 3c1eac020..7197af2c3 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -45,7 +45,7 @@ module Redmine # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. # http://code.whytheluckystiff.net/redcloth/changeset/128 def hard_break( text ) - text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1
        \n" ) if hard_breaks + text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1
        \n" ) if hard_breaks end # Patch to add code highlighting support to RedCloth diff --git a/public/images/jstoolbar/bt_bq.png b/public/images/jstoolbar/bt_bq.png new file mode 100644 index 0000000000000000000000000000000000000000..c3af4e07f65847c109006a911359565dd88f7a12 GIT binary patch literal 503 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!Zi#C|iE~kEVo7FxoWhEpegoTCq`T0FPJ)NDMZEbDM&CLxB4YjqkRaI5x<>jTMq(ntU1qB6p zczCK-zqGTnv#_u*GBVQ9(NR)TYTocVf8pc1&%P%nCdS0XgoT9#1_t{0_;`4DG;Mr7 zZO_||%`eK9KEC(t`;=X88`eL6@bX7+aImkhualFLjg5_&nVErs!R&n>3JMCcv$NCE z(!#^TgMxxeOG}H3i}UmI)6>(Fl9FO$V_!@p zljrH;7*cU7IYEIbA}VtBtsA#)M!&GMcodMtb3|=f+9$4OVgii1xw34rwxYGn&dLJe zp`pI9&cd$UJ!@o|_cSe%y{W;-5;PJOS+@4BLl<6e(pbstU&(Z0G|-o|NsA6TU(o$nCR*0 zX=rFDD=W*&%1TH`2n!4I^YeRpdOABh+uGWio0}UN8ft56tE#HX%gakiNr{Sz3JMDH z@bHwZcxh*6XJKJsWMrhHqobsx^x)->qGgY3*S<+9s?WMgAvW@ct!U@(2phk}BF?Ck8cw6yT> z@Svce($do6;^O@L{Pgtnq@<+S*w~1O2zPgPM@Pp~?MEws-eD{W@(X5gcy=QV$jSF~ zaSW-rm7Ji!77-COGvfA*TQgtZvVIVd#B$`A+OkhvY0nhaP20vc;mXC(iy*<1(dTdVI-qJn0eR_YqLx4wwi^y&+H?=ge iFtai~zeRsqIT $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/public/javascripts/jstoolbar/lang/jstoolbar-bg.js b/public/javascripts/jstoolbar/lang/jstoolbar-bg.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-bg.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-cs.js b/public/javascripts/jstoolbar/lang/jstoolbar-cs.js index 8a59a8162..f2c0dbff5 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-cs.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-da.js b/public/javascripts/jstoolbar/lang/jstoolbar-da.js index 9996acaf3..6ccc8ead2 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-da.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-de.js b/public/javascripts/jstoolbar/lang/jstoolbar-de.js index e2ba3fc1c..ce686860f 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-de.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-en.js b/public/javascripts/jstoolbar/lang/jstoolbar-en.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-en.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-es.js b/public/javascripts/jstoolbar/lang/jstoolbar-es.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-es.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-fi.js b/public/javascripts/jstoolbar/lang/jstoolbar-fi.js index 357d25951..c2229b281 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-fi.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-fr.js b/public/javascripts/jstoolbar/lang/jstoolbar-fr.js index 3cbc67863..c52a783bc 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-fr.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-he.js b/public/javascripts/jstoolbar/lang/jstoolbar-he.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-he.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-hu.js b/public/javascripts/jstoolbar/lang/jstoolbar-hu.js index e586a123b..c31ba00c0 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-hu.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-hu.js @@ -9,6 +9,8 @@ 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/public/javascripts/jstoolbar/lang/jstoolbar-it.js b/public/javascripts/jstoolbar/lang/jstoolbar-it.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-it.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-ja.js b/public/javascripts/jstoolbar/lang/jstoolbar-ja.js index fc4d987de..c9413dac2 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ja.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-ko.js b/public/javascripts/jstoolbar/lang/jstoolbar-ko.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ko.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-lt.js b/public/javascripts/jstoolbar/lang/jstoolbar-lt.js index f0a7c5d90..8af364c8d 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-lt.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-nl.js b/public/javascripts/jstoolbar/lang/jstoolbar-nl.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-nl.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-no.js b/public/javascripts/jstoolbar/lang/jstoolbar-no.js index cf6e19ff9..2fb098a9d 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-no.js +++ b/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'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = 'Preformatert tekst'; jsToolBar.strings['Wiki link'] = 'Lenke til Wiki-side'; jsToolBar.strings['Image'] = 'Bilde'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-pl.js b/public/javascripts/jstoolbar/lang/jstoolbar-pl.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pl.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js b/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js index 8fc58ba67..5035524ab 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js @@ -11,6 +11,8 @@ 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/public/javascripts/jstoolbar/lang/jstoolbar-pt.js b/public/javascripts/jstoolbar/lang/jstoolbar-pt.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-pt.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-ro.js b/public/javascripts/jstoolbar/lang/jstoolbar-ro.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ro.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-ru.js b/public/javascripts/jstoolbar/lang/jstoolbar-ru.js index 6370a3e2d..a6d8c4fad 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-ru.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-sr.js b/public/javascripts/jstoolbar/lang/jstoolbar-sr.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-sr.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-sv.js b/public/javascripts/jstoolbar/lang/jstoolbar-sv.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-sv.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-th.js b/public/javascripts/jstoolbar/lang/jstoolbar-th.js index 2e2f2b88e..d87164226 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-th.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-th.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/public/javascripts/jstoolbar/lang/jstoolbar-uk.js b/public/javascripts/jstoolbar/lang/jstoolbar-uk.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-uk.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js index 1e46e2470..a87ad3442 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js +++ b/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['Quote'] = 'Quote'; +jsToolBar.strings['Unquote'] = 'Remove Quote'; jsToolBar.strings['Preformatted text'] = '格式化文字'; jsToolBar.strings['Wiki link'] = '連結至 Wiki 頁面'; jsToolBar.strings['Image'] = '圖片'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-zh.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh.js index cd36a4b55..2d68498f9 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-zh.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/public/stylesheets/application.css b/public/stylesheets/application.css index 4ae1c70f1..f51d1841c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -153,6 +153,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;} diff --git a/public/stylesheets/jstoolbar.css b/public/stylesheets/jstoolbar.css index c4ab55711..4e9d44b6c 100644 --- a/public/stylesheets/jstoolbar.css +++ b/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/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index ffee28b00..f8bf4a602 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -178,6 +178,46 @@ class ApplicationHelperTest < HelperTestCase assert_equal '

        Dashes: ---

        ', textilizable('Dashes: ---') 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 +

        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.

        +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!/) From 5d2abb84bdcb8724e5d24a5299bb90f0770f6c23 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 30 May 2008 17:42:25 +0000 Subject: [PATCH 470/710] Adds a Reply link to each issue note (#739). Reply is pre-filled with the quoted note. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1480 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 22 +++++++++++++++++++++- app/helpers/journals_helper.rb | 11 +++++++---- app/views/issues/_history.rhtml | 3 ++- app/views/issues/show.rhtml | 6 ++++++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/da.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/hu.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/no.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/th.yml | 1 + lang/uk.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + lib/redmine.rb | 4 ++-- public/images/comment.png | Bin 0 -> 413 bytes test/functional/issues_controller_test.rb | 16 ++++++++++++++++ 34 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 public/images/comment.png diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index ca3309c46..defc0a11e 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -19,7 +19,7 @@ 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] @@ -208,6 +208,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{
        ((.|\s)*?)
        }m, '[...]').gsub("\n", "\n> ") + "\n\n" + render(:update) { |page| + page.replace_html "notes", 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? diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index 234bfabc0..92d6e5593 100644 --- a/app/helpers/journals_helper.rb +++ b/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_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 + links << link_to_remote(image_tag('comment.png'), + { :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} }, + :title => l(:button_reply)) if options[:reply_links] 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/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml index f29a44daf..b8efdb400 100644 --- a/app/views/issues/_history.rhtml +++ b/app/views/issues/_history.rhtml @@ -1,3 +1,4 @@ +<% reply_links = authorize_for('issues', 'edit') -%> <% for journal in journals %>

        <%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %>
        @@ -8,6 +9,6 @@
      • <%= show_detail(detail) %>
      • <% end %>

      - <%= render_notes(journal) unless journal.notes.blank? %> + <%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %> <% end %> diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index f788d0ec8..f1c8a82cd 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -56,6 +56,12 @@ end %>
      +
      +<%= link_to_remote(image_tag('comment.png'), + { :url => {:controller => 'issues', :action => 'reply', :id => @issue} }, + :title => l(:button_reply)) if authorize_for('issues', 'edit') %> +
      +

      <%=l(:field_description)%>

      <%= textilizable @issue, :description, :attachments => @issue.attachments %> diff --git a/lang/bg.yml b/lang/bg.yml index 10198bce9..05a0b0d60 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/cs.yml b/lang/cs.yml index f90068b3b..829f4449e 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -626,3 +626,4 @@ 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:' diff --git a/lang/da.yml b/lang/da.yml index ca4ddeb46..92df87e3f 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -623,3 +623,4 @@ 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:' diff --git a/lang/de.yml b/lang/de.yml index fa33e1c74..1351d1944 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -622,3 +622,4 @@ 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:' diff --git a/lang/en.yml b/lang/en.yml index f9e07d211..4184a9175 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -596,6 +596,7 @@ 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:' default_role_manager: Manager default_role_developper: Developer diff --git a/lang/es.yml b/lang/es.yml index 3d8c0a931..ea479630d 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -624,3 +624,4 @@ 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:' diff --git a/lang/fi.yml b/lang/fi.yml index 60428bb49..b3b888622 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -621,3 +621,4 @@ text_subprojects_destroy_warning: 'Tämän alaprojekti(t): %s tullaan myös pois 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:' diff --git a/lang/fr.yml b/lang/fr.yml index 8b002bbf6..a802a44cc 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -596,6 +596,7 @@ 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:' default_role_manager: Manager default_role_developper: Développeur diff --git a/lang/he.yml b/lang/he.yml index f972ef62a..c4d69ebb9 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/hu.yml b/lang/hu.yml index f6d312ce0..fd7bf9f52 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -622,3 +622,4 @@ enumeration_doc_categories: Dokumentum kategóriák enumeration_activities: Tevékenységek (idő rögzítés) 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:' diff --git a/lang/it.yml b/lang/it.yml index a8646644a..519dcd01f 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/ja.yml b/lang/ja.yml index d119cf55e..714539c7d 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -622,3 +622,4 @@ 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:' diff --git a/lang/ko.yml b/lang/ko.yml index 98dde9e06..81656040c 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/lt.yml b/lang/lt.yml index 6b791d87b..89e607a83 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -623,3 +623,4 @@ label_and_its_subprojects: %s projektas ir jo subprojektai 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:' diff --git a/lang/nl.yml b/lang/nl.yml index c36eaecd5..eebc7d993 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -622,3 +622,4 @@ 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:' diff --git a/lang/no.yml b/lang/no.yml index 5e788dc50..3f6ef6ded 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -622,3 +622,4 @@ enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) 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:' diff --git a/lang/pl.yml b/lang/pl.yml index 4476c2160..81a7a0dba 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 9b3d23151..47e80a144 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/pt.yml b/lang/pt.yml index c56b76a0b..002a1b7f8 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/ro.yml b/lang/ro.yml index 30464569b..6bcd55a50 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -621,3 +621,4 @@ 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:' diff --git a/lang/ru.yml b/lang/ru.yml index f4fd92037..fa8fd69a9 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -625,3 +625,4 @@ 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:' diff --git a/lang/sr.yml b/lang/sr.yml index 53e4f9f2c..9a4adf214 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -622,3 +622,4 @@ 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:' diff --git a/lang/sv.yml b/lang/sv.yml index 248f3dcba..1ab11e98b 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -622,3 +622,4 @@ 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:' diff --git a/lang/th.yml b/lang/th.yml index 444316aa1..751aac749 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -624,3 +624,4 @@ 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:' diff --git a/lang/uk.yml b/lang/uk.yml index 3cc60ef48..3ab4462ed 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -623,3 +623,4 @@ 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:' diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 3926113b2..3b7808aae 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -622,3 +622,4 @@ enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) 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:' diff --git a/lang/zh.yml b/lang/zh.yml index 55516e033..b5ecace82 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -622,3 +622,4 @@ enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) 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:" +text_user_wrote: '%s wrote:' diff --git a/lib/redmine.rb b/lib/redmine.rb index b21f5716d..7b9832d62 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -32,9 +32,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 diff --git a/public/images/comment.png b/public/images/comment.png new file mode 100644 index 0000000000000000000000000000000000000000..7bc9233ea63c89d52a99494dd0f0735a29a3ec3b GIT binary patch literal 413 zcmV;O0b>4%P)i2vikyMR~)n*keF9=!Gc_n*K2@qsNT?}H4v4a974 z1ArVJApZ0B-@pGKzWw|E^3%Wn&p!V9|K$C@{}12&`+x7vzyG&i{r!LE6~yrB1;;^# zm?0Y=moxPMSn>r>N00000NkvXX Hu0mjf$^yWL literal 0 HcmV?d00001 diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index c4389fedd..7c7d44e5f 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -263,6 +263,22 @@ 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" + assert_select_rjs :replace_html, "notes" + 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" + assert_select_rjs :replace_html, "notes" + end def test_post_edit @request.session[:user_id] = 2 From 9894a3781e9cc7fc295f7fa7133fe951ca843e51 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 30 May 2008 18:40:02 +0000 Subject: [PATCH 471/710] Fixed: browser's accept-language subcodes ignored (#1320). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1481 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/application.rb | 6 +++--- test/functional/welcome_controller_test.rb | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index abf621641..2daee50de 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -61,11 +61,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 diff --git a/test/functional/welcome_controller_test.rb b/test/functional/welcome_controller_test.rb index 18146c6aa..3e1308ef1 100644 --- a/test/functional/welcome_controller_test.rb +++ b/test/functional/welcome_controller_test.rb @@ -46,4 +46,11 @@ 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 end From 50c338b229eddd9772974bf64071c0b330ffb2c6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 30 May 2008 18:42:56 +0000 Subject: [PATCH 472/710] Additional test for r1481. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1482 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/functional/welcome_controller_test.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/functional/welcome_controller_test.rb b/test/functional/welcome_controller_test.rb index 3e1308ef1..df565a751 100644 --- a/test/functional/welcome_controller_test.rb +++ b/test/functional/welcome_controller_test.rb @@ -53,4 +53,11 @@ class WelcomeControllerTest < Test::Unit::TestCase 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 From a06d2c41d5e54db8e434bf80aa1fe11d82ffb870 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 2 Jun 2008 16:59:15 +0000 Subject: [PATCH 473/710] Fixed: Reminder emails shouldn't include archived projects (#1351). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1483 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/mailer.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/mailer.rb b/app/models/mailer.rb index aae374268..61e5d596c 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -164,7 +164,9 @@ class Mailer < ActionMailer::Base 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 <= ? AND #{Issue.table_name}.assigned_to_id IS NOT NULL", false, days.day.from_now.to_date] + 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 From 4cc3ba88a8740fdbbaa4d8e6369464127c88c300 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 2 Jun 2008 17:01:02 +0000 Subject: [PATCH 474/710] Fixed: Link to PDF doesn't work after creating new issue (#1346). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1484 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index defc0a11e..682a0cc6b 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -149,7 +149,7 @@ class IssuesController < ApplicationController 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 From 870e8654b4dd6d36f816343f20d2fa603331efb6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 2 Jun 2008 18:16:56 +0000 Subject: [PATCH 475/710] Fixed: Atom feeds don't provide author section for repository revisions (#1348). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1485 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/feed.atom.rxml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/common/feed.atom.rxml b/app/views/common/feed.atom.rxml index b5cbeeed9..ac087d36b 100644 --- a/app/views/common/feed.atom.rxml +++ b/app/views/common/feed.atom.rxml @@ -14,7 +14,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do 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? From 36162c6cf2615485cd5e9bf314cba2920943ce75 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 2 Jun 2008 19:18:17 +0000 Subject: [PATCH 476/710] Diff: adds some space between 2 changes in the same file and reduces html size. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1486 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/diff.rhtml | 142 +++++++++++++++--------------- public/stylesheets/scm.css | 2 +- 2 files changed, 70 insertions(+), 74 deletions(-) diff --git a/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml index eaef1abf5..73c1e8c9e 100644 --- a/app/views/repositories/diff.rhtml +++ b/app/views/repositories/diff.rhtml @@ -11,82 +11,78 @@ <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %>

      <% end %> -<% cache(@cache_key) do %> -<% @diff.each do |table_file| %> +<% cache(@cache_key) do -%> +<% @diff.each do |table_file| -%>
      -<% if @diff_type == 'sbs' %> - - - - - - - - - - - - <% table_file.keys.sort.each do |key| %> - - - - - - - <% end %> - -
      - <%= table_file.file_name %> -
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      - <%= table_file[key].nb_line_left %> - -
      <%=to_utf8 table_file[key].line_left %>
      -
      - <%= table_file[key].nb_line_right %> - -
      <%=to_utf8 table_file[key].line_right %>
      -
      +<% if @diff_type == 'sbs' -%> + + + + + + + + + +<% 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) -%> + +<% end -%> + + + + + + +<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> +<% end -%> + +
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      <%= table_file[key].nb_line_left %> +
      <%=to_utf8 table_file[key].line_left %>
      +
      <%= table_file[key].nb_line_right %> +
      <%=to_utf8 table_file[key].line_right %>
      +
      + +<% else -%> + + + + + + + + + + +<% 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) -%> + +<% end -%> + + + + <% if table_file[key].line_left.empty? -%> + + <% else -%> + + <% end -%> + +<% 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 -%> + +
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      <%= table_file[key].nb_line_left %><%= table_file[key].nb_line_right %> +
      <%=to_utf8 table_file[key].line_right %>
      +
      +
      <%=to_utf8 table_file[key].line_left %>
      +
      +<% end -%> -<% else %> - - - - - - - - - - - - - <% table_file.keys.sort.each do |key, line| %> - - - - <% if table_file[key].line_left.empty? %> - - <% else %> - - <% end %> - - <% end %> - -
      - <%= table_file.file_name %> -
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      - <%= table_file[key].nb_line_left %> - - <%= table_file[key].nb_line_right %> - -
      <%=to_utf8 table_file[key].line_right %>
      -
      -
      <%=to_utf8 table_file[key].line_left %>
      -
      -<% end %>
      -<% end %> -<% end %> +<% end -%> +<% end -%> <% html_title(with_leading_slash(@path), 'Diff') -%> diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css index 66847af8c..d1495a5ef 100644 --- a/public/stylesheets/scm.css +++ b/public/stylesheets/scm.css @@ -2,7 +2,7 @@ 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 tr.spacing td { border: 1px solid #d7d7d7; height: 0.4em; } table.filecontent th.line-num { border: 1px solid #d7d7d7; font-size: 0.8em; From 891a71ad474812ce0108d84d649706dc64336c90 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 2 Jun 2008 20:14:06 +0000 Subject: [PATCH 477/710] Fixed: new line at the end of a file is not displayed in diff. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1487 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/abstract_adapter.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 2c254d48d..8fbae9ff8 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -279,7 +279,6 @@ module Redmine end else if line =~ /^[^\+\-\s@\\]/ - self.delete(self.keys.sort.last) @parsing = false return false elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/ From 7042879811ab27fea45ea23a06b95eb8d70207b7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 3 Jun 2008 18:30:29 +0000 Subject: [PATCH 478/710] Make the 'duplicates of' relation asymmetric: * closing a issue will close its duplicates * closing a duplicate won't close the main issue git-svn-id: http://redmine.rubyforge.org/svn/trunk@1488 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/issue.rb | 4 ++-- app/models/issue_relation.rb | 2 +- test/unit/issue_test.rb | 30 +++++++++++++++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 633253db7..d83b2ab02 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -225,9 +225,9 @@ 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 diff --git a/app/models/issue_relation.rb b/app/models/issue_relation.rb index 07e940b85..49329e0bb 100644 --- a/app/models/issue_relation.rb +++ b/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/test/unit/issue_test.rb b/test/unit/issue_test.rb index 36ba1fb45..999c4480d 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -42,7 +42,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 +52,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,6 +69,26 @@ class IssueTest < Test::Unit::TestCase assert issue3.reload.closed? end + def test_should_not_close_duplicated_issue + # Create 3 issues + issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => 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 issue = Issue.find(1) assert issue.move_to(Project.find(2)) From a77fb3b59109a296605fa435db349c3b1eb442eb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 3 Jun 2008 18:41:36 +0000 Subject: [PATCH 479/710] Missing strings (r1488). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1489 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/da.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 3 ++- lang/he.yml | 1 + lang/hu.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/no.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/th.yml | 1 + lang/uk.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + 27 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lang/bg.yml b/lang/bg.yml index 05a0b0d60..6ab6f26c6 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/cs.yml b/lang/cs.yml index 829f4449e..7413b79dd 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -627,3 +627,4 @@ 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 diff --git a/lang/da.yml b/lang/da.yml index 92df87e3f..2107498ad 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -624,3 +624,4 @@ 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 diff --git a/lang/de.yml b/lang/de.yml index 1351d1944..27d22fc54 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -623,3 +623,4 @@ 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 diff --git a/lang/en.yml b/lang/en.yml index 4184a9175..262570f1d 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -448,6 +448,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 diff --git a/lang/es.yml b/lang/es.yml index ea479630d..62528e688 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -625,3 +625,4 @@ 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 diff --git a/lang/fi.yml b/lang/fi.yml index b3b888622..b32bbb259 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/fr.yml b/lang/fr.yml index a802a44cc..8b040463f 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -447,7 +447,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 diff --git a/lang/he.yml b/lang/he.yml index c4d69ebb9..da31812cf 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/hu.yml b/lang/hu.yml index fd7bf9f52..803aa8833 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -623,3 +623,4 @@ enumeration_activities: Tevékenységek (idő rögzítés) 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 diff --git a/lang/it.yml b/lang/it.yml index 519dcd01f..e5557f915 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/ja.yml b/lang/ja.yml index 714539c7d..1ae9ab1b6 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -623,3 +623,4 @@ 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 diff --git a/lang/ko.yml b/lang/ko.yml index 81656040c..c00a9e9a0 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/lt.yml b/lang/lt.yml index 89e607a83..140631fcf 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -624,3 +624,4 @@ label_and_its_subprojects: %s projektas ir jo subprojektai 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 diff --git a/lang/nl.yml b/lang/nl.yml index eebc7d993..62bbc1468 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -623,3 +623,4 @@ 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 diff --git a/lang/no.yml b/lang/no.yml index 3f6ef6ded..d16509551 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -623,3 +623,4 @@ enumeration_activities: Aktiviteter (tidssporing) 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 diff --git a/lang/pl.yml b/lang/pl.yml index 81a7a0dba..adf676210 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 47e80a144..ca9b9073b 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/pt.yml b/lang/pt.yml index 002a1b7f8..f9472b7ea 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/ro.yml b/lang/ro.yml index 6bcd55a50..840fa7073 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -622,3 +622,4 @@ 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 diff --git a/lang/ru.yml b/lang/ru.yml index fa8fd69a9..91e509ffc 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -626,3 +626,4 @@ 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 diff --git a/lang/sr.yml b/lang/sr.yml index 9a4adf214..d507a1d97 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -623,3 +623,4 @@ 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 diff --git a/lang/sv.yml b/lang/sv.yml index 1ab11e98b..fbd7c5c40 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -623,3 +623,4 @@ 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 diff --git a/lang/th.yml b/lang/th.yml index 751aac749..9dfaf2865 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -625,3 +625,4 @@ 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 diff --git a/lang/uk.yml b/lang/uk.yml index 3ab4462ed..beabfd1f0 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -624,3 +624,4 @@ 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 3b7808aae..76db2f052 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -623,3 +623,4 @@ enumeration_activities: 活動 (時間追蹤) 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 diff --git a/lang/zh.yml b/lang/zh.yml index b5ecace82..cdb7aed75 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -623,3 +623,4 @@ enumeration_activities: 活动(时间跟踪) 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:" text_user_wrote: '%s wrote:' +label_duplicated_by: duplicated by From 735db3dae5c16d13bf7ae3af992bb9b23644a4fa Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 4 Jun 2008 17:12:59 +0000 Subject: [PATCH 480/710] Allow empty cells in wiki tables. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1490 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 17 ++++-------- test/unit/helpers/application_helper_test.rb | 29 ++++++++++++++++---- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 344fd6c78..3880eb9d3 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -504,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#{ cell }" - end + catts = shelve( catts ) if catts + cells << "\t\t\t#{ cell }" end ratts = shelve( ratts ) if ratts rows << "\t\t\n#{ cells.join( "\n" ) }\n\t\t" diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index f8bf4a602..937d8aa20 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -152,12 +152,7 @@ class ApplicationHelperTest < HelperTestCase end def test_wiki_links_in_tables - to_test = {"|Cell 11|Cell 12|Cell 13|\n|Cell 21|Cell 22||\n|Cell 31||Cell 33|" => - 'Cell 11Cell 12Cell 13' + - 'Cell 21Cell 22' + - 'Cell 31Cell 33', - - "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" => + to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" => 'Link title' + 'Other title' + 'Cell 21Last page' @@ -218,6 +213,28 @@ 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 +

      This is a table with empty cells:

      + + + + + +
      cell11cell12
      cell21cell23
      cell31cell32cell33
      +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!/) From 4db45b8ceddfa77e1935b7a4f1939b33d11244aa Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 4 Jun 2008 18:13:14 +0000 Subject: [PATCH 481/710] Fixed: changesets titles should not be multiline in atom feeds (#1356). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1491 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 11 +++++++++-- app/helpers/projects_helper.rb | 4 ++++ app/views/common/feed.atom.rxml | 4 ++-- app/views/projects/activity.rhtml | 3 ++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 47a251053..589b054f3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -90,6 +90,11 @@ module ApplicationHelper 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+)}, '\1.\2') end @@ -301,7 +306,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 @@ -340,7 +345,9 @@ 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 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ffbf25e83..49a2e5721 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -21,6 +21,10 @@ 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 diff --git a/app/views/common/feed.atom.rxml b/app/views/common/feed.atom.rxml index ac087d36b..8ceffc91c 100644 --- a/app/views/common/feed.atom.rxml +++ b/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,7 +10,7 @@ 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) + xml.title truncate_single_line(item.event_title, 100) xml.link "rel" => "alternate", "href" => url xml.id url xml.updated item.event_datetime.xmlschema diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index d4b47f7be..1a3fd9ff2 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -7,7 +7,8 @@
      <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
      <%= format_time(e.event_datetime, false) %> - <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %>
      + <%= 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 %>
      <%= format_activity_description(e.event_description) %> <%= e.event_author if e.respond_to?(:event_author) %>
      <% end -%> From cd824c6ecf38c030603cec4a8c035fb2071421fc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 4 Jun 2008 21:30:12 +0000 Subject: [PATCH 482/710] Redmine links regexp fix (#1369, url hash turned into a ticket link). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1492 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/application_helper.rb | 2 +- test/unit/helpers/application_helper_test.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 589b054f3..405c5bf44 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -298,7 +298,7 @@ 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| + text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m| leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8 link = nil if esc.nil? diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 937d8aa20..4f8ededd1 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -108,7 +108,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" => 'http://foo.bar/FAQ#3', } @project = Project.find(1) to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } From a5ee8f89866c81c400d36568b6d140b2c501d439 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 6 Jun 2008 14:37:49 +0000 Subject: [PATCH 483/710] Fixed: SVN errors lead to svn username/password being displayed to end users (#1368). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1493 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/abstract_adapter.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 8fbae9ff8..9563ed800 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -118,7 +118,7 @@ module Redmine def logger RAILS_DEFAULT_LOGGER end - + def shellout(cmd, &block) logger.debug "Shelling out: #{cmd}" if logger && logger.debug? begin @@ -127,11 +127,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: #{strip_credential(cmd)}\n with: #{msg}") + raise CommandFailed.new(msg) end end + + # Hides username/password in a given command + def self.hide_credential(cmd) + q = (RUBY_PLATFORM =~ /mswin/ ? '"' : "'") + cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx') + end + + def strip_credential(cmd) + self.class.hide_credential(cmd) + end end class Entries < Array From 8aa57058cf0eafdf4fe02f4ab9e4ceb2d2d6d23b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 6 Jun 2008 15:02:47 +0000 Subject: [PATCH 484/710] Trac importer: improves wiki link conversion (#1287). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1494 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 5341fa14a..8d5a35fd0 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -237,7 +237,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]]') From a0f83e42e42066a6b9b148f4bbc88b8ca26f7562 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 6 Jun 2008 15:14:09 +0000 Subject: [PATCH 485/710] Hides the "Replies" heading below a message if there is no reply (#1350). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1495 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/messages/show.rhtml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/messages/show.rhtml b/app/views/messages/show.rhtml index 251b7c7a5..c04c40934 100644 --- a/app/views/messages/show.rhtml +++ b/app/views/messages/show.rhtml @@ -17,6 +17,7 @@

      +<% unless @replies.empty? %>

      <%= l(:label_reply_plural) %>

      <% @replies.each do |message| %> "> @@ -30,6 +31,7 @@ <%= link_to_attachments message.attachments, :no_author => true %> <% end %> +<% end %> <% if !@topic.locked? && authorize_for('messages', 'reply') %>

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

      From 9f901a0ba2188368d64e65c9a2b35abba520cd33 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 6 Jun 2008 15:20:08 +0000 Subject: [PATCH 486/710] Fixed: Atom link on saved query does not include query_id (#1390). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1496 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/index.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 027f3f006..973f3eb25 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -45,7 +45,7 @@

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

      From f4aa04b96e4b4eaa77ff1bda7490935f5db23232 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 08:39:06 +0000 Subject: [PATCH 487/710] Fixes Chinese pdf export when the issue description is too long (#1170). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1497 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- vendor/plugins/rfpdf/lib/rfpdf/chinese.rb | 946 +++++++++++----------- 1 file changed, 473 insertions(+), 473 deletions(-) diff --git a/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb b/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb index 6fe3eee8a..5684c702d 100644 --- a/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb +++ b/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb @@ -1,473 +1,473 @@ -# Copyright (c) 2006 4ssoM LLC -# 1.12 contributed by Ed Moss. -# -# The MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# This is direct port of chinese.php -# -# Chinese PDF support. -# -# Usage is as follows: -# -# require 'fpdf' -# require 'chinese' -# pdf = FPDF.new -# pdf.extend(PDF_Chinese) -# -# This allows it to be combined with other extensions, such as the bookmark -# module. - -module PDF_Chinese - - Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,''=>490,'%'=>875,'&'=>698,'\''=>250, - '('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500, - '2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,''=>250, - '<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625, - 'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823, - 'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677, - 'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427, - 'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802, - 'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677, - 'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'end'=>480,'~'=>667} - - GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,''=>462,'%'=>797,'&'=>710,'\''=>239, - '('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462, - '2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,''=>238, - '<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563, - 'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772, - 'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620, - 'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427, - 'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793, - 'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652, - 'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'end'=>370,'~'=>605} - - def AddCIDFont(family,style,name,cw,cMap,registry) -#ActionController::Base::logger.debug registry.to_a.join(":").to_s - fontkey=family.downcase+style.upcase - unless @fonts[fontkey].nil? - Error("Font already added: family style") - end - i=@fonts.length+1 - name=name.gsub(' ','') - @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry} - end - - def AddCIDFonts(family,name,cw,cMap,registry) - AddCIDFont(family,'',name,cw,cMap,registry) - AddCIDFont(family,'B',name+',Bold',cw,cMap,registry) - AddCIDFont(family,'I',name+',Italic',cw,cMap,registry) - AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry) - end - - def AddBig5Font(family='Big5',name='MSungStd-Light-Acro') - #Add Big5 font with proportional Latin - cw=Big5_widths - cMap='ETenms-B5-H' - registry={'ordering'=>'CNS1','supplement'=>0} -#ActionController::Base::logger.debug registry.to_a.join(":").to_s - AddCIDFonts(family,name,cw,cMap,registry) - end - - def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro') - #Add Big5 font with half-witdh Latin - cw = {} - 32.upto(126) do |i| - cw[i.chr]=500 - end - cMap='ETen-B5-H' - registry={'ordering'=>'CNS1','supplement'=>0} - AddCIDFonts(family,name,cw,cMap,registry) - end - - def AddGBFont(family='GB',name='STSongStd-Light-Acro') - #Add GB font with proportional Latin - cw=GB_widths - cMap='GBKp-EUC-H' - registry={'ordering'=>'GB1','supplement'=>2} - AddCIDFonts(family,name,cw,cMap,registry) - end - - def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro') - #Add GB font with half-width Latin - 32.upto(126) do |i| - cw[i.chr]=500 - end - cMap='GBK-EUC-H' - registry={'ordering'=>'GB1','supplement'=>2} - AddCIDFonts(family,name,cw,cMap,registry) - end - - def GetStringWidth(s) - if(@CurrentFont['type']=='Type0') - return GetMBStringWidth(s) - else - return super(s) - end - end - - def GetMBStringWidth(s) - #Multi-byte version of GetStringWidth() - l=0 - cw=@CurrentFont['cw'] - nb=s.length - i=0 - while(i0 and s[nb-1]=="\n") - nb-=1 - end - b=0 - if(border) - if(border==1) - border='LTRB' - b='LRT' - b2='LR' - else - b2='' - if(border.to_s.index('L')) - b2+='L' - end - if(border.to_s.index('R')) - b2+='R' - end - b=border.to_s.index('T') ? b2+'T' : b2 - end - end - sep=-1 - i=0 - j=0 - l=0 - nl=1 - while(iwmax) - #Automatic line break - if(sep==-1 or i==j) - if(i==j) - i+=ascii ? 1 : 2 - end - Cell(w,h,s[j,i-j],b,2,align,fill) - else - Cell(w,h,s[j,sep-j],b,2,align,fill) - i=(s[sep]==' ') ? sep+1 : sep - end - sep=-1 - j=i - l=0 -# nl+=1 - if(border and nl==2) - b=b2 - end - else - i+=ascii ? 1 : 2 - end - end - #Last chunk - if(border and not border.to_s.index('B').nil?) - b+='B' - end - Cell(w,h,s[j,i-j],b,2,align,fill) - @x=@lMargin - end - - def Write(h,txt,link='') - if(@CurrentFont['type']=='Type0') - MBWrite(h,txt,link) - else - super(h,txt,link) - end - end - - def MBWrite(h,txt,link) - #Multi-byte version of Write() - cw=@CurrentFont['cw'] - w=@w-@rMargin-@x - wmax=(w-2*@cMargin)*1000/@FontSize - s=txt.gsub("\r",'') - nb=s.length - sep=-1 - i=0 - j=0 - l=0 - nl=1 - while(iwmax) - #Automatic line break - if(sep==-1 or i==j) - if(@x>@lMargin) - #Move to next line - @x=@lMargin - @y+=h - w=@w-@rMargin-@x - wmax=(w-2*@cMargin)*1000/@FontSize - i+=1 - nl+=1 - next - end - if(i==j) - i+=ascii ? 1 : 2 - end - Cell(w,h,s[j,i-j],0,2,'',0,link) - else - Cell(w,h,s[j,sep-j],0,2,'',0,link) - i=(s[sep]==' ') ? sep+1 : sep - end - sep=-1 - j=i - l=0 - if(nl==1) - @x=@lMargin - w=@w-@rMargin-@x - wmax=(w-2*@cMargin)*1000/@FontSize - end - nl+=1 - else - i+=ascii ? 1 : 2 - end - end - #Last chunk - if(i!=j) - Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link) - end - end - -private - - def putfonts() - nf=@n - @diffs.each do |diff| - #Encodings - newobj() - out('<>') - out('endobj') - end - # mqr=get_magic_quotes_runtime() - # set_magic_quotes_runtime(0) - @FontFiles.each_pair do |file, info| - #Font file embedding - newobj() - @FontFiles[file]['n']=@n - if(defined('FPDF_FONTPATH')) - file=FPDF_FONTPATH+file - end - size=filesize(file) - if(!size) - Error('Font file not found') - end - out('<>') - f=fopen(file,'rb') - putstream(fread(f,size)) - fclose(f) - out('endobj') - end -# - # set_magic_quotes_runtime(mqr) -# - @fonts.each_pair do |k, font| - #Font objects - newobj() - @fonts[k]['n']=@n - out('<>') - out('endobj') - if(font['type']!='core') - #Widths - newobj() - cw=font['cw'] - s='[' - 32.upto(255) do |i| - s+=cw[i.chr]+' ' - end - out(s+']') - out('endobj') - #Descriptor - newobj() - s='<>') - out('endobj') - end - end - end - end - - def putType0(font) - #Type0 - out('/Subtype /Type0') - out('/BaseFont /'+font['name']+'-'+font['CMap']) - out('/Encoding /'+font['CMap']) - out('/DescendantFonts ['+(@n+1).to_s+' 0 R]') - out('>>') - out('endobj') - #CIDFont - newobj() - out('<>') - out('/FontDescriptor '+(@n+1).to_s+' 0 R') - if(font['CMap']=='ETen-B5-H') - w='13648 13742 500' - elsif(font['CMap']=='GBK-EUC-H') - w='814 907 500 7716 [500]' - else - # ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s - # ActionController::Base::logger.debug font['cw'].values.join(' ').to_s - w='1 [' - font['cw'].keys.sort.each {|key| - w+=font['cw'][key].to_s + " " -# ActionController::Base::logger.debug key.to_s -# ActionController::Base::logger.debug font['cw'][key].to_s - } - w +=']' - end - out('/W ['+w+']>>') - out('endobj') - #Font descriptor - newobj() - out('<>') - out('endobj') - end -end +# Copyright (c) 2006 4ssoM LLC +# 1.12 contributed by Ed Moss. +# +# The MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# This is direct port of chinese.php +# +# Chinese PDF support. +# +# Usage is as follows: +# +# require 'fpdf' +# require 'chinese' +# pdf = FPDF.new +# pdf.extend(PDF_Chinese) +# +# This allows it to be combined with other extensions, such as the bookmark +# module. + +module PDF_Chinese + + Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,''=>490,'%'=>875,'&'=>698,'\''=>250, + '('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500, + '2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,''=>250, + '<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625, + 'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823, + 'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677, + 'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427, + 'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802, + 'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677, + 'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'end'=>480,'~'=>667} + + GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,''=>462,'%'=>797,'&'=>710,'\''=>239, + '('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462, + '2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,''=>238, + '<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563, + 'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772, + 'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620, + 'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427, + 'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793, + 'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652, + 'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'end'=>370,'~'=>605} + + def AddCIDFont(family,style,name,cw,cMap,registry) +#ActionController::Base::logger.debug registry.to_a.join(":").to_s + fontkey=family.downcase+style.upcase + unless @fonts[fontkey].nil? + Error("Font already added: family style") + end + i=@fonts.length+1 + name=name.gsub(' ','') + @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry} + end + + def AddCIDFonts(family,name,cw,cMap,registry) + AddCIDFont(family,'',name,cw,cMap,registry) + AddCIDFont(family,'B',name+',Bold',cw,cMap,registry) + AddCIDFont(family,'I',name+',Italic',cw,cMap,registry) + AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry) + end + + def AddBig5Font(family='Big5',name='MSungStd-Light-Acro') + #Add Big5 font with proportional Latin + cw=Big5_widths + cMap='ETenms-B5-H' + registry={'ordering'=>'CNS1','supplement'=>0} +#ActionController::Base::logger.debug registry.to_a.join(":").to_s + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro') + #Add Big5 font with half-witdh Latin + cw = {} + 32.upto(126) do |i| + cw[i.chr]=500 + end + cMap='ETen-B5-H' + registry={'ordering'=>'CNS1','supplement'=>0} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddGBFont(family='GB',name='STSongStd-Light-Acro') + #Add GB font with proportional Latin + cw=GB_widths + cMap='GBKp-EUC-H' + registry={'ordering'=>'GB1','supplement'=>2} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro') + #Add GB font with half-width Latin + 32.upto(126) do |i| + cw[i.chr]=500 + end + cMap='GBK-EUC-H' + registry={'ordering'=>'GB1','supplement'=>2} + AddCIDFonts(family,name,cw,cMap,registry) + end + + def GetStringWidth(s) + if(@CurrentFont['type']=='Type0') + return GetMBStringWidth(s) + else + return super(s) + end + end + + def GetMBStringWidth(s) + #Multi-byte version of GetStringWidth() + l=0 + cw=@CurrentFont['cw'] + nb=s.length + i=0 + while(i0 and s[nb-1]=="\n") + nb-=1 + end + b=0 + if(border) + if(border==1) + border='LTRB' + b='LRT' + b2='LR' + else + b2='' + if(border.to_s.index('L')) + b2+='L' + end + if(border.to_s.index('R')) + b2+='R' + end + b=border.to_s.index('T') ? b2+'T' : b2 + end + end + sep=-1 + i=0 + j=0 + l=0 + nl=1 + while(iwmax) + #Automatic line break + if(sep==-1 or i==j) + if(i==j) + i+=ascii ? 1 : 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(iwmax) + #Automatic line break + if(sep==-1 or i==j) + if(@x>@lMargin) + #Move to next line + @x=@lMargin + @y+=h + w=@w-@rMargin-@x + wmax=(w-2*@cMargin)*1000/@FontSize + i+=1 + nl+=1 + next + end + if(i==j) + i+=ascii ? 1 : 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('<>') + out('endobj') + end + # mqr=get_magic_quotes_runtime() + # set_magic_quotes_runtime(0) + @FontFiles.each_pair do |file, info| + #Font file embedding + newobj() + @FontFiles[file]['n']=@n + if(defined('FPDF_FONTPATH')) + file=FPDF_FONTPATH+file + end + size=filesize(file) + if(!size) + Error('Font file not found') + end + out('<>') + f=fopen(file,'rb') + putstream(fread(f,size)) + fclose(f) + out('endobj') + end +# + # set_magic_quotes_runtime(mqr) +# + @fonts.each_pair do |k, font| + #Font objects + newobj() + @fonts[k]['n']=@n + out('<>') + out('endobj') + if(font['type']!='core') + #Widths + newobj() + cw=font['cw'] + s='[' + 32.upto(255) do |i| + s+=cw[i.chr]+' ' + end + out(s+']') + out('endobj') + #Descriptor + newobj() + s='<>') + out('endobj') + end + end + end + end + + def putType0(font) + #Type0 + out('/Subtype /Type0') + out('/BaseFont /'+font['name']+'-'+font['CMap']) + out('/Encoding /'+font['CMap']) + out('/DescendantFonts ['+(@n+1).to_s+' 0 R]') + out('>>') + out('endobj') + #CIDFont + newobj() + out('<>') + out('/FontDescriptor '+(@n+1).to_s+' 0 R') + if(font['CMap']=='ETen-B5-H') + w='13648 13742 500' + elsif(font['CMap']=='GBK-EUC-H') + w='814 907 500 7716 [500]' + else + # ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s + # ActionController::Base::logger.debug font['cw'].values.join(' ').to_s + w='1 [' + font['cw'].keys.sort.each {|key| + w+=font['cw'][key].to_s + " " +# ActionController::Base::logger.debug key.to_s +# ActionController::Base::logger.debug font['cw'][key].to_s + } + w +=']' + end + out('/W ['+w+']>>') + out('endobj') + #Font descriptor + newobj() + out('<>') + out('endobj') + end +end From a6da479a63c9c5add6b6d81adfca99fbaa73d0a7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 09:03:20 +0000 Subject: [PATCH 488/710] Translations updates. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1498 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/hu.yml | 8 ++-- lang/no.yml | 10 ++--- lang/ru.yml | 12 +++--- lang/zh-tw.yml | 8 ++-- lang/zh.yml | 6 +-- .../calendar/lang/calendar-zh-tw.js | 12 +++--- .../javascripts/calendar/lang/calendar-zh.js | 40 +++++++++---------- .../jstoolbar/lang/jstoolbar-zh-tw.js | 6 +-- .../jstoolbar/lang/jstoolbar-zh.js | 30 +++++++------- 9 files changed, 66 insertions(+), 66 deletions(-) diff --git a/lang/hu.yml b/lang/hu.yml index 803aa8833..2a399bc13 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -620,7 +620,7 @@ 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 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 +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 diff --git a/lang/no.yml b/lang/no.yml index d16509551..a31751da6 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -91,6 +91,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 @@ -445,7 +447,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 @@ -594,6 +597,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 @@ -620,7 +624,3 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) -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 diff --git a/lang/ru.yml b/lang/ru.yml index 91e509ffc..37f2aef30 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -332,7 +332,7 @@ 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: Добавленный комментарий @@ -621,9 +621,9 @@ label_overall_activity: Сводная активность 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:' +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: duplicated by diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 76db2f052..c3cd096a3 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -91,6 +91,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 個錯誤 @@ -446,6 +448,7 @@ label_relation_new: 建立新關聯 label_relation_delete: 刪除關聯 label_relates_to: 關聯至 label_duplicates: 已重複 +label_duplicated_by: 與後面所列項目重複 label_blocks: 阻擋 label_blocked_by: 被阻擋 label_precedes: 優先於 @@ -594,6 +597,7 @@ text_destroy_time_entries_question: 您即將刪除的項目已報工 %.02f 小 text_destroy_time_entries: 刪除已報工的時數 text_assign_time_entries_to_project: 指定已報工的時數至專案中 text_reassign_time_entries: '重新指定已報工的時數至此項目:' +text_user_wrote: '%s 先前提到:' default_role_manager: 管理人員 default_role_developper: 開發人員 @@ -620,7 +624,3 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) -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 diff --git a/lang/zh.yml b/lang/zh.yml index cdb7aed75..9e22e6874 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -91,6 +91,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 个错误 @@ -594,6 +596,7 @@ text_destroy_time_entries_question: 您要删除的问题已经上报了 %.02f text_destroy_time_entries: 删除上报的工作量 text_assign_time_entries_to_project: 将已上报的工作量提交到项目中 text_reassign_time_entries: '将已上报的工作量指定到此问题:' +text_user_wrote: '%s 写到:' default_role_manager: 管理人员 default_role_developper: 开发人员 @@ -620,7 +623,4 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) -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:" -text_user_wrote: '%s wrote:' label_duplicated_by: duplicated by diff --git a/public/javascripts/calendar/lang/calendar-zh-tw.js b/public/javascripts/calendar/lang/calendar-zh-tw.js index c48d25b0e..1e759db10 100644 --- a/public/javascripts/calendar/lang/calendar-zh-tw.js +++ b/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/public/javascripts/calendar/lang/calendar-zh.js b/public/javascripts/calendar/lang/calendar-zh.js index ddb092bfa..121653fba 100644 --- a/public/javascripts/calendar/lang/calendar-zh.js +++ b/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/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js index a87ad3442..86599c5a2 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js @@ -9,8 +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['Quote'] = '引文'; +jsToolBar.strings['Unquote'] = '取消引文'; +jsToolBar.strings['Preformatted text'] = '已格式文字'; jsToolBar.strings['Wiki link'] = '連結至 Wiki 頁面'; jsToolBar.strings['Image'] = '圖片'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-zh.js b/public/javascripts/jstoolbar/lang/jstoolbar-zh.js index 2d68498f9..a9b6ba230 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-zh.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-zh.js @@ -1,16 +1,16 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Code'] = 'Inline Code'; -jsToolBar.strings['Heading 1'] = 'Heading 1'; -jsToolBar.strings['Heading 2'] = 'Heading 2'; -jsToolBar.strings['Heading 3'] = 'Heading 3'; -jsToolBar.strings['Unordered list'] = 'Unordered list'; -jsToolBar.strings['Ordered list'] = 'Ordered list'; -jsToolBar.strings['Quote'] = 'Quote'; -jsToolBar.strings['Unquote'] = 'Remove Quote'; -jsToolBar.strings['Preformatted text'] = 'Preformatted text'; -jsToolBar.strings['Wiki link'] = 'Link to a Wiki page'; -jsToolBar.strings['Image'] = 'Image'; +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'] = '图片'; From aa9d04a4a7ec1232477f0823a584da49aae11d58 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 09:19:50 +0000 Subject: [PATCH 489/710] Mercurial adapter improvements (patch #1199 by Pierre Paysant-Le Roux). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1499 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/abstract_adapter.rb | 7 +- .../adapters/mercurial/hg-template-0.9.5.tmpl | 12 ++ .../adapters/mercurial/hg-template-1.0.tmpl | 12 ++ lib/redmine/scm/adapters/mercurial_adapter.rb | 160 +++++++++--------- test/unit/mercurial_adapter_test.rb | 49 ++++++ test/unit/repository_mercurial_test.rb | 20 +++ 6 files changed, 181 insertions(+), 79 deletions(-) create mode 100644 lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl create mode 100644 lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl create mode 100644 test/unit/mercurial_adapter_test.rb diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 9563ed800..80058a2bf 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -94,6 +94,11 @@ module Redmine path ||= '' (path[0,1]!="/") ? "/#{path}" : path end + + def with_trailling_slash(path) + path ||= '' + (path[-1,1] == "/") ? path : "#{path}/" + end def shell_quote(str) if RUBY_PLATFORM =~ /mswin/ @@ -102,7 +107,7 @@ module Redmine "'" + str.gsub(/'/, "'\"'\"'") + "'" end end - + private def retrieve_root_url info = self.info diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl new file mode 100644 index 000000000..b3029e6ff --- /dev/null +++ b/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 = '\n{author|escape}\n{date|isodate}\n\n{files}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' + +file = '{file|escape}\n' +file_add = '{file_add|escape}\n' +file_del = '{file_del|escape}\n' +file_copy = '{name|urlescape}\n' +tag = '{tag|escape}\n' +header='\n\n\n' +# footer="" \ No newline at end of file diff --git a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl new file mode 100644 index 000000000..3eef85016 --- /dev/null +++ b/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 = '\n{author|escape}\n{date|isodate}\n\n{file_mods}{file_adds}{file_dels}{file_copies}\n{desc|escape}\n{tags}\n\n' + +file_mod = '{file_mod|escape}\n' +file_add = '{file_add|escape}\n' +file_del = '{file_del|escape}\n' +file_copy = '{name|urlescape}\n' +tag = '{tag|escape}\n' +header='\n\n\n' +# footer="" diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 6f42dda06..be01b7bbc 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -21,9 +21,12 @@ 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" def info cmd = "#{HG_BIN} -R #{target('')} root" @@ -33,8 +36,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,62 +46,72 @@ 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.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 << "") + 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 @@ -125,7 +138,7 @@ module Redmine 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 +153,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 @@ -153,45 +167,35 @@ module Redmine 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 - }) + # The hg version 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. + def hgversion + theversion = hgversion_from_command_line + if theversion.match(/^\d+(\.\d+)+/) + theversion.split(".").collect(&:to_i) + # elsif match = theversion.match(/[[:xdigit:]]{12}/) + # match[0] + else + "Unknown version" + end 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 + def template_path + @template ||= begin + if hgversion.is_a?(String) or ((hgversion <=> [0,9,5]) > 0) + ver = "1.0" + else + ver = "0.9.5" + end + "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" + end + end + + private + + def hgversion_from_command_line + @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] end end end diff --git a/test/unit/mercurial_adapter_test.rb b/test/unit/mercurial_adapter_test.rb new file mode 100644 index 000000000..b4aaaec61 --- /dev/null +++ b/test/unit/mercurial_adapter_test.rb @@ -0,0 +1,49 @@ +require File.dirname(__FILE__) + '/../test_helper' +begin + require 'mocha' + + class MercurialAdapterTest < Test::Unit::TestCase + + TEMPLATES_DIR = "#{RAILS_ROOT}/extra/mercurial" + TEMPLATE_NAME = "hg-template" + TEMPLATE_EXTENSION = "tmpl" + + REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository' + + + def test_version_template_0_9_5 + # 0.9.5 + test_version_template_for("0.9.5", [0,9,5], "0.9.5") + end + + def test_version_template_1_0 + # 1.0 + test_version_template_for("1.0", [1,0], "1.0") + end + + def test_version_template_1_0_win + test_version_template_for("1e4ddc9ac9f7+20080325", "Unknown version", "1.0") + end + + def test_version_template_1_0_1_win + test_version_template_for("1.0.1+20080525", [1,0,1], "1.0") + end + + def test_version_template_changeset_id + test_version_template_for("1916e629a29d", "Unknown version", "1.0") + end + + private + + def test_version_template_for(hgversion, version, templateversion) + Redmine::Scm::Adapters::MercurialAdapter.any_instance.stubs(:hgversion_from_command_line).returns(hgversion) + adapter = Redmine::Scm::Adapters::MercurialAdapter.new(REPOSITORY_PATH) + assert_equal version, adapter.hgversion + assert_equal "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{templateversion}.#{TEMPLATE_EXTENSION}", adapter.template_path + assert File.exist?(adapter.template_path) + end + end + +rescue LoadError + def test_fake; assert(false, "Requires mocha to run those tests") end +end diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb index 21ddf1e3a..0f993ac16 100644 --- a/test/unit/repository_mercurial_test.rb +++ b/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 From 6d5db302eeabd3a64ba6c1106e3eadde1ad05aae Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 10:08:11 +0000 Subject: [PATCH 490/710] Subversion adapter: ignore directories with no commit date (#1370). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1500 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/subversion_adapter.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index 40c7eb3f1..1cbdce135 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -64,6 +64,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'], From 956ad0a3f954d71240e454df950cddfc230076bf Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 11:42:37 +0000 Subject: [PATCH 491/710] Fixed: Reply/Quote Function Newline Issue in Internet Explorer (#1362). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1501 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 682a0cc6b..c042a80ef 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -217,10 +217,10 @@ class IssuesController < ApplicationController user = @issue.author text = @issue.description end - content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " - content << text.to_s.strip.gsub(%r{
      ((.|\s)*?)
      }m, '[...]').gsub("\n", "\n> ") + "\n\n" + content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " + content << text.to_s.strip.gsub(%r{
      ((.|\s)*?)
      }m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" render(:update) { |page| - page.replace_html "notes", content + page.<< "$('notes').value = \"#{content}\";" page.show 'update' page << "Form.Element.focus('notes');" page << "Element.scrollTo('update');" From 042da97f541824d72a60d51acd118dcf7892853e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 14:55:32 +0000 Subject: [PATCH 492/710] Trac importer: read session_attribute table to find user's email and real name (#1340). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1502 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/tasks/migrate_from_trac.rake | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/tasks/migrate_from_trac.rake b/lib/tasks/migrate_from_trac.rake index 8d5a35fd0..4226d79c0 100644 --- a/lib/tasks/migrate_from_trac.rake +++ b/lib/tasks/migrate_from_trac.rake @@ -195,6 +195,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? @@ -202,10 +206,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') From d446469f5d2d7c50911465c681ab7a469a2f9b24 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 15:08:59 +0000 Subject: [PATCH 493/710] Removes constraint on enumerations name (#1384). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1503 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/enumeration.rb | 1 - app/views/enumerations/list.rhtml | 2 +- app/views/timelog/_report_criteria.rhtml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/enumeration.rb b/app/models/enumeration.rb index 400681a43..e86768724 100644 --- a/app/models/enumeration.rb +++ b/app/models/enumeration.rb @@ -23,7 +23,6 @@ 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 OPTIONS = { "IPRI" => :enumeration_issue_priorities, diff --git a/app/views/enumerations/list.rhtml b/app/views/enumerations/list.rhtml index 9de9bf37c..1967e5cf9 100644 --- a/app/views/enumerations/list.rhtml +++ b/app/views/enumerations/list.rhtml @@ -8,7 +8,7 @@ <% enumerations.each do |enumeration| %> - + <%= '' * level %> - + <%= '' * (criterias.length - level - 1) -%> <% total = 0 -%> <% @periods.each do |period| -%> From 383da1e6d610c9738aa3ed76db5c68de0dfcea5c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 15:14:42 +0000 Subject: [PATCH 494/710] Fixes functional tests broken by r1501. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1504 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/functional/issues_controller_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 7c7d44e5f..582c27d9b 100644 --- a/test/functional/issues_controller_test.rb +++ b/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 @@ -269,7 +271,6 @@ class IssuesControllerTest < Test::Unit::TestCase get :reply, :id => 1 assert_response :success assert_select_rjs :show, "update" - assert_select_rjs :replace_html, "notes" end def test_reply_to_note @@ -277,7 +278,6 @@ class IssuesControllerTest < Test::Unit::TestCase get :reply, :id => 1, :journal_id => 2 assert_response :success assert_select_rjs :show, "update" - assert_select_rjs :replace_html, "notes" end def test_post_edit From 2e8b2d5e1312365afc60d2456ab1f67a336d77f3 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 7 Jun 2008 20:06:44 +0000 Subject: [PATCH 495/710] Display status change before subject of issue on the activity view otherwise it may be truncated. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1505 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/journal.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/journal.rb b/app/models/journal.rb index edf261e6d..ac141f68c 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -30,7 +30,7 @@ class Journal < ActiveRecord::Base :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' }, From 05cd95987f1946f2bce138d339ef64511e15bde6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 10:10:54 +0000 Subject: [PATCH 496/710] Fixes tabulation with Firefox 2 on tabular forms. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1506 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/stylesheets/application.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index f51d1841c..dd942c819 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -229,7 +229,7 @@ height: 1%; clear:left; } -html>body .tabular p {overflow:auto;} +html>body .tabular p {overflow:hidden;} .tabular label{ font-weight: bold; From dfe62d7f51b4aadb1034e2e64de2524b30dfd17b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 14:59:26 +0000 Subject: [PATCH 497/710] Ability to disable unused SCM adapters in application settings. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1507 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 4 ++-- app/helpers/repositories_helper.rb | 9 ++++++--- app/models/repository.rb | 5 ++++- app/views/projects/settings/_repository.rhtml | 2 +- app/views/settings/_repositories.rhtml | 7 +++++++ config/settings.yml | 9 +++++++++ lang/bg.yml | 1 + lang/cs.yml | 1 + lang/da.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/hu.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/no.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/th.yml | 1 + lang/uk.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + test/unit/repository_test.rb | 8 ++++++++ 34 files changed, 64 insertions(+), 7 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 64eb05793..ea3b117d9 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -34,9 +34,9 @@ class RepositoriesController < ApplicationController @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 diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 22bdec9df..d2cc664e6 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -48,10 +48,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)") ) diff --git a/app/models/repository.rb b/app/models/repository.rb index 1ea77f24f..e6ed7da52 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -19,7 +19,10 @@ 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 :changes, :through => :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) diff --git a/app/views/projects/settings/_repository.rhtml b/app/views/projects/settings/_repository.rhtml index 95830ab98..dcfabbbf0 100644 --- a/app/views/projects/settings/_repository.rhtml +++ b/app/views/projects/settings/_repository.rhtml @@ -17,5 +17,5 @@ :class => 'icon icon-del') if @repository && !@repository.new_record? %> -<%= 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/app/views/settings/_repositories.rhtml b/app/views/settings/_repositories.rhtml index 59b3b51de..127801be2 100644 --- a/app/views/settings/_repositories.rhtml +++ b/app/views/settings/_repositories.rhtml @@ -7,6 +7,13 @@

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

      +

      +<% 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][]', '' %> +

      +

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

      diff --git a/config/settings.yml b/config/settings.yml index bb501823e..616665f23 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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: diff --git a/lang/bg.yml b/lang/bg.yml index 6ab6f26c6..5f3548684 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/cs.yml b/lang/cs.yml index 7413b79dd..26e0f5131 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -628,3 +628,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/da.yml b/lang/da.yml index 2107498ad..b2f18db4c 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -625,3 +625,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/de.yml b/lang/de.yml index 27d22fc54..1e4040f3d 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -624,3 +624,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/en.yml b/lang/en.yml index 262570f1d..b7f217e6c 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -213,6 +213,7 @@ 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 project_module_issue_tracking: Issue tracking project_module_time_tracking: Time tracking diff --git a/lang/es.yml b/lang/es.yml index 62528e688..c0ecb997b 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -626,3 +626,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/fi.yml b/lang/fi.yml index b32bbb259..6ac4aea7e 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/fr.yml b/lang/fr.yml index 8b040463f..4db2f8f3a 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -214,6 +214,7 @@ 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 project_module_issue_tracking: Suivi des demandes project_module_time_tracking: Suivi du temps passé diff --git a/lang/he.yml b/lang/he.yml index da31812cf..e23698d32 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/hu.yml b/lang/hu.yml index 2a399bc13..64518271c 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -624,3 +624,4 @@ mail_body_reminder: "%d neked kiosztott feladat határidős az elkövetkező %d 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: Enabled SCM diff --git a/lang/it.yml b/lang/it.yml index e5557f915..5ab9ad310 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/ja.yml b/lang/ja.yml index 1ae9ab1b6..c693d68e5 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -624,3 +624,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/ko.yml b/lang/ko.yml index c00a9e9a0..af1c768af 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/lt.yml b/lang/lt.yml index 140631fcf..ff8f6b695 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -625,3 +625,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/nl.yml b/lang/nl.yml index 62bbc1468..9ee913848 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -624,3 +624,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/no.yml b/lang/no.yml index a31751da6..e3b88b110 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -624,3 +624,4 @@ default_activity_development: Utvikling enumeration_issue_priorities: Sakssprioriteringer enumeration_doc_categories: Dokument-kategorier enumeration_activities: Aktiviteter (tidssporing) +setting_enabled_scm: Enabled SCM diff --git a/lang/pl.yml b/lang/pl.yml index adf676210..75e04ecd4 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index ca9b9073b..dabab5bfe 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/pt.yml b/lang/pt.yml index f9472b7ea..2845f390c 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/ro.yml b/lang/ro.yml index 840fa7073..dd51e59be 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -623,3 +623,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/ru.yml b/lang/ru.yml index 37f2aef30..c3ec91897 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -627,3 +627,4 @@ mail_body_reminder: "%d назначенных на вас задач на сл mail_subject_reminder: "%d назначенных на вас задач в ближайшие дни" text_user_wrote: '%s написал:' label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM diff --git a/lang/sr.yml b/lang/sr.yml index d507a1d97..92906760b 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -624,3 +624,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/sv.yml b/lang/sv.yml index fbd7c5c40..f08e45eb0 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -624,3 +624,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/th.yml b/lang/th.yml index 9dfaf2865..85a1f8c01 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -626,3 +626,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/uk.yml b/lang/uk.yml index beabfd1f0..ae8c5cbc0 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -625,3 +625,4 @@ mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index c3cd096a3..2ee595dcd 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -624,3 +624,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +setting_enabled_scm: Enabled SCM diff --git a/lang/zh.yml b/lang/zh.yml index 9e22e6874..d82363414 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -624,3 +624,4 @@ enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) label_duplicated_by: duplicated by +setting_enabled_scm: Enabled SCM diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb index 7764ee04a..270b0bea1 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -45,6 +45,14 @@ class RepositoryTest < Test::Unit::TestCase assert_equal repository, project.repository 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) + 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 From e69b4647f201b33fff3a87890807dbffd549c0fc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 15:40:24 +0000 Subject: [PATCH 498/710] Adds Filesystem adapter (patch #1393 by Paul R). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1508 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/repositories_helper.rb | 4 + app/models/repository/filesystem.rb | 43 ++++++++ doc/RUNNING_TESTS | 4 + lib/redmine.rb | 2 +- lib/redmine/scm/adapters/abstract_adapter.rb | 10 ++ .../scm/adapters/filesystem_adapter.rb | 94 ++++++++++++++++++ .../repositories/filesystem_repository.tar.gz | Bin 0 -> 265 bytes test/unit/filesystem_adapter_test.rb | 42 ++++++++ test/unit/repository_filesystem_test.rb | 54 ++++++++++ test/unit/repository_test.rb | 2 + 10 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 app/models/repository/filesystem.rb create mode 100644 lib/redmine/scm/adapters/filesystem_adapter.rb create mode 100644 test/fixtures/repositories/filesystem_repository.tar.gz create mode 100644 test/unit/filesystem_adapter_test.rb create mode 100644 test/unit/repository_filesystem_test.rb diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index d2cc664e6..10f6e7396 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -98,4 +98,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/app/models/repository/filesystem.rb b/app/models/repository/filesystem.rb new file mode 100644 index 000000000..da096cc09 --- /dev/null +++ b/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/doc/RUNNING_TESTS b/doc/RUNNING_TESTS index 7a5e2b992..18dfe6983 100644 --- a/doc/RUNNING_TESTS +++ b/doc/RUNNING_TESTS @@ -24,6 +24,10 @@ Git --- gunzip < test/fixtures/repositories/git_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/lib/redmine.rb b/lib/redmine.rb index 7b9832d62..84eda9f72 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -11,7 +11,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| diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 80058a2bf..bd77ce203 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -100,6 +100,16 @@ module Redmine (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/ '"' + str.gsub(/"/, '\\"') + '"' diff --git a/lib/redmine/scm/adapters/filesystem_adapter.rb b/lib/redmine/scm/adapters/filesystem_adapter.rb new file mode 100644 index 000000000..c06b7e6e3 --- /dev/null +++ b/lib/redmine/scm/adapters/filesystem_adapter.rb @@ -0,0 +1,94 @@ +# 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 => if (File.directory?(target)) + nil else File.size(target) end, + :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)).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/test/fixtures/repositories/filesystem_repository.tar.gz b/test/fixtures/repositories/filesystem_repository.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7e8a4ac56a63c90171110e9433a8762bcc7c136b GIT binary patch literal 265 zcmb2|=HO^%_4Ht1PRq}^IzA0+Yb#*tDB`V6EQEDEZn|faF$mPkN>o0vSa_+XL+utucS07?GWB>U% z{pIfR8v?Uz>KBFo^J@EJ%6WK4$Mr)y?9ba<&J(J;p+0-7bYm#b`Ttupl>RZNy{Q+C z`}=QuSkiy}M4SH~zO)Ozn{nsA{Lk638aMuPOPs&Ib-G7(!nHq_mFqJ(KJzmm1B2tT MmP_Io88jFe02tJEzyJUM literal 0 HcmV?d00001 diff --git a/test/unit/filesystem_adapter_test.rb b/test/unit/filesystem_adapter_test.rb new file mode 100644 index 000000000..720d1e92c --- /dev/null +++ b/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/test/unit/repository_filesystem_test.rb b/test/unit/repository_filesystem_test.rb new file mode 100644 index 000000000..6b643f96f --- /dev/null +++ b/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/test/unit/repository_test.rb b/test/unit/repository_test.rb index 270b0bea1..03d836ef7 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -51,6 +51,8 @@ class RepositoryTest < Test::Unit::TestCase 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 From 5dea2007746a5d7027cb5b3c70674e94c3563f37 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 15:43:32 +0000 Subject: [PATCH 499/710] Filesystem adapter: negative size is displayed for large files under win32. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1509 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/filesystem_adapter.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/redmine/scm/adapters/filesystem_adapter.rb b/lib/redmine/scm/adapters/filesystem_adapter.rb index c06b7e6e3..d300f6cbb 100644 --- a/lib/redmine/scm/adapters/filesystem_adapter.rb +++ b/lib/redmine/scm/adapters/filesystem_adapter.rb @@ -60,8 +60,7 @@ module Redmine # below : list unreadable files, but dont link them. :path => File.readable?(target) ? relative_path : "", :kind => (File.directory?(target) ? 'dir' : 'file'), - :size => if (File.directory?(target)) - nil else File.size(target) end, + :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first), :lastrev => Revision.new({:time => (File.mtime(target)).localtime, }) From a0d141460655e0bd6e3b2b6e8ccccddc34ade68a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 15:44:23 +0000 Subject: [PATCH 500/710] Filesystem adapter: read files in binary mode. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1510 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/filesystem_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redmine/scm/adapters/filesystem_adapter.rb b/lib/redmine/scm/adapters/filesystem_adapter.rb index d300f6cbb..99296a090 100644 --- a/lib/redmine/scm/adapters/filesystem_adapter.rb +++ b/lib/redmine/scm/adapters/filesystem_adapter.rb @@ -72,7 +72,7 @@ module Redmine end def cat(path, identifier=nil) - File.new(target(path)).read + File.new(target(path), "rb").read end private From 0aba4255f58252da835729b09ccfce9b0ded14be Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 15:46:15 +0000 Subject: [PATCH 501/710] Don't display the table headers if there is no changeset to display. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1511 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/changes.rhtml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/repositories/changes.rhtml b/app/views/repositories/changes.rhtml index 2d7462b29..341c6cba5 100644 --- a/app/views/repositories/changes.rhtml +++ b/app/views/repositories/changes.rhtml @@ -13,6 +13,7 @@ <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>

      -<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%> +<%= render(:partial => 'revisions', + :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %> <% html_title(l(:label_change_plural)) -%> From 731d15fe9b29a28362fff19ed7dad9df2516d73b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 15:47:55 +0000 Subject: [PATCH 502/710] Don't search for the changeset if revision identifier is nil. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1512 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/_dir_list_content.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/repositories/_dir_list_content.rhtml b/app/views/repositories/_dir_list_content.rhtml index 3564e52ab..c3bd56e09 100644 --- a/app/views/repositories/_dir_list_content.rhtml +++ b/app/views/repositories/_dir_list_content.rhtml @@ -26,7 +26,7 @@ end %>
      -<% 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 %> <% end %> From ec0525d4975b9cae9982695cb0a19ac9c4bd1a3c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 16:28:42 +0000 Subject: [PATCH 503/710] Move unified diff parser out of the scm abstract adapter so it can be reused for viewing attached diffs (#1403). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1513 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 2 +- app/models/repository.rb | 4 +- app/models/repository/cvs.rb | 4 +- app/models/repository/darcs.rb | 4 +- app/views/repositories/diff.rhtml | 2 +- lib/redmine/scm/adapters/abstract_adapter.rb | 161 +--------------- lib/redmine/scm/adapters/bazaar_adapter.rb | 4 +- lib/redmine/scm/adapters/cvs_adapter.rb | 4 +- lib/redmine/scm/adapters/darcs_adapter.rb | 4 +- lib/redmine/scm/adapters/git_adapter.rb | 4 +- lib/redmine/scm/adapters/mercurial_adapter.rb | 4 +- .../scm/adapters/subversion_adapter.rb | 2 +- lib/redmine/unified_diff.rb | 178 ++++++++++++++++++ 13 files changed, 198 insertions(+), 179 deletions(-) create mode 100644 lib/redmine/unified_diff.rb diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index ea3b117d9..5fb2fdd75 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -153,7 +153,7 @@ class RepositoriesController < ApplicationController @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) + @diff = @repository.diff(@path, @rev, @rev_to) show_error_not_found unless @diff end rescue Redmine::Scm::Adapters::CommandFailed => e diff --git a/app/models/repository.rb b/app/models/repository.rb index e6ed7da52..1cf61393f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -55,8 +55,8 @@ class Repository < ActiveRecord::Base scm.entries(path, identifier) end - def diff(path, rev, rev_to, type) - scm.diff(path, rev, rev_to, type) + def diff(path, rev, rev_to) + scm.diff(path, rev, rev_to) end # Default behaviour: we search in cached changesets diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb index c2d8be977..ea75de5d3 100644 --- a/app/models/repository/cvs.rb +++ b/app/models/repository/cvs.rb @@ -53,7 +53,7 @@ class Repository::Cvs < Repository entries end - def diff(path, rev, rev_to, type) + 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 +76,7 @@ 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) + diff=diff+scm.diff(change_from.path, revision_from, revision_to) end end return diff diff --git a/app/models/repository/darcs.rb b/app/models/repository/darcs.rb index c7c14a397..034c0c145 100644 --- a/app/models/repository/darcs.rb +++ b/app/models/repository/darcs.rb @@ -46,14 +46,14 @@ class Repository::Darcs < Repository entries end - def diff(path, rev, rev_to, type) + 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/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml index 73c1e8c9e..953c7c02a 100644 --- a/app/views/repositories/diff.rhtml +++ b/app/views/repositories/diff.rhtml @@ -12,7 +12,7 @@ <% end %> <% cache(@cache_key) do -%> -<% @diff.each do |table_file| -%> +<% Redmine::UnifiedDiff.new(@diff, @diff_type).each do |table_file| -%>
      <% if @diff_type == 'sbs' -%>
      <%= link_to enumeration.name, :action => 'edit', :id => enumeration %><%= link_to h(enumeration), :action => 'edit', :id => enumeration %> <%= image_tag('true.png') if enumeration.is_default? %> <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> diff --git a/app/views/timelog/_report_criteria.rhtml b/app/views/timelog/_report_criteria.rhtml index 94f3d20f9..c9a1cfb45 100644 --- a/app/views/timelog/_report_criteria.rhtml +++ b/app/views/timelog/_report_criteria.rhtml @@ -3,7 +3,7 @@ <% next if hours_for_value.empty? -%>
      <%= format_criteria_value(criterias[level], value) %><%= h(format_criteria_value(criterias[level], value)) %><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> <%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %> <%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %><%=h truncate(changeset.comments, 50) unless changeset.nil? %>
      diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index bd77ce203..0bacda770 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -82,7 +82,7 @@ module Redmine return nil end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) return nil end @@ -234,166 +234,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@\\]/ - @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/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb index 2225a627c..ff69e3e6b 100644 --- a/lib/redmine/scm/adapters/bazaar_adapter.rb +++ b/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/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index 37920b599..c86b02cb7 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -227,7 +227,7 @@ module Redmine end end - def diff(path, identifier_from, identifier_to=nil, type="inline") + def diff(path, identifier_from, identifier_to=nil) logger.debug " diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}" path_with_project="#{url}#{with_leading_slash(path)}" cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}" @@ -238,7 +238,7 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def cat(path, identifier=nil) diff --git a/lib/redmine/scm/adapters/darcs_adapter.rb b/lib/redmine/scm/adapters/darcs_adapter.rb index a1d1867b1..b1b2a4573 100644 --- a/lib/redmine/scm/adapters/darcs_adapter.rb +++ b/lib/redmine/scm/adapters/darcs_adapter.rb @@ -94,7 +94,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,7 +111,7 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end private diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index 9dfbd17a8..d05b4fb38 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -204,7 +204,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 = nil @@ -220,7 +220,7 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def annotate(path, identifier=nil) diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index be01b7bbc..28b842c27 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -117,7 +117,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 @@ -133,7 +133,7 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def cat(path, identifier=nil) diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index 1cbdce135..7c98eee8b 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -142,7 +142,7 @@ module Redmine end end return nil if $? && $?.exitstatus != 0 - DiffTableList.new diff, type + diff end def cat(path, identifier=nil) diff --git a/lib/redmine/unified_diff.rb b/lib/redmine/unified_diff.rb new file mode 100644 index 000000000..aa8994454 --- /dev/null +++ b/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 From b78b62df8d1fd34321cffd3785121da98f64265b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 16:48:21 +0000 Subject: [PATCH 504/710] SCM browser: ability to download raw unified diffs. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1514 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 34 ++++++++++++++-------- app/views/repositories/diff.rhtml | 5 ++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 5fb2fdd75..ef49cf248 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -142,19 +142,29 @@ class RepositoriesController < ApplicationController 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) + if params[:format] == 'diff' @diff = @repository.diff(@path, @rev, @rev_to) - show_error_not_found unless @diff + 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) diff --git a/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml index 953c7c02a..802082740 100644 --- a/app/views/repositories/diff.rhtml +++ b/app/views/repositories/diff.rhtml @@ -84,6 +84,11 @@ <% end -%> <% end -%> +

      +<%= l(:label_export_to) %> +<%= link_to 'Unified diff', params.merge(:format => 'diff') %> +

      + <% html_title(with_leading_slash(@path), 'Diff') -%> <% content_for :header_tags do %> From e833cab30e19b5c9d17db405660f081923482f11 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 18:10:49 +0000 Subject: [PATCH 505/710] Move diff viewer to a partial. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1515 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/_diff.rhtml | 74 +++++++++++++++++++++++++++++++ app/views/repositories/diff.rhtml | 71 +---------------------------- 2 files changed, 75 insertions(+), 70 deletions(-) create mode 100644 app/views/common/_diff.rhtml diff --git a/app/views/common/_diff.rhtml b/app/views/common/_diff.rhtml new file mode 100644 index 000000000..c849b7ff7 --- /dev/null +++ b/app/views/common/_diff.rhtml @@ -0,0 +1,74 @@ +<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%> +
      +<% if diff_type == 'sbs' -%> +
      + + +<% unless @rev.nil? -%> + + + + +<% end -%> + + +<% 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) -%> + +<% end -%> + + + + + + +<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> +<% end -%> + +
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      <%= table_file[key].nb_line_left %> +
      <%=to_utf8 table_file[key].line_left %>
      +
      <%= table_file[key].nb_line_right %> +
      <%=to_utf8 table_file[key].line_right %>
      +
      + +<% else -%> + + + +<% unless @rev.nil? -%> + + + + + +<% end -%> + + +<% 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) -%> + +<% end -%> + + + + <% if table_file[key].line_left.empty? -%> + + <% else -%> + + <% end -%> + +<% 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 -%> + +
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      <%= table_file[key].nb_line_left %><%= table_file[key].nb_line_right %> +
      <%=to_utf8 table_file[key].line_right %>
      +
      +
      <%=to_utf8 table_file[key].line_left %>
      +
      +<% end -%> + + +<% end -%> diff --git a/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml index 802082740..52a5d6057 100644 --- a/app/views/repositories/diff.rhtml +++ b/app/views/repositories/diff.rhtml @@ -12,76 +12,7 @@ <% end %> <% cache(@cache_key) do -%> -<% Redmine::UnifiedDiff.new(@diff, @diff_type).each do |table_file| -%> -
      -<% if @diff_type == 'sbs' -%> - - - - - - - - - -<% 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) -%> - -<% end -%> - - - - - - -<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> -<% end -%> - -
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      <%= table_file[key].nb_line_left %> -
      <%=to_utf8 table_file[key].line_left %>
      -
      <%= table_file[key].nb_line_right %> -
      <%=to_utf8 table_file[key].line_right %>
      -
      - -<% else -%> - - - - - - - - - - -<% 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) -%> - -<% end -%> - - - - <% if table_file[key].line_left.empty? -%> - - <% else -%> - - <% end -%> - -<% 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 -%> - -
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      <%= table_file[key].nb_line_left %><%= table_file[key].nb_line_right %> -
      <%=to_utf8 table_file[key].line_right %>
      -
      -
      <%=to_utf8 table_file[key].line_left %>
      -
      -<% end -%> - -
      -<% end -%> +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> <% end -%>

      From d77c1d2829f985c418442940c623ec6ec5d5457b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 18:26:39 +0000 Subject: [PATCH 506/710] Unified diff viewer for attached files with .patch or .diff extension (#1403). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1516 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/attachments_controller.rb | 13 ++++++++++--- app/helpers/attachments_helper.rb | 4 ++++ app/helpers/issues_helper.rb | 2 +- app/models/attachment.rb | 4 ++++ app/views/attachments/_links.rhtml | 2 +- app/views/attachments/diff.rhtml | 15 +++++++++++++++ config/routes.rb | 2 ++ 7 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 app/views/attachments/diff.rhtml diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 4e87e5442..cfc15669f 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -19,19 +19,26 @@ class AttachmentsController < ApplicationController layout 'base' before_filter :find_project, :check_project_privacy + def show + if @attachment.is_diff? + @diff = File.new(@attachment.diskfile, "rb").read + render :action => 'diff' + else + download + end + end + def download # images are sent inline send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), :type => @attachment.content_type, :disposition => (@attachment.image? ? 'inline' : 'attachment') - rescue - # in case the disk file was deleted - render_404 end private def find_project @attachment = Attachment.find(params[:id]) + render_404 and return false unless File.readable?(@attachment.diskfile) @project = @attachment.project rescue render_404 diff --git a/app/helpers/attachments_helper.rb b/app/helpers/attachments_helper.rb index 989cd3e66..ebf417bab 100644 --- a/app/helpers/attachments_helper.rb +++ b/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/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 6013f1ec8..915a80b2a 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -91,7 +91,7 @@ module IssuesHelper old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?) if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key) # Link to the attachment if it has not been removed - value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key) + value = link_to(value, :controller => 'attachments', :action => 'show', :id => detail.prop_key) else value = content_tag("i", h(value)) if value end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 45bbd5428..95de4837a 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -88,6 +88,10 @@ class Attachment < ActiveRecord::Base self.filename =~ /\.(jpe?g|gif|png)$/i end + def is_diff? + self.filename =~ /\.(patch|diff)$/i + end + private def sanitize_filename(value) # get only the filename, not the whole path diff --git a/app/views/attachments/_links.rhtml b/app/views/attachments/_links.rhtml index 4d485548b..9e3ac747c 100644 --- a/app/views/attachments/_links.rhtml +++ b/app/views/attachments/_links.rhtml @@ -1,6 +1,6 @@

      <% for attachment in attachments %> -

      <%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' -%> +

      <%= link_to attachment.filename, {:controller => 'attachments', :action => 'show', :id => attachment }, :class => 'icon icon-attachment' -%> <%= h(" - #{attachment.description}") unless attachment.description.blank? %> (<%= number_to_human_size attachment.filesize %>) <% if options[:delete_url] %> diff --git a/app/views/attachments/diff.rhtml b/app/views/attachments/diff.rhtml new file mode 100644 index 000000000..4a9f5c9bd --- /dev/null +++ b/app/views/attachments/diff.rhtml @@ -0,0 +1,15 @@ +

      <%=h @attachment.filename %>

      + +
      +

      <%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <%= @attachment.author %>, <%= format_time(@attachment.created_on) %>

      +

      <%= link_to l(:button_download), {:controller => 'attachments', :action => 'download', :id => @attachment } -%> + (<%= number_to_human_size @attachment.filesize %>)

      + +
      +  +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/config/routes.rb b/config/routes.rb index 0edb71a06..3cb18d1bd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,8 @@ ActionController::Routing::Routes.draw do |map| omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate' end + map.connect 'attachments/:id', :controller => 'attachments', :action => 'show' + # Allow downloading Web Service WSDL as a file with an extension # instead of a file named 'wsdl' map.connect ':controller/service.wsdl', :action => 'wsdl' From 0389c60129636969d3727b752625d95a559deff6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 20:31:36 +0000 Subject: [PATCH 507/710] Fixed: notextile tag has no effect. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1517 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 2 +- test/unit/helpers/application_helper_test.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 3880eb9d3..42eddd8e0 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -1149,7 +1149,7 @@ 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}#{'>' unless $3.blank?}" } diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 4f8ededd1..1e75dbd64 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -153,6 +153,14 @@ class ApplicationHelperTest < HelperTestCase to_test.each { |text, result| assert_equal result, textilizable(text) } end + def test_allowed_html_tags + to_test = { + "
      preformatted text
      " => "
      preformatted text
      ", + "no *textile* formatting" => "no *textile* formatting", + } + to_test.each { |text, result| assert_equal result, textilizable(text) } + end + def test_wiki_links_in_tables to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" => 'Link title' + From e46b56d7fcad573e9531acae9337811e586ed7b8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 8 Jun 2008 22:45:22 +0000 Subject: [PATCH 508/710] "New Project" link on Projects page for admin users (#1082). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1518 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/projects/index.rhtml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/index.rhtml b/app/views/projects/index.rhtml index e12b05979..4c68717f5 100644 --- a/app/views/projects/index.rhtml +++ b/app/views/projects/index.rhtml @@ -1,4 +1,5 @@
      + <%= 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' }%>
      From aa0beecad0fa7cd8c62816b4dc7918f03b214b91 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 9 Jun 2008 17:02:55 +0000 Subject: [PATCH 509/710] Move the file viewer to a partial. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1519 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/_file.rhtml | 11 +++++++++++ app/views/repositories/entry.rhtml | 12 +----------- 2 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 app/views/common/_file.rhtml diff --git a/app/views/common/_file.rhtml b/app/views/common/_file.rhtml new file mode 100644 index 000000000..43f5c6c4b --- /dev/null +++ b/app/views/common/_file.rhtml @@ -0,0 +1,11 @@ +
      + + +<% line_num = 1 %> +<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %> + +<% line_num += 1 %> +<% end %> + +
      <%= line_num %>
      <%= line %>
      +
      diff --git a/app/views/repositories/entry.rhtml b/app/views/repositories/entry.rhtml index 309da76fc..8e1e1992c 100644 --- a/app/views/repositories/entry.rhtml +++ b/app/views/repositories/entry.rhtml @@ -1,16 +1,6 @@

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

      -
      - - -<% line_num = 1 %> -<% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %> - -<% line_num += 1 %> -<% end %> - -
      <%= line_num %>
      <%= line %>
      -
      +<%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %> <% content_for :header_tags do %> <%= stylesheet_link_tag "scm" %> From 80a7486f95a39ad3fc0946fad17fe51cff01cec6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 9 Jun 2008 18:40:59 +0000 Subject: [PATCH 510/710] File viewer for attached text files. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1520 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/attachments_controller.rb | 11 ++-- app/helpers/application_helper.rb | 8 +++ app/helpers/repositories_helper.rb | 7 --- app/models/attachment.rb | 4 ++ app/views/attachments/file.rhtml | 15 +++++ test/fixtures/attachments.yml | 38 ++++++++++- test/fixtures/files/060719210727_archive.zip | Bin 0 -> 157 bytes .../files/060719210727_changeset.diff | 13 ++++ test/fixtures/files/060719210727_source.rb | 10 +++ .../functional/attachments_controller_test.rb | 59 ++++++++++++++++++ test/functional/documents_controller_test.rb | 2 + test/functional/issues_controller_test.rb | 2 + test/integration/issues_test.rb | 18 ++++++ test/test_helper.rb | 6 ++ 14 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 app/views/attachments/file.rhtml create mode 100644 test/fixtures/files/060719210727_archive.zip create mode 100644 test/fixtures/files/060719210727_changeset.diff create mode 100644 test/fixtures/files/060719210727_source.rb create mode 100644 test/functional/attachments_controller_test.rb diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index cfc15669f..9ea9ac48e 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -23,7 +23,10 @@ class AttachmentsController < ApplicationController if @attachment.is_diff? @diff = File.new(@attachment.diskfile, "rb").read render :action => 'diff' - else + elsif @attachment.is_text? + @content = File.new(@attachment.diskfile, "rb").read + render :action => 'file' + elsif download end end @@ -38,9 +41,9 @@ class AttachmentsController < ApplicationController private def find_project @attachment = Attachment.find(params[:id]) - render_404 and return false unless File.readable?(@attachment.diskfile) + #render_404 and return false unless File.readable?(@attachment.diskfile) @project = @attachment.project - rescue - render_404 + #rescue + # render_404 end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 405c5bf44..16904c251 100644 --- a/app/helpers/application_helper.rb +++ b/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 @@ -116,6 +119,11 @@ 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 pagination_links_full(paginator, count=nil, options={}) page_param = options.delete(:page_param) || :page url_param = params.dup diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 10f6e7396..e94ae2e7f 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -15,16 +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' 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 diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 95de4837a..7d6a74ebb 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -88,6 +88,10 @@ 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 diff --git a/app/views/attachments/file.rhtml b/app/views/attachments/file.rhtml new file mode 100644 index 000000000..7988d0aed --- /dev/null +++ b/app/views/attachments/file.rhtml @@ -0,0 +1,15 @@ +

      <%=h @attachment.filename %>

      + +
      +

      <%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <%= @attachment.author %>, <%= format_time(@attachment.created_on) %>

      +

      <%= link_to l(:button_download), {:controller => 'attachments', :action => 'download', :id => @attachment } -%> + (<%= number_to_human_size @attachment.filesize %>)

      + +
      +  +<%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml index 162d44720..a73d6b385 100644 --- a/test/fixtures/attachments.yml +++ b/test/fixtures/attachments.yml @@ -36,4 +36,40 @@ attachments_003: filename: logo.gif description: This is a logo author_id: 2 - \ No newline at end of file +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 diff --git a/test/fixtures/files/060719210727_archive.zip b/test/fixtures/files/060719210727_archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..5467885d4b28eedd531d78c32380ffd3e69eded0 GIT binary patch literal 157 zcmWIWW@Zs#U|`^2Fv$%vtB#e4Q3moW^-3yAv`?PU31e8~(mLr% zmr(1LGjl>(xBO9Rz0@@4&k>OY35A}93<2JZOd<@pjRTs 'issues', :action => 'show', :id => @issue, :project_id => @project ++ redirect_to :controller => 'issues', :action => 'show', :id => @issue + return + end + end diff --git a/test/fixtures/files/060719210727_source.rb b/test/fixtures/files/060719210727_source.rb new file mode 100644 index 000000000..dccb59165 --- /dev/null +++ b/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/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb new file mode 100644 index 000000000..d088c0b0f --- /dev/null +++ b/test/functional/attachments_controller_test.rb @@ -0,0 +1,59 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require '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_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 +end diff --git a/test/functional/documents_controller_test.rb b/test/functional/documents_controller_test.rb index f150a5b7a..7c1f0213a 100644 --- a/test/functional/documents_controller_test.rb +++ b/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/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 582c27d9b..32d2a7f41 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -361,6 +361,8 @@ class IssuesControllerTest < Test::Unit::TestCase end def test_post_edit_with_attachment_only + set_tmp_attachments_directory + # anonymous user post :edit, :id => 1, diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index b9e21719c..1b57ba8f8 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -1,3 +1,20 @@ +# 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 @@ -47,6 +64,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/test/test_helper.rb b/test/test_helper.rb index 61670318a..150b063e8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -57,6 +57,12 @@ class Test::Unit::TestCase def test_uploaded_file(name, mime) ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + "/files/#{name}", mime) end + + # Use a temporary directory for attachment related tests + def set_tmp_attachments_directory + Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments") + Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments" + end end From 19cb6f96f4bf3f10780e292d7b5b59d880b673e8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 9 Jun 2008 18:59:15 +0000 Subject: [PATCH 511/710] Log the user in after registration if account activation is not needed. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1521 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/account_controller.rb | 3 ++- test/integration/account_test.rb | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index b9224c158..3fbbc8912 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -134,8 +134,9 @@ class AccountController < ApplicationController # Automatic activation @user.status = User::STATUS_ACTIVE if @user.save + 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 diff --git a/test/integration/account_test.rb b/test/integration/account_test.rb index e9d665d19..a01a3ba09 100644 --- a/test/integration/account_test.rb +++ b/test/integration/account_test.rb @@ -67,8 +67,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 From efe790a8d3ca2688e5ad639e7166b5f77adb7335 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 9 Jun 2008 19:02:22 +0000 Subject: [PATCH 512/710] Removed inconsistent revision numbers on diff view. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1522 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/_diff.rhtml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/views/common/_diff.rhtml b/app/views/common/_diff.rhtml index c849b7ff7..b95ef116b 100644 --- a/app/views/common/_diff.rhtml +++ b/app/views/common/_diff.rhtml @@ -4,12 +4,6 @@ -<% unless @rev.nil? -%> - - - - -<% end -%> <% prev_line_left, prev_line_right = nil, nil -%> @@ -36,13 +30,6 @@
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      -<% unless @rev.nil? -%> - - - - - -<% end -%> <% prev_line_left, prev_line_right = nil, nil -%> From 53b570398132d41202e75ce9dd3f6bb36dc2981e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 9 Jun 2008 19:15:40 +0000 Subject: [PATCH 513/710] Slight changes to diff view and style. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1523 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/_diff.rhtml | 7 +++++-- public/stylesheets/scm.css | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/views/common/_diff.rhtml b/app/views/common/_diff.rhtml index b95ef116b..0b28101b7 100644 --- a/app/views/common/_diff.rhtml +++ b/app/views/common/_diff.rhtml @@ -9,7 +9,8 @@ <% 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) -%> - + + <% end -%> @@ -35,7 +36,9 @@ <% 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) -%> - + + + <% end -%> diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css index d1495a5ef..d5a879bf1 100644 --- a/public/stylesheets/scm.css +++ b/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 td { border: 1px solid #d7d7d7; height: 0.4em; } +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 */ From cc9b8f78783bd9f9d8cdcc4c6f47e4416af83b3e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 9 Jun 2008 20:05:32 +0000 Subject: [PATCH 514/710] Fixed: SVG::Graph raises an error when using external stylesheet (#1402). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1524 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/SVG/Graph/Graph.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SVG/Graph/Graph.rb b/lib/SVG/Graph/Graph.rb index 403a0202b..a5e1ea732 100644 --- a/lib/SVG/Graph/Graph.rb +++ b/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", { From b42b697ffbbf87f9973a4b9ee70e0d53c4938c38 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 14 Jun 2008 10:51:15 +0000 Subject: [PATCH 515/710] Fixed: unexpected nil when viewing differences on CVS (#1444). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1525 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/repository/cvs.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb index ea75de5d3..5ff7af999 100644 --- a/app/models/repository/cvs.rb +++ b/app/models/repository/cvs.rb @@ -76,7 +76,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) + file_diff = scm.diff(change_from.path, revision_from, revision_to) + diff = diff + file_diff unless file_diff.nil? end end return diff From d2ad4edc8679ce541bc566c69ee404e8b3b34e1f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 14 Jun 2008 11:15:57 +0000 Subject: [PATCH 516/710] Fixed: page has no title when adding a project (#1436). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1526 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/layouts/base.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml index 0b9d31512..62d542b7b 100644 --- a/app/views/layouts/base.rhtml +++ b/app/views/layouts/base.rhtml @@ -36,7 +36,7 @@ <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %> -

      <%= h(@project ? @project.name : Setting.app_title) %>

      +

      <%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %>

      ' diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 1e75dbd64..7fa96d7e2 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -183,6 +183,34 @@ class ApplicationHelperTest < HelperTestCase assert_equal '

      Dashes: ---

      ', 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 = '
      ' + + 'Title' + + 'Subtitle' + + 'Subtitle with red text' + + 'Another title' + + '
      ' + + assert textilizable(raw).include?(expected) + end + def test_blockquote # orig raw text raw = <<-RAW From 597c1e6c09f57e5e3b5f471f774137fbe21b06cd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 11:40:13 +0000 Subject: [PATCH 525/710] Adds links to repository directories in the browser (#1094). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1544 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- .../repositories/_dir_list_content.rhtml | 24 ++++++------------ public/images/bullet_toggle_minus.png | Bin 0 -> 335 bytes public/images/bullet_toggle_plus.png | Bin 0 -> 335 bytes public/stylesheets/application.css | 4 +++ 4 files changed, 12 insertions(+), 16 deletions(-) create mode 100644 public/images/bullet_toggle_minus.png create mode 100644 public/images/bullet_toggle_plus.png diff --git a/app/views/repositories/_dir_list_content.rhtml b/app/views/repositories/_dir_list_content.rhtml index c3bd56e09..c30216533 100644 --- a/app/views/repositories/_dir_list_content.rhtml +++ b/app/views/repositories/_dir_list_content.rhtml @@ -1,26 +1,18 @@ <% @entries.each do |entry| %> <% tr_id = Digest::MD5.hexdigest(entry.path) depth = params[:depth].to_i %> -
      - + diff --git a/public/images/bullet_toggle_minus.png b/public/images/bullet_toggle_minus.png new file mode 100644 index 0000000000000000000000000000000000000000..5ce75938fdeca159bf2917dcafae187bfb8dc52e GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!UWsc&iE~kEVo7Fxo&%v+ZMj zvk)l6S>O>_%)r1c1j3A$?$-SQ3bLd-`Z_W&Z0zU$lgJ9>dj$A|xc>kDf77N-ot>S{ z&CPXnbrlsA#l^*Q=FFKnbLPQ=2TMy!7cE+}X3d(_t5>(RwQb(Kxxc@E&z?P(E?wHU zZ{NlCSLIOf!55+}= z-8ndlni$xf8s<-KZ*LD~P&{y&Q^n$^0MnfWQ5_COz73Kw9E@&iX<}h!Wm%R|!VCD=R-fLC|8pq)2>gTe~DWM4f!lvI6-Y0X`wF|NsBrv}sdkXJ>PB zb6s6sMMXt%aq*lvb7sz*dGO%D($dmJix#a}vu5?`)opEUn>TOn@9*EUXV0Zem-g-3 zw{z#tb?erxSg~T)u3hsEZD#?R$ygHP7tG-B>_!@p Date: Sun, 15 Jun 2008 12:25:48 +0000 Subject: [PATCH 526/710] Doc update for 0.7.2 release. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1546 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- doc/CHANGELOG | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ doc/INSTALL | 17 +++++++++++++---- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 3a39061c6..5fe6bfc4e 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -5,6 +5,59 @@ Copyright (C) 2006-2008 Jean-Philippe Lang http://www.redmine.org/ +== 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) diff --git a/doc/INSTALL b/doc/INSTALL index 872543d95..ab35bd1b8 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -7,7 +7,7 @@ http://www.redmine.org/ == Requirements -* Ruby on Rails 2.0.2 +* Ruby on Rails 2.0.2 (not Rails 2.1) * A database (see compatibility below) Optional: @@ -33,17 +33,26 @@ 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 From 5051566e51a30cbc3776e84774d492b24abb03d6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 14:19:06 +0000 Subject: [PATCH 527/710] Prettier url for changesets (#1443). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1549 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 3cb18d1bd..a77a8833b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,6 +29,7 @@ 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' From 8a54c455b808eb6a5477f57b61b6f50a23d3347e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 14:34:23 +0000 Subject: [PATCH 528/710] Fixes test broken by r1549. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1550 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/functional/repositories_controller_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb index 47455dc55..2892f3bd1 100644 --- a/test/functional/repositories_controller_test.rb +++ b/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 From 7c1c2e1ba22e7fbf64012ab927c49ee62687472e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 14:39:52 +0000 Subject: [PATCH 529/710] Fixes test broken by r1549. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1551 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/unit/mailer_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/mailer_test.rb b/test/unit/mailer_test.rb index 8cf6c1423..402624f5f 100644 --- a/test/unit/mailer_test.rb +++ b/test/unit/mailer_test.rb @@ -36,7 +36,7 @@ class MailerTest < Test::Unit::TestCase # link to a referenced ticket assert mail.body.include?('#2') # link to a changeset - assert mail.body.include?('r2') + assert mail.body.include?('r2') end # test mailer methods for each language From 93b3dba926ef9593c6849b2e84f57de176c595f1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 14:40:05 +0000 Subject: [PATCH 530/710] Makes changes link to entries on the revision view. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1552 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/revision.rhtml | 4 +++- test/functional/repositories_subversion_controller_test.rb | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 2fdf58faf..8d994ef7f 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -46,7 +46,9 @@ <% @changes.each do |change| %> - + + <% end %>
      <%= table_file.file_name %>
      @<%= format_revision @rev %>@<%= format_revision @rev_to %>
      ......
      <%= table_file[key].nb_line_left %>
      ......
      <%= table_file[key].nb_line_left %>
      -<%= 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}, +
      +<% if entry.is_dir? %> + "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), + :condition => "scmEntryClick('#{tr_id}')"%>">  +<% end %> +<%= 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 %> + :class => (entry.is_dir? ? 'icon icon-folder' : 'icon icon-file')%> <%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %> <%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
      <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %>
      +<%= link_to change.path, :action => 'entry', :id => @project, :path => change.relative_path, :rev => @changeset.revision %> +<%= "(#{change.revision})" unless change.revision.blank? %>
      <% if change.action == "M" %> <%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.relative_path), :rev => @changeset.revision %> diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index bc3f261a0..efb824992 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -102,8 +102,13 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase assert_response :success assert_template 'revision' assert_tag :tag => 'tr', - :child => { :tag => 'td', :content => %r{/test/some/path/in/the/repo} }, :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 From ca6e69ec247e10c521f22fba542404720cc2ebff Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 15:47:28 +0000 Subject: [PATCH 531/710] Fixed: view file at given revision with CVS. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1553 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 6 +++--- app/models/repository.rb | 8 ++++++++ app/models/repository/cvs.rb | 11 ++++++++--- lib/redmine/scm/adapters/cvs_adapter.rb | 4 +++- test/functional/repositories_cvs_controller_test.rb | 13 +++++++++++++ .../repositories_subversion_controller_test.rb | 9 +++++++++ 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index ef49cf248..c14449290 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -73,7 +73,7 @@ class RepositoriesController < ApplicationController 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 @@ -96,13 +96,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 diff --git a/app/models/repository.rb b/app/models/repository.rb index 1cf61393f..c7bf0dbf4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -51,10 +51,18 @@ 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 cat(path, identifier=nil) + scm.cat(path, identifier) + end + def diff(path, rev, rev_to) scm.diff(path, rev, rev_to) end diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb index 5ff7af999..dd9d35c8f 100644 --- a/app/models/repository/cvs.rb +++ b/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,6 +53,11 @@ class Repository::Cvs < Repository entries end + def cat(path, identifier=nil) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.cat(path, rev.nil? ? nil : rev.committed_on) + end + def diff(path, rev, rev_to) #convert rev to revision. CVS can't handle changesets here diff=[] diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index 18f26b9c8..089a6b153 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -245,7 +245,9 @@ module Redmine identifier = (identifier) ? identifier : "HEAD" logger.debug " cat path:'#{path}',identifier #{identifier}" path_with_project="#{url}#{with_leading_slash(path)}" - cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{shell_quote 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 diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb index e12bb53ac..254fbc69c 100644 --- a/test/functional/repositories_cvs_controller_test.rb +++ b/test/functional/repositories_cvs_controller_test.rb @@ -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/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb index efb824992..6af5cd5dd 100644 --- a/test/functional/repositories_subversion_controller_test.rb +++ b/test/functional/repositories_subversion_controller_test.rb @@ -78,6 +78,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/ }, From 9737beecc4cdfba8bb8aba84283c95d54e5fa61b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 15:56:47 +0000 Subject: [PATCH 532/710] Adds a field on the repository view to browse at specific revision (#1443). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1554 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/show.rhtml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/repositories/show.rhtml b/app/views/repositories/show.rhtml index 469ac063e..9a73183e8 100644 --- a/app/views/repositories/show.rhtml +++ b/app/views/repositories/show.rhtml @@ -1,11 +1,16 @@
      <%= 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 -%>

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

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

      <%= l(:label_browse) %>

      <%= render :partial => 'dir_list' %> <% end %> From 0223b87612fd536b51c448916dc5fff1284c6b82 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 15 Jun 2008 16:11:07 +0000 Subject: [PATCH 533/710] RepositoriesController cleanup with rescue_from. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1555 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/repositories_controller.rb | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index c14449290..99e2b7203 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -30,6 +30,8 @@ class RepositoriesController < ApplicationController 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 @@ -56,8 +58,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 @@ -68,16 +68,12 @@ class RepositoriesController < ApplicationController show_error_not_found and return unless @entries render :action => 'browse' end - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def changes @entry = @repository.entry(@path, @rev) show_error_not_found and return unless @entry @changesets = @repository.changesets_for_path(@path) - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def revisions @@ -111,15 +107,11 @@ class RepositoriesController < ApplicationController # Prevent empty lines when displaying a file with Windows style eol @content.gsub!("\r\n", "\n") end - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def annotate @annotate = @repository.scm.annotate(@path, @rev) render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def revision @@ -137,8 +129,6 @@ 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 @@ -166,8 +156,6 @@ class RepositoriesController < ApplicationController show_error_not_found unless @diff end end - rescue Redmine::Scm::Adapters::CommandFailed => e - show_error_command_failed(e.message) end def stats @@ -217,8 +205,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) From f4e0c77c8357958e713466c000e135e321600249 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 16 Jun 2008 19:37:09 +0000 Subject: [PATCH 534/710] Prevent unwanted textile link parsing at end of line. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1557 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redcloth.rb | 2 +- test/unit/helpers/application_helper_test.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/redcloth.rb b/lib/redcloth.rb index 42eddd8e0..59d02fbab 100644 --- a/lib/redcloth.rb +++ b/lib/redcloth.rb @@ -786,7 +786,7 @@ class RedCloth < String \s? (?:\(([^)]+?)\)(?="))? # $title ": - (\S+?) # $url + ([\w\/]\S+?) # $url (\/)? # $slash ([^\w\/;]*?) # $post (?=<|\s|$) diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 7fa96d7e2..c6afdda0e 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -59,6 +59,7 @@ class ApplicationHelperTest < HelperTestCase 'This is a "link":http://foo.bar' => 'This is a link', 'This is an intern "link":/foo/bar' => 'This is an intern link', '"link (Link title)":http://foo.bar' => 'link', + "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":

      \n\n\n\t

      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
      \nand another on a second line\":test" } From 3c95f761e610a1d5957ca9f96708958ba80565ab Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 17 Jun 2008 19:10:54 +0000 Subject: [PATCH 535/710] Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value (#1467). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1558 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/enumerations_controller.rb | 21 +++++--- app/models/enumeration.rb | 37 ++++++++----- app/views/enumerations/destroy.rhtml | 12 +++++ app/views/enumerations/list.rhtml | 7 ++- lang/bg.yml | 2 + lang/cs.yml | 2 + lang/da.yml | 2 + lang/de.yml | 2 + lang/en.yml | 2 + lang/es.yml | 2 + lang/fi.yml | 2 + lang/fr.yml | 2 + lang/he.yml | 2 + lang/hu.yml | 2 + lang/it.yml | 2 + lang/ja.yml | 2 + lang/ko.yml | 2 + lang/lt.yml | 2 + lang/nl.yml | 2 + lang/no.yml | 2 + lang/pl.yml | 2 + lang/pt-br.yml | 2 + lang/pt.yml | 2 + lang/ro.yml | 2 + lang/ru.yml | 2 + lang/sr.yml | 2 + lang/sv.yml | 2 + lang/th.yml | 2 + lang/uk.yml | 2 + lang/zh-tw.yml | 2 + lang/zh.yml | 2 + test/functional/enumerations_controller.rb | 61 ++++++++++++++++++++++ test/unit/enumeration_test.rb | 45 ++++++++++++++++ 33 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 app/views/enumerations/destroy.rhtml create mode 100644 test/functional/enumerations_controller.rb create mode 100644 test/unit/enumeration_test.rb diff --git a/app/controllers/enumerations_controller.rb b/app/controllers/enumerations_controller.rb index 7a7f1685a..788fa11b2 100644 --- a/app/controllers/enumerations_controller.rb +++ b/app/controllers/enumerations_controller.rb @@ -75,11 +75,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/app/models/enumeration.rb b/app/models/enumeration.rb index e86768724..d32a0c049 100644 --- a/app/models/enumeration.rb +++ b/app/models/enumeration.rb @@ -24,10 +24,11 @@ class Enumeration < ActiveRecord::Base validates_uniqueness_of :name, :scope => [:opt] validates_length_of :name, :maximum => 30 + # 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) @@ -39,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 @@ -54,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/app/views/enumerations/destroy.rhtml b/app/views/enumerations/destroy.rhtml new file mode 100644 index 000000000..657df8322 --- /dev/null +++ b/app/views/enumerations/destroy.rhtml @@ -0,0 +1,12 @@ +

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

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

      <%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %>

      +

      <%= l(:text_enumeration_category_reassign_to) %> +<%= select_tag 'reassign_to_id', ("" + options_from_collection_for_select(@enumerations, 'id', 'name')) %>

      +
      + +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'enumerations', :action => 'index' %> +<% end %> diff --git a/app/views/enumerations/list.rhtml b/app/views/enumerations/list.rhtml index 1967e5cf9..7f3886b44 100644 --- a/app/views/enumerations/list.rhtml +++ b/app/views/enumerations/list.rhtml @@ -1,7 +1,7 @@

      <%=l(:label_enumerations)%>

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

      <%= l(name) %>

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

      <%= l(params[:label]) %>

      <% enumerations = Enumeration.get_values(option) %> <% if enumerations.any? %> @@ -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) %>
      + <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %> +
      diff --git a/lang/bg.yml b/lang/bg.yml index 5f3548684..48226c79a 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/cs.yml b/lang/cs.yml index 26e0f5131..de460ba3a 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -629,3 +629,5 @@ 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.' diff --git a/lang/da.yml b/lang/da.yml index b2f18db4c..6919cdfcb 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -626,3 +626,5 @@ 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.' diff --git a/lang/de.yml b/lang/de.yml index 1e4040f3d..290acd916 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -625,3 +625,5 @@ 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.' diff --git a/lang/en.yml b/lang/en.yml index b7f217e6c..ffbd10622 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -599,6 +599,8 @@ 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:' default_role_manager: Manager default_role_developper: Developer diff --git a/lang/es.yml b/lang/es.yml index c0ecb997b..b027f48e3 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -627,3 +627,5 @@ 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.' diff --git a/lang/fi.yml b/lang/fi.yml index 6ac4aea7e..e1d188dcc 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/fr.yml b/lang/fr.yml index 4db2f8f3a..eaae51765 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -599,6 +599,8 @@ 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:' default_role_manager: Manager default_role_developper: Développeur diff --git a/lang/he.yml b/lang/he.yml index e23698d32..5f14ee16e 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/hu.yml b/lang/hu.yml index 1df061942..c46d0e26c 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -625,3 +625,5 @@ 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: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' diff --git a/lang/it.yml b/lang/it.yml index 5ab9ad310..8aec9ef0e 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/ja.yml b/lang/ja.yml index c693d68e5..c48579dda 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -625,3 +625,5 @@ 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.' diff --git a/lang/ko.yml b/lang/ko.yml index af1c768af..f2339556f 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/lt.yml b/lang/lt.yml index ff8f6b695..61533b3ab 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -626,3 +626,5 @@ 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.' diff --git a/lang/nl.yml b/lang/nl.yml index 9ee913848..109d444f1 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -625,3 +625,5 @@ 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.' diff --git a/lang/no.yml b/lang/no.yml index aa9d0396b..4e47c75a2 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -625,3 +625,5 @@ 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.' diff --git a/lang/pl.yml b/lang/pl.yml index 75e04ecd4..97378b5b1 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/pt-br.yml b/lang/pt-br.yml index dabab5bfe..67822499e 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/pt.yml b/lang/pt.yml index 2845f390c..a91eea01b 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/ro.yml b/lang/ro.yml index dd51e59be..aafc61916 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -624,3 +624,5 @@ 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.' diff --git a/lang/ru.yml b/lang/ru.yml index c3ec91897..e04377781 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -628,3 +628,5 @@ mail_subject_reminder: "%d назначенных на вас задач в бл text_user_wrote: '%s написал:' 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.' diff --git a/lang/sr.yml b/lang/sr.yml index 92906760b..ec01774bc 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -625,3 +625,5 @@ 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.' diff --git a/lang/sv.yml b/lang/sv.yml index f08e45eb0..e28943d9b 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -625,3 +625,5 @@ 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.' diff --git a/lang/th.yml b/lang/th.yml index 85a1f8c01..6c84dba70 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -627,3 +627,5 @@ 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.' diff --git a/lang/uk.yml b/lang/uk.yml index ae8c5cbc0..365ced150 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -626,3 +626,5 @@ 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.' diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 064004c3a..c7b473548 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -625,3 +625,5 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' diff --git a/lang/zh.yml b/lang/zh.yml index ac650c853..981e8102d 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -625,3 +625,5 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) +text_enumeration_category_reassign_to: 'Reassign them to this value:' +text_enumeration_destroy_question: '%d objects are assigned to this value.' diff --git a/test/functional/enumerations_controller.rb b/test/functional/enumerations_controller.rb new file mode 100644 index 000000000..36ff86720 --- /dev/null +++ b/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/test/unit/enumeration_test.rb b/test/unit/enumeration_test.rb new file mode 100644 index 000000000..9b7bfd174 --- /dev/null +++ b/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 From d991e46f122d084c9465073256e7750b24898d49 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 17 Jun 2008 19:27:03 +0000 Subject: [PATCH 536/710] Fixed: urls containing @ are parsed as email adress by the wiki formatter (#1456). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1559 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/wiki_formatting.rb | 10 +++++++--- test/unit/helpers/application_helper_test.rb | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 88a858b04..6c8eebbbc 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -152,12 +152,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 - %{} + mail = $1 + if text.match(/]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) + mail + else + %{} + end end end end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index c6afdda0e..9504a8c79 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -34,7 +34,8 @@ class ApplicationHelperTest < HelperTestCase 'http://foo.bar/foo.bar#foo.bar.' => 'http://foo.bar/foo.bar#foo.bar.', 'www.foo.bar' => 'www.foo.bar', 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', - 'http://foo.bar/page#125' => 'http://foo.bar/page#125' + 'http://foo.bar/page#125' => 'http://foo.bar/page#125', + 'http://foo@www.bar.com' => 'http://foo@www.bar.com', } to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } end From 062a2d6f5d6ebdc74d5586cb151fd07a219b1b31 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 17 Jun 2008 20:01:15 +0000 Subject: [PATCH 537/710] Adds atom feed on time entries details (#1479). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1560 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 8 ++++++++ app/models/time_entry.rb | 5 +++++ app/views/timelog/details.rhtml | 5 +++++ test/functional/timelog_controller_test.rb | 8 ++++++++ 4 files changed, 26 insertions(+) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 29c2635d6..2b763129e 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -154,6 +154,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, diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index b234a8b21..61b53d1c0 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -24,6 +24,11 @@ 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_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 diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/details.rhtml index f02da9959..f111cbfc0 100644 --- a/app/views/timelog/details.rhtml +++ b/app/views/timelog/details.rhtml @@ -24,8 +24,13 @@

      <%= l(:label_export_to) %> +<%= link_to 'Atom', {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> <%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %>

      <% 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/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index c8b47cf8d..7b4622daa 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -210,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 From 7d143bc3f7394f0af2d95b1e3ce3a6db282516cb Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 17 Jun 2008 20:14:31 +0000 Subject: [PATCH 538/710] Allow dot in firstnames and lastnames (closes #1426). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1561 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 573261b77..5568027d5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -54,7 +54,7 @@ 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 From af734b1b38e9e8534e02f2859f9cc53d6b385de6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 18 Jun 2008 16:49:40 +0000 Subject: [PATCH 539/710] Enable syntax highlight on issues, messages and news (#1473, #1466). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1562 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/show.rhtml | 1 + app/views/messages/show.rhtml | 4 ++++ app/views/news/show.rhtml | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index f1c8a82cd..044637094 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -116,4 +116,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/app/views/messages/show.rhtml b/app/views/messages/show.rhtml index c04c40934..07508fbfd 100644 --- a/app/views/messages/show.rhtml +++ b/app/views/messages/show.rhtml @@ -50,3 +50,7 @@
      <% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index a55b56f0b..cc9fe1fbb 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -55,3 +55,7 @@ <% end %> <% html_title @news.title -%> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> From fda7bcdfb6e6200b14f91baba9e3c3143635a50c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 18 Jun 2008 17:02:39 +0000 Subject: [PATCH 540/710] Mercurial adapter tests fix (#1469). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1563 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/scm/adapters/mercurial_adapter.rb | 2 -- test/unit/mercurial_adapter_test.rb | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 28b842c27..a00ff4c49 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -192,8 +192,6 @@ module Redmine end end - private - def hgversion_from_command_line @hgversion ||= %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] end diff --git a/test/unit/mercurial_adapter_test.rb b/test/unit/mercurial_adapter_test.rb index b4aaaec61..7c1ef85fd 100644 --- a/test/unit/mercurial_adapter_test.rb +++ b/test/unit/mercurial_adapter_test.rb @@ -4,9 +4,9 @@ begin class MercurialAdapterTest < Test::Unit::TestCase - TEMPLATES_DIR = "#{RAILS_ROOT}/extra/mercurial" - TEMPLATE_NAME = "hg-template" - TEMPLATE_EXTENSION = "tmpl" + 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' From 0870f6267fc93400f1dc126fd6d329988a65c7a7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 18 Jun 2008 20:15:51 +0000 Subject: [PATCH 541/710] Add target version to the issue list context menu. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1564 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/context_menu.rhtml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index 04c21ceae..69a5ec02a 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -20,6 +20,19 @@ <% end -%>
    + <% unless @project.versions.empty? -%> +
  • + <%= l(:field_fixed_version) %> +
      + <% @project.versions.sort.each do |v| -%> +
    • <%= context_menu_link v.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[fixed_version_id]' => v, :back_to => @back}, :method => :post, + :selected => (v == @issue.fixed_version), :disabled => !@can[:update] %>
    • + <% end -%> +
    • <%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[fixed_version_id]' => '', :back_to => @back}, :method => :post, + :selected => @issue.fixed_version.nil?, :disabled => !@can[:update] %>
    • +
    +
  • + <% end %>
  • <%= l(:field_assigned_to) %>
      From 9cfa233001b0cca2c978372bfc70729fdbad4e9c Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 19 Jun 2008 18:52:20 +0000 Subject: [PATCH 542/710] Track project and tracker changes in issue history. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1565 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/issues_controller.rb | 1 + app/helpers/issues_helper.rb | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index a86e9b00c..69c8e7932 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -285,6 +285,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? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 915a80b2a..e8f21e9a4 100644 --- a/app/helpers/issues_helper.rb +++ b/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 From ba8a36a39b0aeafd45aeff2a71d5cf689cabaea1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 21 Jun 2008 10:15:07 +0000 Subject: [PATCH 543/710] Removes spaces before colons on issue detail view (#1512). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1566 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/issues/show.rhtml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 044637094..57fbd05c7 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -18,34 +18,34 @@ - - + + - - + + - - + + - + <% if User.current.allowed_to?(:view_time_entries, @project) %> - + <% end %> - + <% if @issue.estimated_hours %> - + <% end %> <% n = 0 for custom_value in @custom_values %> - + <% n = n + 1 if (n > 1) n = 0 %> From bb1edda6e803c6a91fbcb7941fd619eb55be8d32 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 21 Jun 2008 12:32:47 +0000 Subject: [PATCH 544/710] Display issue notes in the activity view (#1509). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1567 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 3 ++- app/models/journal.rb | 2 +- public/images/ticket_note.png | Bin 0 -> 784 bytes public/stylesheets/application.css | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 public/images/ticket_note.png diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index d15c6bc2a..c9a55088b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -258,7 +258,8 @@ class ProjectsController < ApplicationController @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]) + cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + cond.add("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''") @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions) end diff --git a/app/models/journal.rb b/app/models/journal.rb index ac141f68c..67a3eee3b 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -33,7 +33,7 @@ class Journal < ActiveRecord::Base 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}"}} def save diff --git a/public/images/ticket_note.png b/public/images/ticket_note.png new file mode 100644 index 0000000000000000000000000000000000000000..c69db223f73adee7b012c00c27770678fe529ca2 GIT binary patch literal 784 zcmV+r1MmEaP)Z48Qo<`@IKIWA*;*1{Rj3DHVA#j+%xz6HS ziMZf>1K)W7>!(%u=C<+CmFTBS>#thw!hOfq=kfOWxXt2MhPQK?!|SVE_~W$ZpEBvI zJNW6SroPyXt-zlt`1}3c=keaAvwz3lP$ z=kWOE@Au^H_xbz%;OX*kn8NF=Uhu<%{PWZN^U?b4vR;e2^7s1e^ZM!W_~PvL=kNFB z?DTAv!0xtj^wFO1%6Ra{a`47(^7i`g_4@Ag`t9@j>+|{I>ho`x!SK0x{Px@U=bZTE zmG|S1e4xj@(d6;=`tkMp@b&uX@b;|3+>NZx^u>(*_ucsDqWI^X%-idGpvPZ~yI_pF zT8Ow|jk{=)zNWv~@o^}>bpzkc+(cJa1u@Ums_tzCoUq^JM@00DGTPE!Ct=GbNc007WQL_t(|+GAiK z4BFZ|I=i}idi(k(Oau!wHi1BMOH)%zD_EeSvZ|`Orna`OuD$^*keioZkX%?)TvA$A z4i-pAOiB(*Nli;n%gD^i&S78(4hanlkBIb%ii(bjjf-bsaQE=^^0xQ!_45yK@(m1P zV6d{bv9+^zaC9;;b#`%eV_?wL(>E|QGS)CLHPbS;uw-D6S5Q<^R#8<`SJ%+g($-;M z5D^s Date: Sun, 22 Jun 2008 10:45:03 +0000 Subject: [PATCH 545/710] Adds basic support for issue creation via email (#1110). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1568 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/mail_handler.rb | 124 +++++++-- lib/tasks/email.rake | 37 +++ test/fixtures/enabled_modules.yml | 4 + test/fixtures/enumerations.yml | 1 + .../mail_handler/add_note_to_issue.txt | 14 - .../mail_handler/ticket_on_given_project.eml | 41 +++ test/fixtures/mail_handler/ticket_reply.eml | 73 ++++++ .../mail_handler/ticket_with_attachment.eml | 248 ++++++++++++++++++ test/unit/mail_handler_test.rb | 70 +++-- 9 files changed, 555 insertions(+), 57 deletions(-) create mode 100644 lib/tasks/email.rake delete mode 100644 test/fixtures/mail_handler/add_note_to_issue.txt create mode 100644 test/fixtures/mail_handler/ticket_on_given_project.eml create mode 100644 test/fixtures/mail_handler/ticket_reply.eml create mode 100644 test/fixtures/mail_handler/ticket_with_attachment.eml diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 7a1d73244..03e48e170 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -16,25 +16,119 @@ # 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 + 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 + # TODO: make the tracker configurable + tracker = project.trackers.find(:first) # 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) + 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) + identifier = if @@handler_options[:project] + @@handler_options[:project] + elsif email.plain_text_body =~ %r{^Project:[ \t]*(.+)$}i + $1 + end + + target = Project.find_by_identifier(identifier.to_s) + 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) + 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) # add the note - issue.init_journal(user, email.body.chomp) - issue.save + journal = issue.init_journal(user, email.plain_text_body.chomp) + add_attachments(journal) + 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 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/lib/tasks/email.rake b/lib/tasks/email.rake new file mode 100644 index 000000000..dbdbe71fc --- /dev/null +++ b/lib/tasks/email.rake @@ -0,0 +1,37 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +desc <<-END_DESC +Read an email from standard input. + +Available options: + * project => identifier of the project the issue should be added to + +Example: + rake redmine:email:receive project=foo RAILS_ENV="production" +END_DESC + +namespace :redmine do + namespace :email do + task :receive => :environment do + options = {} + options[:project] = ENV['project'] if ENV['project'] + + MailHandler.receive(STDIN.read, options) + end + end +end diff --git a/test/fixtures/enabled_modules.yml b/test/fixtures/enabled_modules.yml index 8d1565534..da63bad5d 100644 --- a/test/fixtures/enabled_modules.yml +++ b/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/test/fixtures/enumerations.yml b/test/fixtures/enumerations.yml index 5e2615476..22a581ab9 100644 --- a/test/fixtures/enumerations.yml +++ b/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 diff --git a/test/fixtures/mail_handler/add_note_to_issue.txt b/test/fixtures/mail_handler/add_note_to_issue.txt deleted file mode 100644 index 4fc6b68fb..000000000 --- a/test/fixtures/mail_handler/add_note_to_issue.txt +++ /dev/null @@ -1,14 +0,0 @@ -x-sender: -x-receiver: -Received: from somenet.foo ([127.0.0.1]) by somenet.foo; - Sun, 25 Feb 2007 09:57:56 GMT -Date: Sun, 25 Feb 2007 10:57:56 +0100 -From: jsmith@somenet.foo -To: redmine@somenet.foo -Message-Id: <45e15df440c00_b90238570a27b@osiris.tmail> -In-Reply-To: <45e15df440c29_b90238570a27b@osiris.tmail> -Subject: [Cookbook - Feature #2] -Mime-Version: 1.0 -Content-Type: text/plain; charset=utf-8 - -Note added by mail diff --git a/test/fixtures/mail_handler/ticket_on_given_project.eml b/test/fixtures/mail_handler/ticket_on_given_project.eml new file mode 100644 index 000000000..07c7f16b2 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_on_given_project.eml @@ -0,0 +1,41 @@ +Return-Path: +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" +To: +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 + diff --git a/test/fixtures/mail_handler/ticket_reply.eml b/test/fixtures/mail_handler/ticket_reply.eml new file mode 100644 index 000000000..99fcfa0d1 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_reply.eml @@ -0,0 +1,73 @@ +Return-Path: +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" +To: +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 + + + + + + +
      This is=20 +reply
      + +------=_NextPart_000_0067_01C8D3CE.711F9CC0-- + diff --git a/test/fixtures/mail_handler/ticket_with_attachment.eml b/test/fixtures/mail_handler/ticket_with_attachment.eml new file mode 100644 index 000000000..c85f6b4a2 --- /dev/null +++ b/test/fixtures/mail_handler/ticket_with_attachment.eml @@ -0,0 +1,248 @@ +Return-Path: +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" +To: +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 + + + + + + + + +
      This is  a new ticket with=20 +attachments
      + +------=_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/test/unit/mail_handler_test.rb b/test/unit/mail_handler_test.rb index d0fc68de8..6bb638f21 100644 --- a/test/unit/mail_handler_test.rb +++ b/test/unit/mail_handler_test.rb @@ -20,38 +20,52 @@ require File.dirname(__FILE__) + '/../test_helper' class MailHandlerTest < Test::Unit::TestCase fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations - FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' - CHARSET = "utf-8" - - include ActionMailer::Quoting - + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' + def setup - ActionMailer::Base.delivery_method = :test - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries = [] - - @expected = TMail::Mail.new - @expected.set_content_type "text", "plain", { "charset" => CHARSET } - @expected.mime_version = '1.0' + ActionMailer::Base.deliveries.clear end - def test_add_note_to_issue - raw = read_fixture("add_note_to_issue.txt").join - MailHandler.receive(raw) - - issue = Issue.find(2) - journal = issue.journals.find(:first, :order => "created_on DESC") - assert journal - assert_equal User.find_by_mail("jsmith@somenet.foo"), journal.user - assert_equal "Note added by mail", journal.notes + 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 + + def test_add_issue_with_attachment_to_specific_project + issue = submit_email('ticket_with_attachment.eml', :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_equal 'This is reply', journal.notes 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 From 3d8d4fa0d6cc945f18b90c139069e8c806e72668 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 12:27:00 +0000 Subject: [PATCH 546/710] Adds a rake task (redmine:email:receive_imap) to read emails from an IMAP server (#1110). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1569 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/imap.rb | 43 +++++++++++++++++++++++++++++++++++++++++++ lib/tasks/email.rake | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 lib/redmine/imap.rb diff --git a/lib/redmine/imap.rb b/lib/redmine/imap.rb new file mode 100644 index 000000000..d09832c2e --- /dev/null +++ b/lib/redmine/imap.rb @@ -0,0 +1,43 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require '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(['ALL']).each do |message_id| + puts "Receiving message #{message_id}" + msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822'] + if MailHandler.receive(msg, options) + imap.store(message_id, "+FLAGS", [:Deleted]) + end + end + imap.expunge + end + end + end +end diff --git a/lib/tasks/email.rake b/lib/tasks/email.rake index dbdbe71fc..01407c36d 100644 --- a/lib/tasks/email.rake +++ b/lib/tasks/email.rake @@ -15,7 +15,10 @@ # 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 +namespace :redmine do + namespace :email do + + desc <<-END_DESC Read an email from standard input. Available options: @@ -25,13 +28,42 @@ Example: rake redmine:email:receive project=foo RAILS_ENV="production" END_DESC -namespace :redmine do - namespace :email do task :receive => :environment do options = {} options[:project] = ENV['project'] if ENV['project'] MailHandler.receive(STDIN.read, options) end + + desc <<-END_DESC +Read emails from an IMAP server. + +Available IMAP options: + * host => IMAP server host (default: 127.0.0.1) + * port => IMAP server port (default: 143) + * ssl => Use SSL? (default: false) + * username => IMAP account + * password => IMAP password + * folder => IMAP folder to read (default: INBOX) +Other options: + * project => identifier of the project the issue should be added to + +Example: + rake redmine:email:receive_iamp host=imap.foo.bar username=redmine@somenet.foo password=xxx project=foo RAILS_ENV="production" +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 = {} + options[:project] = ENV['project'] if ENV['project'] + + Redmine::IMAP.check(imap_options, options) + end end end From 268165a013ebf6734aa377cf62ce0a4f8b894921 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 12:37:24 +0000 Subject: [PATCH 547/710] Fixes reply attachments handling. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1570 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/mail_handler.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 03e48e170..7b85077e1 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -103,7 +103,7 @@ class MailHandler < ActionMailer::Base raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) # add the note journal = issue.init_journal(user, email.plain_text_body.chomp) - add_attachments(journal) + add_attachments(issue) 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') From 8b33565b3e7bbca7ec2ef4aeedba4eb20cb66bdc Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 12:56:10 +0000 Subject: [PATCH 548/710] IMAP: Mark emails as Seen. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1571 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/imap.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/redmine/imap.rb b/lib/redmine/imap.rb index d09832c2e..aceb75cb2 100644 --- a/lib/redmine/imap.rb +++ b/lib/redmine/imap.rb @@ -30,14 +30,22 @@ module Redmine imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil? imap.select(folder) imap.search(['ALL']).each do |message_id| - puts "Receiving message #{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", [:Deleted]) + 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 From 8b6dd3fdcd5c5d0d52cab2d22623971c1adb1944 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 13:14:45 +0000 Subject: [PATCH 549/710] IMAP: fetch unseen messages only. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1572 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/imap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redmine/imap.rb b/lib/redmine/imap.rb index aceb75cb2..a6cd958cd 100644 --- a/lib/redmine/imap.rb +++ b/lib/redmine/imap.rb @@ -29,7 +29,7 @@ module Redmine 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(['ALL']).each do |message_id| + 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) From c107fee54e453cf35081ee5046b878ac719333b0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 14:35:56 +0000 Subject: [PATCH 550/710] Use new image instead of expand.png git-svn-id: http://redmine.rubyforge.org/svn/trunk@1573 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/queries/_filters.rhtml | 2 +- public/images/expand.png | Bin 266 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 public/images/expand.png diff --git a/app/views/queries/_filters.rhtml b/app/views/queries/_filters.rhtml index ec9d4fef6..c9d612364 100644 --- a/app/views/queries/_filters.rhtml +++ b/app/views/queries/_filters.rhtml @@ -78,7 +78,7 @@ function toggle_multi_select(field) { - <%= 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/public/images/expand.png b/public/images/expand.png deleted file mode 100644 index 3e3aaa441ffe1779acb1434f30f6d3560bd485d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1SIo6Pjm-T>?NMQuI$%%ICU%k`|lb-N-@9}(-9=Yl1*8@9T&e%k(&3<@N>4d`(m4dx@7xJ@T5H#NO zXRnQ>E7R0%mG5LP@D(}D^=e(%EA`^ah6Bf^#mHG5EfSc~?-rF<_+Mr6PMfGwO_pOF z4}aVZ)v`UhLEn$5_2t~$W3A1TzpveDD#~>2n~h*scl}q^d#8N9$UEmZ0UgQU>FVdQ I&MBb@0GV%N-v9sr From b14aa23c8cf2cb35b863cbd41fc6c0d1286161aa Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 14:40:45 +0000 Subject: [PATCH 551/710] Revision view: do not display links for deleted files. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1574 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/revision.rhtml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 8d994ef7f..527ae6dcd 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -47,7 +47,11 @@ <% @changes.each do |change| %>
      -<% n = 0 -for custom_value in @custom_values %> - +<% n = 0 -%> +<% @issue.custom_values.each do |value| -%> + <% n = n + 1 if (n > 1) n = 0 %> diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index 774e73977..11f7e3933 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -17,8 +17,8 @@

      <%= f.check_box :is_public %>

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

      <%= custom_field_tag_with_label @custom_value %>

      +<% @project.custom_field_values.each do |value| %> +

      <%= custom_field_tag_with_label :project, value %>

      <% end %> @@ -34,15 +34,15 @@ <% end %> -<% unless @custom_fields.empty? %> +<% unless @issue_custom_fields.empty? %>
      <%=l(:label_custom_field_plural)%> -<% for custom_field in @custom_fields %> +<% @issue_custom_fields.each do |custom_field| %> <% end %> -<%= hidden_field_tag 'project[custom_field_ids][]', '' %> +<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
      <% end %> diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index 62b911937..6c82c80b4 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -10,7 +10,7 @@ <% if @project.parent %>
    • <%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %>
    • <% end %> - <% for custom_value in @custom_values %> + <% @project.custom_values.each do |custom_value| %> <% if !custom_value.value.empty? %>
    • <%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %>
    • <% end %> diff --git a/app/views/users/_form.rhtml b/app/views/users/_form.rhtml index 09a798468..799ebde47 100644 --- a/app/views/users/_form.rhtml +++ b/app/views/users/_form.rhtml @@ -8,9 +8,9 @@

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

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

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

      <%= custom_field_tag_with_label @custom_value %>

      -<% end if @custom_values%> +<% @user.custom_field_values.each do |value| %> +

      <%= custom_field_tag_with_label :user, value %>

      +<% end %>

      <%= f.check_box :admin, :disabled => (@user == User.current) %>

      diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index 9d88bc6fb..1005edae4 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -7,7 +7,10 @@ custom_fields_001: 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 @@ -33,7 +36,11 @@ custom_fields_003: 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/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 32d2a7f41..ac1f4f834 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -170,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 @@ -203,8 +203,8 @@ class IssuesControllerTest < Test::Unit::TestCase :subject => 'This is the test_new issue', :description => 'This is the description', :priority_id => 5, - :estimated_hours => ''}, - :custom_fields => {'2' => 'Value for field 2'} + :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') @@ -226,6 +226,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 @@ -280,18 +324,28 @@ class IssuesControllerTest < Test::Unit::TestCase 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 @@ -299,6 +353,29 @@ 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 diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 5b7c29b18..88c0319ba 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -83,7 +83,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 diff --git a/test/integration/admin_test.rb b/test/integration/admin_test.rb index a424247cc..6e385873e 100644 --- a/test/integration/admin_test.rb +++ b/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/test/unit/issue_test.rb b/test/unit/issue_test.rb index 999c4480d..0d98f89d2 100644 --- a/test/unit/issue_test.rb +++ b/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 diff --git a/vendor/plugins/acts_as_customizable/init.rb b/vendor/plugins/acts_as_customizable/init.rb new file mode 100644 index 000000000..9036aa579 --- /dev/null +++ b/vendor/plugins/acts_as_customizable/init.rb @@ -0,0 +1,2 @@ +require File.dirname(__FILE__) + '/lib/acts_as_customizable' +ActiveRecord::Base.send(:include, Redmine::Acts::Customizable) diff --git a/vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb b/vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb new file mode 100644 index 000000000..05857d0a0 --- /dev/null +++ b/vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb @@ -0,0 +1,82 @@ +# redMine - project management software +# Copyright (C) 2006-2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module Redmine + module Acts + module Customizable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_customizable(options = {}) + return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods) + cattr_accessor :customizable_options + self.customizable_options = options + has_many :custom_values, :dependent => :delete_all, :as => :customized + before_validation_on_create { |customized| customized.custom_field_values } + # Trigger validation only if custom values were changed + validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? } + send :include, Redmine::Acts::Customizable::InstanceMethods + # Save custom values when saving the customized object + after_save :save_custom_field_values + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + def available_custom_fields + CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'", + :order => 'position') + end + + def custom_field_values=(values) + @custom_field_values_changed = true + values = values.stringify_keys + custom_field_values.each do |custom_value| + custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s) + end if values.is_a?(Hash) + end + + def custom_field_values + @custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) } + end + + def custom_field_values_changed? + @custom_field_values_changed == true + end + + def custom_value_for(c) + field_id = (c.is_a?(CustomField) ? c.id : c.to_i) + custom_values.detect {|v| v.custom_field_id == field_id } + end + + def save_custom_field_values + custom_field_values.each(&:save) + @custom_field_values_changed = false + @custom_field_values = nil + end + + module ClassMethods + end + end + end + end +end From bd3191f80da433322fc9d2ec7b61ae6b89e216fa Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 27 Jun 2008 20:18:23 +0000 Subject: [PATCH 570/710] New rake task name in comments for creating sessions migration (#1520). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1593 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- config/environment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environment.rb b/config/environment.rb index 7878eca47..f09c1009a 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -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 From 75a5dbd01dd4c57b96dd1fa322bc4d9ec9f865d1 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 28 Jun 2008 11:34:53 +0000 Subject: [PATCH 571/710] Upgraded to Prototype 1.6.0.1. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1594 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/javascripts/context_menu.js | 8 +- public/javascripts/controls.js | 848 +++--- public/javascripts/dragdrop.js | 146 +- public/javascripts/effects.js | 760 ++--- public/javascripts/prototype.js | 4540 +++++++++++++++++++--------- 5 files changed, 4102 insertions(+), 2200 deletions(-) diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js index 3e2d571fa..dfd72b1fa 100644 --- a/public/javascripts/context_menu.js +++ b/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.findElement(e, 'a') != document && Event.findElement(e, 'a') != undefined) { 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.findElement(e, 'a') != document && Event.findElement(e, 'a') != undefined ) { 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'); diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js index 8c273f874..5aaf0bb2b 100644 --- a/public/javascripts/controls.js +++ b/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, '
      <%=l(:field_status)%> :<%= @issue.status.name %><%=l(:field_start_date)%> :<%= format_date(@issue.start_date) %><%=l(:field_status)%>:<%= @issue.status.name %><%=l(:field_start_date)%>:<%= format_date(@issue.start_date) %>
      <%=l(:field_priority)%> :<%= @issue.priority.name %><%=l(:field_due_date)%> :<%= format_date(@issue.due_date) %><%=l(:field_priority)%>:<%= @issue.priority.name %><%=l(:field_due_date)%>:<%= format_date(@issue.due_date) %>
      <%=l(:field_assigned_to)%> :<%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %><%=l(:field_done_ratio)%> :<%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %><%=l(:field_assigned_to)%>:<%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %><%=l(:field_done_ratio)%>:<%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %>
      <%=l(:field_category)%> :<%=h @issue.category ? @issue.category.name : "-" %><%=l(:field_category)%>:<%=h @issue.category ? @issue.category.name : "-" %><%=l(:label_spent_time)%> :<%=l(:label_spent_time)%>: <%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %>
      <%=l(:field_fixed_version)%> :<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %><%=l(:field_fixed_version)%>:<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %><%=l(:field_estimated_hours)%> :<%= lwr(:label_f_hour, @issue.estimated_hours) %><%=l(:field_estimated_hours)%>:<%= lwr(:label_f_hour, @issue.estimated_hours) %>
      <%= custom_value.custom_field.name %> :<%= simple_format(h(show_value(custom_value))) %><%= custom_value.custom_field.name %>:<%= simple_format(h(show_value(custom_value))) %>
      -<%= link_to change.path, :action => 'entry', :id => @project, :path => change.relative_path, :rev => @changeset.revision %> +<% if change.action == "D" -%> + <%= change.path -%> +<% else -%> + <%= link_to change.path, :action => 'entry', :id => @project, :path => change.relative_path, :rev => @changeset.revision -%> +<% end -%> <%= "(#{change.revision})" unless change.revision.blank? %>
      <% if change.action == "M" %> From b025b63111089a5048f4fef5af30c98c0de26b8a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 15:20:59 +0000 Subject: [PATCH 552/710] Hide 'Target version' filter if no version is defined. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1575 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/query.rb b/app/models/query.rb index c19bb8d7e..77893bcf8 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -168,7 +168,9 @@ class Query < ActiveRecord::Base if project # project specific filters @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } - @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } + unless @project.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 From 0d5b03bab7338da4a5c456f21d18dd09308e0d8a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sun, 22 Jun 2008 15:35:11 +0000 Subject: [PATCH 553/710] Add filters on cross-project issue list for custom fields marked as 'For all projects'. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1576 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/query.rb | 49 ++++++++++++++++++++------------- test/fixtures/custom_fields.yml | 1 + test/unit/query_test.rb | 6 ++++ 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/app/models/query.rb b/app/models/query.rb index 77893bcf8..4c72e23f2 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -166,31 +166,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] } } + # 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_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 @@ -368,4 +357,26 @@ class Query < ActiveRecord::Base (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/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml index 3a9e79a29..9d88bc6fb 100644 --- a/test/fixtures/custom_fields.yml +++ b/test/fixtures/custom_fields.yml @@ -30,6 +30,7 @@ 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 diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 147bfbea3..3f77cd835 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -20,6 +20,12 @@ 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 test_query_with_multiple_custom_fields query = Query.find(1) assert query.valid? From 28c094f50e3c64774e04581a77cb7b1c3de231e0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 23 Jun 2008 16:51:13 +0000 Subject: [PATCH 554/710] Turn ftp urls into links (#1514). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1577 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lib/redmine/wiki_formatting.rb | 1 + test/unit/helpers/application_helper_test.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 6c8eebbbc..952f636d3 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -129,6 +129,7 @@ module Redmine ) ( (?:https?://)| # protocol spec, or + (?:ftp://)| (?:www\.) # www.* ) ( diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 9504a8c79..45eb2df90 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -36,6 +36,7 @@ class ApplicationHelperTest < HelperTestCase 'http://foo.bar/page?p=1&t=z&s=' => 'http://foo.bar/page?p=1&t=z&s=', 'http://foo.bar/page#125' => 'http://foo.bar/page#125', 'http://foo@www.bar.com' => 'http://foo@www.bar.com', + 'ftp://foo.bar' => 'ftp://foo.bar', } to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } end From dc57b06b6c92bbb463a899c1546d7976594aea6f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 23 Jun 2008 17:01:21 +0000 Subject: [PATCH 555/710] Adds a class ('me') to events of the activity view created by current user. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1578 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/projects/activity.rhtml | 3 ++- public/stylesheets/application.css | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index 1a3fd9ff2..c08cd06f9 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -6,7 +6,8 @@

      <%= format_activity_day(day) %>

      <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> -
      <%= format_time(e.event_datetime, false) %> +
      + <%= format_time(e.event_datetime, false) %> <%= 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 %>
      <%= format_activity_description(e.event_description) %> diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 48a8193e2..15e382d80 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -181,6 +181,7 @@ div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} div#activity dl, #search-results { margin-left: 2em; } div#activity dd { margin-bottom: 1em; padding-left: 18px; } div#activity dt, #search-results dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt.me .time { border-bottom: 1px solid #999; } div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description, #search-results dd .description { font-style: italic; } div#activity span.project:after, #search-results span.project:after { content: " -"; } From 810ec643f8cd1ccd492d3691293a1fcfe682f8d6 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 23 Jun 2008 17:06:58 +0000 Subject: [PATCH 556/710] Strip pre/code tags content from activity view events. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1579 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/projects_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 49a2e5721..5d51e7075 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -30,7 +30,7 @@ module ProjectsHelper end def format_activity_description(text) - h(truncate(text, 250)) + h(truncate(text, 250).gsub(%r{<(pre|code)>.*$}m, '...')) end def project_settings_tabs From 3121e0b5d0d7f985ca8ff329804cb0b5c83cb915 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Mon, 23 Jun 2008 17:12:12 +0000 Subject: [PATCH 557/710] Smaller font size for activity events and search results descriptions. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1580 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- public/stylesheets/application.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 15e382d80..c10dfbef1 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -179,8 +179,8 @@ div#issue-changesets .changeset { border-bottom: 1px solid #ddd; } div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} div#activity dl, #search-results { margin-left: 2em; } -div#activity dd { margin-bottom: 1em; padding-left: 18px; } -div#activity dt, #search-results dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } +div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } div#activity dt.me .time { border-bottom: 1px solid #999; } div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description, #search-results dd .description { font-style: italic; } From 8474d05e9994d947d862691751c6b5fa110f6754 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 24 Jun 2008 16:49:51 +0000 Subject: [PATCH 558/710] Fixes test broken by r1578. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1581 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/functional/projects_controller_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index d4ce97dc2..5b7c29b18 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -154,7 +154,7 @@ class ProjectsControllerTest < Test::Unit::TestCase :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})/, } @@ -170,7 +170,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}/, } From c97a5efde91163238e0d4df3de8c08a895c925f4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 24 Jun 2008 16:52:41 +0000 Subject: [PATCH 559/710] Fixed: private method 'gsub' called for nil:NilClass on activity (#1519). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1582 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/projects_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5d51e7075..912482f1c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -30,7 +30,7 @@ module ProjectsHelper end def format_activity_description(text) - h(truncate(text, 250).gsub(%r{<(pre|code)>.*$}m, '...')) + h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...')) end def project_settings_tabs From 4d6f50d3febff0e9cbbab158d39b2d8c0375c6a7 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Tue, 24 Jun 2008 18:39:49 +0000 Subject: [PATCH 560/710] Encoding set to utf8 in example database.yml (#1506). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1583 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- config/database.yml.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/database.yml.example b/config/database.yml.example index f72844a07..1dc678131 100644 --- a/config/database.yml.example +++ b/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 From 25bba80c9eb112b8ded40d2baf9f202c79d4e2ad Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Wed, 25 Jun 2008 19:25:28 +0000 Subject: [PATCH 561/710] Adds a simple API and a standalone script that can be used to forward emails from a local or remote email server to Redmine (#1110). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1584 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/mail_handler_controller.rb | 44 +++++++++++ app/helpers/mail_handler_helper.rb | 19 +++++ app/helpers/settings_helper.rb | 1 + app/models/mail_handler.rb | 2 +- app/views/settings/_mail_handler.rhtml | 18 +++++ config/settings.yml | 4 + extra/mail_handler/rdm-mailhandler.rb | 79 +++++++++++++++++++ lang/bg.yml | 4 + lang/cs.yml | 4 + lang/da.yml | 4 + lang/de.yml | 4 + lang/en.yml | 4 + lang/es.yml | 4 + lang/fi.yml | 4 + lang/fr.yml | 4 + lang/he.yml | 4 + lang/hu.yml | 4 + lang/it.yml | 4 + lang/ja.yml | 4 + lang/ko.yml | 4 + lang/lt.yml | 4 + lang/nl.yml | 4 + lang/no.yml | 4 + lang/pl.yml | 4 + lang/pt-br.yml | 4 + lang/pt.yml | 4 + lang/ro.yml | 4 + lang/ru.yml | 4 + lang/sr.yml | 4 + lang/sv.yml | 4 + lang/th.yml | 4 + lang/uk.yml | 4 + lang/zh-tw.yml | 4 + lang/zh.yml | 4 + public/javascripts/application.js | 9 +++ .../mail_handler_controller_test.rb | 53 +++++++++++++ 36 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 app/controllers/mail_handler_controller.rb create mode 100644 app/helpers/mail_handler_helper.rb create mode 100644 app/views/settings/_mail_handler.rhtml create mode 100644 extra/mail_handler/rdm-mailhandler.rb create mode 100644 test/functional/mail_handler_controller_test.rb diff --git a/app/controllers/mail_handler_controller.rb b/app/controllers/mail_handler_controller.rb new file mode 100644 index 000000000..8bcfce630 --- /dev/null +++ b/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/app/helpers/mail_handler_helper.rb b/app/helpers/mail_handler_helper.rb new file mode 100644 index 000000000..a29a6dd5a --- /dev/null +++ b/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/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f4ec5a7a7..d88269f7d 100644 --- a/app/helpers/settings_helper.rb +++ b/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/app/models/mail_handler.rb b/app/models/mail_handler.rb index 7b85077e1..124f7db74 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -84,7 +84,7 @@ class MailHandler < ActionMailer::Base # TODO: other ways to specify project: # * parse the email To field # * specific project (eg. Setting.mail_handler_target_project) - identifier = if @@handler_options[:project] + identifier = if !@@handler_options[:project].blank? @@handler_options[:project] elsif email.plain_text_body =~ %r{^Project:[ \t]*(.+)$}i $1 diff --git a/app/views/settings/_mail_handler.rhtml b/app/views/settings/_mail_handler.rhtml new file mode 100644 index 000000000..830b1ba4a --- /dev/null +++ b/app/views/settings/_mail_handler.rhtml @@ -0,0 +1,18 @@ +<% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %> + +
      +

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

      + +

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

      +
      + +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/config/settings.yml b/config/settings.yml index 616665f23..78a366f35 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -101,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: diff --git a/extra/mail_handler/rdm-mailhandler.rb b/extra/mail_handler/rdm-mailhandler.rb new file mode 100644 index 000000000..585afefef --- /dev/null +++ b/extra/mail_handler/rdm-mailhandler.rb @@ -0,0 +1,79 @@ +#!/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' + +class RedmineMailHandler + VERSION = '0.1' + + attr_accessor :verbose, :project, :url, :key + + def initialize + 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 ] + ) + + 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' + self.project = arg.dup + end + end + + usage if url.nil? + end + + def submit(email) + uri = url.gsub(%r{/*$}, '') + '/mail_handler' + debug "Posting to #{uri}..." + data = { 'key' => key, 'project' => project, 'email' => email } + response = Net::HTTP.post_form(URI.parse(uri), data) + debug "Response received: #{response.code}" + response.code == 201 ? 0 : 1 + end + + private + + def usage + puts "Usage: rdm-mailhandler [options] --url= --key=" + puts "Reads an email from standard input and forward it to a Redmine server" + puts + puts "Options:" + puts " --help show this help" + puts " --verbose show extra information" + puts " --project identifier of the target project" + puts + puts "Examples:" + puts " rdm-mailhandler --url http://redmine.domain.foo --key secret" + puts " rdm-mailhandler --url https://redmine.domain.foo --key secret --project foo" + exit + end + + def debug(msg) + puts msg if verbose + end +end + +handler = RedmineMailHandler.new +handler.submit(STDIN.read) diff --git a/lang/bg.yml b/lang/bg.yml index 48226c79a..1339d9a83 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/cs.yml b/lang/cs.yml index de460ba3a..00d0642de 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -631,3 +631,7 @@ 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 diff --git a/lang/da.yml b/lang/da.yml index 6919cdfcb..b8546ab20 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -628,3 +628,7 @@ 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 diff --git a/lang/de.yml b/lang/de.yml index 290acd916..6bc7919f9 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -627,3 +627,7 @@ 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 diff --git a/lang/en.yml b/lang/en.yml index ffbd10622..7ab73f051 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -214,6 +214,8 @@ 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 @@ -515,6 +517,8 @@ label_preferences: Preferences label_chronological_order: In chronological order label_reverse_chronological_order: In reverse chronological order label_planning: Planning +label_incoming_emails: Incoming emails +label_generate_key: Generate a key button_login: Login button_submit: Submit diff --git a/lang/es.yml b/lang/es.yml index b027f48e3..2615d8f61 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -629,3 +629,7 @@ 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 diff --git a/lang/fi.yml b/lang/fi.yml index e1d188dcc..51b0c04a0 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/fr.yml b/lang/fr.yml index eaae51765..1fd86dd07 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -215,6 +215,8 @@ 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é @@ -515,6 +517,8 @@ 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é button_login: Connexion button_submit: Soumettre diff --git a/lang/he.yml b/lang/he.yml index 5f14ee16e..7455fc395 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/hu.yml b/lang/hu.yml index c46d0e26c..ba904ed45 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -627,3 +627,7 @@ label_duplicated_by: duplikálta setting_enabled_scm: Forráskódkezelő (SCM) engedélyezése 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 diff --git a/lang/it.yml b/lang/it.yml index 8aec9ef0e..867c73f9a 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/ja.yml b/lang/ja.yml index c48579dda..aa4320c3a 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -627,3 +627,7 @@ 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 diff --git a/lang/ko.yml b/lang/ko.yml index f2339556f..f945537a9 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/lt.yml b/lang/lt.yml index 61533b3ab..355f473c8 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -628,3 +628,7 @@ 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 diff --git a/lang/nl.yml b/lang/nl.yml index 109d444f1..89b8a5736 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -627,3 +627,7 @@ 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 diff --git a/lang/no.yml b/lang/no.yml index 4e47c75a2..5e4bdcca3 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -627,3 +627,7 @@ 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 diff --git a/lang/pl.yml b/lang/pl.yml index 97378b5b1..0b0f87b6f 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 67822499e..e2036afde 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/pt.yml b/lang/pt.yml index a91eea01b..543743cab 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/ro.yml b/lang/ro.yml index aafc61916..16c2ea104 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -626,3 +626,7 @@ 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 diff --git a/lang/ru.yml b/lang/ru.yml index e04377781..f68505be5 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -630,3 +630,7 @@ 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 diff --git a/lang/sr.yml b/lang/sr.yml index ec01774bc..83bfdf43c 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -627,3 +627,7 @@ 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 diff --git a/lang/sv.yml b/lang/sv.yml index e28943d9b..375970f3e 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -627,3 +627,7 @@ 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 diff --git a/lang/th.yml b/lang/th.yml index 6c84dba70..acbf146cf 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -629,3 +629,7 @@ 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 diff --git a/lang/uk.yml b/lang/uk.yml index 365ced150..d70916b1e 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -628,3 +628,7 @@ 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 diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index c7b473548..462633575 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -627,3 +627,7 @@ enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) 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 diff --git a/lang/zh.yml b/lang/zh.yml index 981e8102d..9f81d42fa 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -627,3 +627,7 @@ enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) 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 diff --git a/public/javascripts/application.js b/public/javascripts/application.js index a8b6c0e46..4e5b67e55 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -107,6 +107,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/test/functional/mail_handler_controller_test.rb b/test/functional/mail_handler_controller_test.rb new file mode 100644 index 000000000..6c5af23f0 --- /dev/null +++ b/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 From 31e3d180c01e701eb71ded5dd842fcc5e743365a Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 14:54:45 +0000 Subject: [PATCH 562/710] Add project name to cross-project Atom feeds (#1527). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1585 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/feed.atom.rxml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/common/feed.atom.rxml b/app/views/common/feed.atom.rxml index 8ceffc91c..c1b88a28e 100644 --- a/app/views/common/feed.atom.rxml +++ b/app/views/common/feed.atom.rxml @@ -10,7 +10,11 @@ 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_single_line(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 From ad7816cea4361341a161afae00078ceb0f5fbb35 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 14:59:03 +0000 Subject: [PATCH 563/710] Tests: create tmp/test if it doesn't exist. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1586 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- test/test_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_helper.rb b/test/test_helper.rb index 150b063e8..1340f9c35 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -60,6 +60,7 @@ class Test::Unit::TestCase # 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 From b3939fb134c38c310e0f031ad445035db70ca2a0 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 17:02:09 +0000 Subject: [PATCH 564/710] Link to view specific file on revision view fails with Subversion repository subdirectory (#1525). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1587 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/repositories/revision.rhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml index 527ae6dcd..a43aa4536 100644 --- a/app/views/repositories/revision.rhtml +++ b/app/views/repositories/revision.rhtml @@ -50,7 +50,7 @@ <% if change.action == "D" -%> <%= change.path -%> <% else -%> - <%= link_to change.path, :action => 'entry', :id => @project, :path => change.relative_path, :rev => @changeset.revision -%> + <%= link_to change.path, :action => 'entry', :id => @project, :path => without_leading_slash(change.relative_path), :rev => @changeset.revision -%> <% end -%> <%= "(#{change.revision})" unless change.revision.blank? %>
      From 0fd9b102be7dec664c8df1ddb802fac9ba8cea79 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 19:04:58 +0000 Subject: [PATCH 565/710] Adds anchor to atom feed messages links. git-svn-id: http://redmine.rubyforge.org/svn/trunk@1588 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/models/message.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/message.rb b/app/models/message.rb index f57b90985..888bffcc3 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -29,7 +29,8 @@ class Message < ActiveRecord::Base 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}"})} attr_protected :locked, :sticky validates_presence_of :subject, :content From f79f19f84c3b30b47280592a4ab8eb06dafb168f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 19:11:07 +0000 Subject: [PATCH 566/710] Fixed: timelog redirects inappropriately when :back_url is blank (#1524). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1589 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/timelog_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 2b763129e..2e257c5aa 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -180,7 +180,7 @@ 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') From 864ac367e85160b523b2f05522cbf0d3f2d65c0d Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 19:29:05 +0000 Subject: [PATCH 567/710] =?UTF-8?q?Translations=20updates:=20*=20Simplifie?= =?UTF-8?q?d=20Chinese=20(chaoqun=20zou)=20*=20Hungarian=20(G=C3=A1bor=20T?= =?UTF-8?q?ak=C3=A1cs)=20*=20Russian=20(Denis=20Tomashenko)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://redmine.rubyforge.org/svn/trunk@1590 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- lang/hu.yml | 14 +++++++------- lang/ru.yml | 22 +++++++++++----------- lang/zh.yml | 12 ++++++------ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lang/hu.yml b/lang/hu.yml index ba904ed45..1810e93d8 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -355,7 +355,7 @@ 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ések +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 @@ -625,9 +625,9 @@ 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: '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_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 diff --git a/lang/ru.yml b/lang/ru.yml index f68505be5..10ea1aad6 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -108,7 +108,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: Минимальная длина @@ -239,9 +239,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 +276,7 @@ label_min_max_length: Минимальная - Максимальная длин label_list: Список label_date: Дата label_integer: Целый -label_float: Свободный +label_float: С плавающей точкой label_boolean: Логический label_string: Текст label_text: Длинный текст @@ -337,8 +337,8 @@ 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: Фильтры @@ -626,10 +626,10 @@ label_and_its_subprojects: %s и все подпроекты mail_body_reminder: "%d назначенных на вас задач на следующие %d дней:" mail_subject_reminder: "%d назначенных на вас задач в ближайшие дни" text_user_wrote: '%s написал:' -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_duplicated_by: дублируется +setting_enabled_scm: Задействовать SCM +text_enumeration_category_reassign_to: 'Назначить им следующее значение:' +text_enumeration_destroy_question: '%d объект(а,ов) связаны с этим значением.' label_incoming_emails: Incoming emails label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails diff --git a/lang/zh.yml b/lang/zh.yml index 9f81d42fa..f412f848c 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -214,6 +214,8 @@ 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: 时间跟踪 @@ -515,6 +517,8 @@ label_preferences: 首选项 label_chronological_order: 按时间顺序 label_reverse_chronological_order: 按时间顺序(倒序) label_planning: 计划 +label_incoming_emails: 接收邮件 +label_generate_key: 生成一个key button_login: 登录 button_submit: 提交 @@ -599,6 +603,8 @@ 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 个对象被关联到了这个枚举值。' default_role_manager: 管理人员 default_role_developper: 开发人员 @@ -625,9 +631,3 @@ default_activity_development: 开发 enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) -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 From a4a8b6381e4a162da85319e216a770ee7bd82202 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Thu, 26 Jun 2008 19:46:57 +0000 Subject: [PATCH 568/710] Adds a key in lang files (general_csv_decimal_separator) to set the decimal separator (point or comma) in csv exports (#1372). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1591 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/issues_helper.rb | 3 ++- app/helpers/timelog_helper.rb | 3 ++- lang/bg.yml | 1 + lang/cs.yml | 1 + lang/da.yml | 1 + lang/de.yml | 1 + lang/en.yml | 1 + lang/es.yml | 1 + lang/fi.yml | 1 + lang/fr.yml | 1 + lang/he.yml | 1 + lang/hu.yml | 1 + lang/it.yml | 1 + lang/ja.yml | 1 + lang/ko.yml | 1 + lang/lt.yml | 1 + lang/nl.yml | 1 + lang/no.yml | 1 + lang/pl.yml | 1 + lang/pt-br.yml | 1 + lang/pt.yml | 1 + lang/ro.yml | 1 + lang/ru.yml | 1 + lang/sr.yml | 1 + lang/sv.yml | 1 + lang/th.yml | 1 + lang/uk.yml | 1 + lang/zh-tw.yml | 1 + lang/zh.yml | 1 + 29 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index e8f21e9a4..f42002ec8 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -126,6 +126,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 @@ -168,7 +169,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/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb index db13556a1..7fd70e744 100644 --- a/app/helpers/timelog_helper.rb +++ b/app/helpers/timelog_helper.rb @@ -44,6 +44,7 @@ module TimelogHelper def entries_to_csv(entries) 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 @@ -67,7 +68,7 @@ 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 ] csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } diff --git a/lang/bg.yml b/lang/bg.yml index 1339d9a83..fd6e7e6cf 100644 --- a/lang/bg.yml +++ b/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: Понеделник,Вторник,Сряда,Четвъртък,Петък,Събота,Неделя diff --git a/lang/cs.yml b/lang/cs.yml index 00d0642de..2c760722b 100644 --- a/lang/cs.yml +++ b/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 diff --git a/lang/da.yml b/lang/da.yml index b8546ab20..d51d30817 100644 --- a/lang/da.yml +++ b/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 diff --git a/lang/de.yml b/lang/de.yml index 6bc7919f9..2bc0a16f7 100644 --- a/lang/de.yml +++ b/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 diff --git a/lang/en.yml b/lang/en.yml index 7ab73f051..180a70565 100644 --- a/lang/en.yml +++ b/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 diff --git a/lang/es.yml b/lang/es.yml index 2615d8f61..44d48190b 100644 --- a/lang/es.yml +++ b/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 diff --git a/lang/fi.yml b/lang/fi.yml index 51b0c04a0..ad719dd6c 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -48,6 +48,7 @@ general_text_no: 'ei' general_text_yes: 'kyllä' general_lang_name: 'Finnish (Suomi)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Maanantai,Tiistai,Keskiviikko,Torstai,Perjantai,Lauantai,Sunnuntai diff --git a/lang/fr.yml b/lang/fr.yml index 1fd86dd07..89b0b4c86 100644 --- a/lang/fr.yml +++ b/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 diff --git a/lang/he.yml b/lang/he.yml index 7455fc395..501cfdc9b 100644 --- a/lang/he.yml +++ b/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: שני,שלישי,רביעי,חמישי,שישי,שבת,ראשון diff --git a/lang/hu.yml b/lang/hu.yml index 1810e93d8..6f88eb962 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -48,6 +48,7 @@ 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 diff --git a/lang/it.yml b/lang/it.yml index 867c73f9a..14dc86fff 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -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 diff --git a/lang/ja.yml b/lang/ja.yml index aa4320c3a..5bdc3b554 100644 --- a/lang/ja.yml +++ b/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: 月曜日,火曜日,水曜日,木曜日,金曜日,土曜日,日曜日 diff --git a/lang/ko.yml b/lang/ko.yml index f945537a9..8e0d35ef4 100644 --- a/lang/ko.yml +++ b/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: 월요일,화요일,수요일,목요일,금요일,토요일,일요일 diff --git a/lang/lt.yml b/lang/lt.yml index 355f473c8..df2e2b471 100644 --- a/lang/lt.yml +++ b/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 diff --git a/lang/nl.yml b/lang/nl.yml index 89b8a5736..d1facc7a6 100644 --- a/lang/nl.yml +++ b/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 diff --git a/lang/no.yml b/lang/no.yml index 5e4bdcca3..bf25a15a5 100644 --- a/lang/no.yml +++ b/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 diff --git a/lang/pl.yml b/lang/pl.yml index 0b0f87b6f..28c5f19d4 100644 --- a/lang/pl.yml +++ b/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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index e2036afde..04c3b347c 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -48,6 +48,7 @@ general_text_no: 'não' general_text_yes: 'sim' general_lang_name: 'Português(Brasil)' general_csv_separator: ',' +general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: ISO-8859-1 general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sabado,Domingo diff --git a/lang/pt.yml b/lang/pt.yml index 543743cab..5a1ffc2e0 100644 --- a/lang/pt.yml +++ b/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 diff --git a/lang/ro.yml b/lang/ro.yml index 16c2ea104..3a1696c7b 100644 --- a/lang/ro.yml +++ b/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 diff --git a/lang/ru.yml b/lang/ru.yml index 10ea1aad6..7d0707f7f 100644 --- a/lang/ru.yml +++ b/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: Понедельник,Вторник,Среда,Четверг,Пятница,Суббота,Воскресенье diff --git a/lang/sr.yml b/lang/sr.yml index 83bfdf43c..5b8e5d934 100644 --- a/lang/sr.yml +++ b/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 diff --git a/lang/sv.yml b/lang/sv.yml index 375970f3e..4c6170f4a 100644 --- a/lang/sv.yml +++ b/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 diff --git a/lang/th.yml b/lang/th.yml index acbf146cf..e7f276a34 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -48,6 +48,7 @@ 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: จันทร์,อังคาร,พุธ,พฤหัสบดี,ศุกร์,เสาร์,อาทิตย์ diff --git a/lang/uk.yml b/lang/uk.yml index d70916b1e..275de0e4b 100644 --- a/lang/uk.yml +++ b/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: Понеділок,Вівторок,Середа,Четвер,П'ятниця,Субота,Неділя diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index 462633575..26ffa9461 100644 --- a/lang/zh-tw.yml +++ b/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: 星期一,星期二,星期三,星期四,星期五,星期六,星期日 diff --git a/lang/zh.yml b/lang/zh.yml index f412f848c..2bd0880a5 100644 --- a/lang/zh.yml +++ b/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: 星期一,星期二,星期三,星期四,星期五,星期六,星期日 From ce6cf66f6c3af26383cd25ed012d908be4b40bae Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 27 Jun 2008 20:13:56 +0000 Subject: [PATCH 569/710] Custom fields refactoring: most of code moved from controllers to models (using new module ActsAsCustomizable). git-svn-id: http://redmine.rubyforge.org/svn/trunk@1592 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/account_controller.rb | 5 -- app/controllers/issues_controller.rb | 24 ++--- app/controllers/projects_controller.rb | 14 +-- app/controllers/timelog_controller.rb | 2 +- app/controllers/users_controller.rb | 11 +-- app/helpers/custom_fields_helper.rb | 27 +++--- app/helpers/issues_helper.rb | 2 +- app/models/issue.rb | 14 ++- app/models/project.rb | 13 +-- app/models/query.rb | 4 +- app/models/user.rb | 8 +- app/views/account/register.rhtml | 4 +- app/views/issues/_form_custom_fields.rhtml | 13 +-- app/views/issues/show.rhtml | 6 +- app/views/projects/_form.rhtml | 12 +-- app/views/projects/show.rhtml | 2 +- app/views/users/_form.rhtml | 6 +- test/fixtures/custom_fields.yml | 11 ++- test/functional/issues_controller_test.rb | 87 +++++++++++++++++-- test/functional/projects_controller_test.rb | 2 +- test/integration/admin_test.rb | 5 +- test/unit/issue_test.rb | 78 ++++++++++++++++- vendor/plugins/acts_as_customizable/init.rb | 2 + .../lib/acts_as_customizable.rb | 82 +++++++++++++++++ 24 files changed, 321 insertions(+), 113 deletions(-) create mode 100644 vendor/plugins/acts_as_customizable/init.rb create mode 100644 vendor/plugins/acts_as_customizable/lib/acts_as_customizable.rb diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 3fbbc8912..a9b8a1b82 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -110,17 +110,12 @@ class AccountController < ApplicationController redirect_to(home_url) && return unless Setting.self_registration? if request.get? @user = User.new(:language => Setting.default_language) - @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) } else @user = User.new(params[:user]) @user.admin = false @user.login = params[:user][:login] @user.status = User::STATUS_REGISTERED @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] - @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, - :customized => @user, - :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) } - @user.custom_values = @custom_values case Setting.self_registration when '1' # Email activation diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 69c8e7932..49b443238 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -94,7 +94,6 @@ 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? @@ -113,15 +112,17 @@ 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) 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 @@ -134,17 +135,10 @@ 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 = IssueStatus.find_by_id(params[:issue][:status_id]) # Check that the user is allowed to apply the requested status @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, - :customized => @issue, - :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) } - @issue.custom_values = @custom_values if @issue.save attach_files(@issue, params[:attachments]) flash[:notice] = l(:notice_successful_create) @@ -165,7 +159,6 @@ class IssuesController < ApplicationController @allowed_statuses = @issue.new_statuses_allowed_to(User.current) @activities = Enumeration::get_values('ACTI') @priorities = Enumeration::get_values('IPRI') - @custom_values = [] @edit_allowed = User.current.allowed_to?(:edit_issues, @project) @notes = params[:notes] @@ -178,14 +171,7 @@ 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]) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c9a55088b..a37ae09a8 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -63,21 +63,17 @@ class ProjectsController < ApplicationController # 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) @@ -88,7 +84,6 @@ class ProjectsController < ApplicationController # Show @project def show - @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} @subprojects = @project.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") @@ -115,11 +110,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 @@ -127,10 +121,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) diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 2e257c5aa..cf1c844df 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -54,7 +54,7 @@ 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| + @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 custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)", :format => cf.field_format, :label => cf.name} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c37709661..eb8aa7bac 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -52,14 +52,11 @@ 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 - @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) @@ -71,16 +68,10 @@ 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 - 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 diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index 61c8d6b36..540c6b4a1 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -25,37 +25,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, custom_value.value, :id => field_id) 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? ? "" : '') : + '' + 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? ? " *" : ""), - :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/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index f42002ec8..403697178 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -149,7 +149,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) diff --git a/app/models/issue.rb b/app/models/issue.rb index d83b2ab02..326e234b0 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -28,13 +28,12 @@ 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_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', "#{table_name}.description"], :include => :project, :with => {:journal => :issue} acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, @@ -44,7 +43,6 @@ class Issue < ActiveRecord::Base 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 +52,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 @@ -168,11 +171,6 @@ 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 diff --git a/app/models/project.rb b/app/models/project.rb index f05ccb2af..a5ba246b1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -22,7 +22,6 @@ class Project < ActiveRecord::Base has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" has_many :users, :through => :members - has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :enabled_modules, :dependent => :delete_all has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] @@ -38,7 +37,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}", @@ -46,6 +45,7 @@ class Project < ActiveRecord::Base acts_as_tree :order => "name", :counter_cache => true + 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}} @@ -54,7 +54,6 @@ class Project < ActiveRecord::Base 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 => 255 @@ -195,12 +194,8 @@ 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 } - end - - def all_custom_fields - @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq + def all_issue_custom_fields + @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq end def project diff --git a/app/models/query.rb b/app/models/query.rb index 4c72e23f2..27ab882c6 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -176,7 +176,7 @@ class Query < ActiveRecord::Base 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 - add_custom_fields_filters(@project.all_custom_fields) + 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})) @@ -226,7 +226,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 diff --git a/app/models/user.rb b/app/models/user.rb index 5568027d5..a34b96861 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -37,12 +37,13 @@ class User < ActiveRecord::Base has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all has_many :projects, :through => :memberships - has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" belongs_to :auth_source + acts_as_customizable + attr_accessor :password, :password_confirmation attr_accessor :last_before_login_on # Prevents unauthorized assignments @@ -60,7 +61,6 @@ class User < ActiveRecord::Base 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 @@ -280,6 +280,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/app/views/account/register.rhtml b/app/views/account/register.rhtml index 7cf4b6da3..4e2b5adf2 100644 --- a/app/views/account/register.rhtml +++ b/app/views/account/register.rhtml @@ -27,8 +27,8 @@

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

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

      <%= custom_field_tag_with_label @custom_value %>

      +<% @user.custom_field_values.each do |value| %> +

      <%= custom_field_tag_with_label :user, value %>

      <% end %> diff --git a/app/views/issues/_form_custom_fields.rhtml b/app/views/issues/_form_custom_fields.rhtml index 1268bb1f9..ebd4c3219 100644 --- a/app/views/issues/_form_custom_fields.rhtml +++ b/app/views/issues/_form_custom_fields.rhtml @@ -1,11 +1,12 @@
      <% i = 1 %> -<% for @custom_value in values %> -

      <%= custom_field_tag_with_label @custom_value %>

      - <% if i == values.size / 2 %> +<% split_on = @issue.custom_field_values.size / 2 %> +<% @issue.custom_field_values.each do |value| %> +

      <%= custom_field_tag_with_label :issue, value %>

      +<% if i == split_on -%>
      - <% end %> - <% i += 1 %> -<% end %> +<% end -%> +<% i += 1 -%> +<% end -%>
      diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 57fbd05c7..a3b26be12 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -43,9 +43,9 @@ <% end %>
      <%= custom_value.custom_field.name %>:<%= simple_format(h(show_value(custom_value))) %><%=h value.custom_field.name %>:<%= simple_format(h(show_value(value))) %>