Merge branch 'ticket/unstable/288-april-upstream-redmine-review' into unstable

pull/351/head
Eric Davis 14 years ago
commit d14417070d
  1. 2
      .gitignore
  2. 4
      .hgignore
  3. 2
      app/controllers/groups_controller.rb
  4. 12
      app/controllers/issues_controller.rb
  5. 36
      app/controllers/journals_controller.rb
  6. 6
      app/controllers/news_controller.rb
  7. 17
      app/controllers/previews_controller.rb
  8. 11
      app/controllers/projects_controller.rb
  9. 9
      app/controllers/queries_controller.rb
  10. 29
      app/controllers/repositories_controller.rb
  11. 90
      app/controllers/timelog_controller.rb
  12. 18
      app/controllers/users_controller.rb
  13. 35
      app/controllers/wiki_controller.rb
  14. 17
      app/controllers/workflows_controller.rb
  15. 46
      app/helpers/application_helper.rb
  16. 10
      app/helpers/calendars_helper.rb
  17. 16
      app/helpers/custom_fields_helper.rb
  18. 20
      app/helpers/gantt_helper.rb
  19. 41
      app/helpers/issues_helper.rb
  20. 10
      app/helpers/queries_helper.rb
  21. 97
      app/helpers/repositories_helper.rb
  22. 8
      app/helpers/sort_helper.rb
  23. 9
      app/helpers/versions_helper.rb
  24. 58
      app/helpers/wiki_helper.rb
  25. 10
      app/models/auth_source.rb
  26. 4
      app/models/auth_source_ldap.rb
  27. 67
      app/models/changeset.rb
  28. 24
      app/models/comment_observer.rb
  29. 33
      app/models/custom_field.rb
  30. 37
      app/models/issue.rb
  31. 27
      app/models/issue_status.rb
  32. 9
      app/models/journal.rb
  33. 9
      app/models/journal_detail.rb
  34. 3
      app/models/mail_handler.rb
  35. 30
      app/models/mailer.rb
  36. 11
      app/models/news.rb
  37. 54
      app/models/project.rb
  38. 24
      app/models/query.rb
  39. 95
      app/models/repository.rb
  40. 16
      app/models/repository/bazaar.rb
  41. 34
      app/models/repository/cvs.rb
  42. 16
      app/models/repository/darcs.rb
  43. 17
      app/models/repository/filesystem.rb
  44. 51
      app/models/repository/git.rb
  45. 136
      app/models/repository/mercurial.rb
  46. 12
      app/models/repository/subversion.rb
  47. 1
      app/models/setting.rb
  48. 11
      app/models/time_entry.rb
  49. 65
      app/models/user.rb
  50. 3
      app/models/user_preference.rb
  51. 4
      app/models/wiki.rb
  52. 30
      app/models/wiki_page.rb
  53. 14
      app/views/activities/index.html.erb
  54. 1
      app/views/admin/projects.rhtml
  55. 1
      app/views/boards/show.rhtml
  56. 12
      app/views/calendars/show.html.erb
  57. 50
      app/views/common/_diff.rhtml
  58. 28
      app/views/custom_fields/_form.rhtml
  59. 18
      app/views/gantts/show.html.erb
  60. 2
      app/views/groups/_users.html.erb
  61. 2
      app/views/issues/_changesets.rhtml
  62. 2
      app/views/issues/_history.rhtml
  63. 1
      app/views/issues/_list.rhtml
  64. 8
      app/views/issues/_sidebar.rhtml
  65. 8
      app/views/issues/bulk_edit.rhtml
  66. 17
      app/views/journals/_notes_form.rhtml
  67. 10
      app/views/journals/diff.html.erb
  68. 10
      app/views/layouts/base.rhtml
  69. 2
      app/views/mailer/_issue_text_html.rhtml
  70. 2
      app/views/mailer/_issue_text_plain.rhtml
  71. 5
      app/views/mailer/news_comment_added.text.html.rhtml
  72. 6
      app/views/mailer/news_comment_added.text.plain.rhtml
  73. 4
      app/views/messages/edit.rhtml
  74. 1
      app/views/news/show.rhtml
  75. 2
      app/views/projects/show.rhtml
  76. 2
      app/views/queries/_columns.rhtml
  77. 18
      app/views/queries/_filters.rhtml
  78. 15
      app/views/repositories/_breadcrumbs.rhtml
  79. 16
      app/views/repositories/_dir_list_content.rhtml
  80. 54
      app/views/repositories/show.rhtml
  81. 4
      app/views/roles/report.rhtml
  82. 12
      app/views/search/index.rhtml
  83. 4
      app/views/settings/_display.rhtml
  84. 2
      app/views/settings/_repositories.rhtml
  85. 12
      app/views/time_entry_reports/report.rhtml
  86. 21
      app/views/timelog/_date_range.rhtml
  87. 6
      app/views/timelog/index.html.erb
  88. 1
      app/views/users/_preferences.html.erb
  89. 7
      app/views/users/index.rhtml
  90. 4
      app/views/versions/_issue_counts.rhtml
  91. 6
      app/views/wiki/diff.rhtml
  92. 2
      app/views/wiki/index.html.erb
  93. 40
      app/views/workflows/_form.html.erb
  94. 71
      app/views/workflows/edit.rhtml
  95. 2
      app/views/workflows/index.rhtml
  96. 14
      config/configuration.yml.example
  97. 6
      config/environment.rb
  98. 16
      config/initializers/10-patches.rb
  99. 10
      config/locales/bg.yml
  100. 10
      config/locales/bs.yml
  101. Some files were not shown because too many files have changed in this diff Show More

2
.gitignore vendored

@ -10,6 +10,8 @@
/db/*.sqlite3
/db/schema.rb
/files/*
/lib/redmine/scm/adapters/mercurial/redminehelper.pyc
/lib/redmine/scm/adapters/mercurial/redminehelper.pyo
/log/*.log*
/log/mongrel_debug
/public/dispatch.*

@ -12,6 +12,8 @@ db/*.db
db/*.sqlite3
db/schema.rb
files/*
lib/redmine/scm/adapters/mercurial/redminehelper.pyc
lib/redmine/scm/adapters/mercurial/redminehelper.pyo
log/*.log*
log/mongrel_debug
public/dispatch.*
@ -23,3 +25,5 @@ tmp/sockets/*
tmp/test/*
vendor/rails
*.rbc
.svn/
.git/

@ -132,7 +132,7 @@ class GroupsController < ApplicationController
def autocomplete_for_user
@group = Group.find(params[:id])
@users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
render :layout => false
end

@ -112,7 +112,7 @@ class IssuesController < ApplicationController
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@priorities = IssuePriority.all
@time_entry = TimeEntry.new
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.api
@ -236,7 +236,13 @@ class IssuesController < ApplicationController
return unless api_request?
end
end
@issues.each(&:destroy)
@issues.each do |issue|
begin
issue.reload.destroy
rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
# nothing to do, issue was already deleted (eg. by a parent)
end
end
respond_to do |format|
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
format.api { head :ok }
@ -265,7 +271,7 @@ private
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@priorities = IssuePriority.all
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
@time_entry.attributes = params[:time_entry]
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -16,13 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit]
before_filter :find_journal, :only => [:edit, :diff]
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
before_filter :authorize, :only => [:new, :edit]
before_filter :authorize, :only => [:new, :edit, :diff]
accept_key_auth :index
menu_item :issues
helper :issues
helper :custom_fields
helper :queries
include QueriesHelper
helper :sort
@ -44,6 +46,17 @@ class JournalsController < ApplicationController
render_404
end
def diff
@issue = @journal.issue
if params[:detail_id].present?
@detail = @journal.details.find_by_id(params[:detail_id])
else
@detail = @journal.details.detect {|d| d.prop_key == 'description'}
end
(render_404; return false) unless @issue && @detail
@diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
end
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
@ -68,6 +81,7 @@ class JournalsController < ApplicationController
end
def edit
(render_403; return false) unless @journal.editable_by?(User.current)
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
@ -76,13 +90,21 @@ class JournalsController < ApplicationController
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
else
respond_to do |format|
format.html {
# TODO: implement non-JS journal update
render :nothing => true
}
format.js
end
end
end
private
private
def find_journal
@journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current)
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -25,6 +25,8 @@ class NewsController < ApplicationController
before_filter :find_optional_project, :only => :index
accept_key_auth :index
helper :watchers
def index
case params[:format]
when 'xml', 'json'

@ -1,3 +1,20 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public 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 PreviewsController < ApplicationController
before_filter :find_project

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -143,7 +143,7 @@ class ProjectsController < ApplicationController
end
@users_by_role = @project.users_by_role
@subprojects = @project.children.visible
@subprojects = @project.children.visible.all
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers
@ -156,11 +156,10 @@ class ProjectsController < ApplicationController
:include => [:project, :status, :tracker],
:conditions => cond)
TimeEntry.visible_by(User.current) do
@total_hours = TimeEntry.sum(:hours,
:include => :project,
:conditions => cond).to_f
if User.current.allowed_to?(:view_time_entries, @project)
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
end
@key = User.current.rss_key
respond_to do |format|

@ -25,10 +25,11 @@ class QueriesController < ApplicationController
@query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.column_names = nil if params[:default_columns]
@query.add_filters(params[:fields], params[:operators], params[:values]) if params[:fields]
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
@query.group_by ||= params[:group_by]
@query.column_names = params[:c] if params[:c]
@query.column_names = nil if params[:default_columns]
if request.post? && params[:confirm] && @query.save
flash[:notice] = l(:notice_successful_create)
@ -41,10 +42,12 @@ class QueriesController < ApplicationController
def edit
if request.post?
@query.filters = {}
@query.add_filters(params[:fields], params[:operators], params[:values]) if params[:fields]
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
@query.attributes = params[:query]
@query.project = nil if params[:query_is_for_all]
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
@query.group_by ||= params[:group_by]
@query.column_names = params[:c] if params[:c]
@query.column_names = nil if params[:default_columns]
if @query.save

@ -77,6 +77,7 @@ class RepositoriesController < ApplicationController
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
@entries = @repository.entries(@path, @rev)
@changeset = @repository.find_changeset_by_name(@rev)
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
@ -122,16 +123,34 @@ class RepositoriesController < ApplicationController
@content = @repository.cat(@path, @rev)
(show_error_not_found; return) unless @content
if 'raw' == params[:format] || @content.is_binary_data? ||
(@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
if 'raw' == params[:format] ||
(@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
! is_entry_text_data?(@content, @path)
# Force the download
send_data @content, :filename => filename_for_content_disposition(@path.split('/').last)
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
send_type = Redmine::MimeType.of(@path)
send_opt[:type] = send_type.to_s if send_type
send_data @content, send_opt
else
# Prevent empty lines when displaying a file with Windows style eol
# TODO: UTF-16
# Is this needs? AttachmentsController reads file simply.
@content.gsub!("\r\n", "\n")
@changeset = @repository.find_changeset_by_name(@rev)
end
end
end
def is_entry_text_data?(ent, path)
# UTF-16 contains "\x00".
# It is very strict that file contains less than 30% of ascii symbols
# in non Western Europe.
return true if Redmine::MimeType.is_type?('text', path)
# Ruby 1.8.6 has a bug of integer divisions.
# http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
return false if ent.is_binary_data?
true
end
private :is_entry_text_data?
def annotate
@entry = @repository.entry(@path, @rev)
@ -218,7 +237,7 @@ class RepositoriesController < ApplicationController
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
@rev_to = params[:rev_to]
unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
if @repository.branches.blank?
raise InvalidRevisionParam
end

@ -40,60 +40,56 @@ class TimelogController < ApplicationController
'hours' => 'hours'
cond = ARCondition.new
if @project.nil?
cond << Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
cond << @project.project_condition(Setting.display_subprojects_issues?)
else
if @issue
cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
elsif @project
cond << @project.project_condition(Setting.display_subprojects_issues?)
end
retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
TimeEntry.visible_by(User.current) do
respond_to do |format|
format.html {
# Paginate results
@entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
@total_hours = TimeEntry.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
respond_to do |format|
format.html {
# Paginate results
@entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.visible.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
@total_hours = TimeEntry.visible.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
render :layout => !request.xhr?
}
format.api {
@entry_count = TimeEntry.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
}
format.atom {
entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => "#{TimeEntry.table_name}.created_on DESC",
:limit => Setting.feeds_limit.to_i)
render_feed(entries, :title => l(:label_spent_time))
}
format.csv {
# Export all entries
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
}
end
render :layout => !request.xhr?
}
format.api {
@entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.visible.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
}
format.atom {
entries = TimeEntry.visible.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.visible.find(:all,
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
}
end
end

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -38,6 +38,9 @@ class UsersController < ApplicationController
@limit = per_page_option
end
scope = User
scope = scope.in_group(params[:group_id].to_i) if params[:group_id].present?
@status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
@ -46,19 +49,22 @@ class UsersController < ApplicationController
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
end
@user_count = User.count(:conditions => c.conditions)
@user_count = scope.count(:conditions => c.conditions)
@user_pages = Paginator.new self, @user_count, @limit, params['page']
@offset ||= @user_pages.current.offset
@users = User.find :all,
@users = scope.find :all,
:order => sort_clause,
:conditions => c.conditions,
:limit => @limit,
:offset => @offset
respond_to do |format|
format.html { render :layout => !request.xhr? }
respond_to do |format|
format.html {
@groups = Group.all.sort
render :layout => !request.xhr?
}
format.api
end
end
end
def show

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -44,7 +44,14 @@ class WikiController < ApplicationController
# List of pages, sorted alphabetically and by parent (hierarchy)
def index
load_pages_grouped_by_date_without_content
load_pages_for_index
@pages_by_parent_id = @pages.group_by(&:parent_id)
end
# List of page, by last update
def date_index
load_pages_for_index
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
end
# display a page (in editing mode if it doesn't exist)
@ -93,9 +100,6 @@ class WikiController < ApplicationController
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
@ -131,7 +135,8 @@ class WikiController < ApplicationController
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
flash.now[:error] = l(:notice_locking_conflict)
render :action => 'edit'
end
# rename a page
@ -215,10 +220,6 @@ class WikiController < ApplicationController
redirect_to :action => 'show', :project_id => @project, :id => nil
end
end
def date_index
load_pages_grouped_by_date_without_content
end
def preview
page = @wiki.find_page(params[:id])
@ -266,14 +267,8 @@ private
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
end
# eager load information about last updates, without loading text
def load_pages_grouped_by_date_without_content
@pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
@pages_by_parent_id = @pages.group_by(&:parent_id)
end
def load_pages_for_index
@pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
end
end

@ -32,14 +32,17 @@ class WorkflowsController < ApplicationController
if request.post?
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
(params[:issue_status] || []).each { |old, news|
news.each { |new|
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
(params[:issue_status] || []).each { |status_id, transitions|
transitions.each { |new_status_id, options|
author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
}
}
if @role.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
return
end
end
@ -48,6 +51,14 @@ class WorkflowsController < ApplicationController
@statuses = @tracker.issue_statuses
end
@statuses ||= IssueStatus.find(:all, :order => 'position')
if @tracker && @role && @statuses.any?
workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
@workflows = {}
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
@workflows['author'] = workflows.select {|w| w.author}
@workflows['assignee'] = workflows.select {|w| w.assignee}
end
end
def copy

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2010 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -187,15 +187,15 @@ module ApplicationHelper
end
end
def render_page_hierarchy(pages, node=nil)
def render_page_hierarchy(pages, node=nil, options={})
content = ''
if pages[node]
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
content << "</li>\n"
end
content << "</ul>\n"
@ -223,8 +223,7 @@ module ApplicationHelper
# Renders the project quick-jump box
def render_project_jump_box
# Retrieve them now to avoid a COUNT query
projects = User.current.projects.all
projects = User.current.memberships.collect(&:project).compact.uniq
if projects.any?
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
@ -341,15 +340,15 @@ module ApplicationHelper
html = ''
if paginator.current.previous
html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
html << link_to_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
end
html << (pagination_links_each(paginator, options) do |n|
link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
link_to_content_update(n.to_s, url_param.merge(page_param => n))
end || '')
if paginator.current.next
html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
html << ' ' + link_to_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
end
unless count.nil?
@ -363,14 +362,8 @@ module ApplicationHelper
end
def per_page_links(selected=nil)
url_param = params.dup
url_param.clear if url_param.has_key?(:set_filter)
links = Setting.per_page_options_array.collect do |n|
n == selected ? n : link_to_remote(n, {:update => "content",
:url => params.dup.merge(:per_page => n),
:method => :get},
{:href => url_for(url_param.merge(:per_page => n))})
n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
@ -713,7 +706,7 @@ module ApplicationHelper
item = strip_tags(content).strip
anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
@parsed_headings << [level, anchor, item]
"<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
"<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
end
end
@ -855,6 +848,8 @@ module ApplicationHelper
'Calendar._FD = 1;' # Monday
when 7
'Calendar._FD = 0;' # Sunday
when 6
'Calendar._FD = 6;' # Saturday
else
'' # use language
end
@ -894,6 +889,15 @@ module ApplicationHelper
''
end
end
# Returns the javascript tags that are included in the html layout head
def javascript_heads
tags = javascript_include_tag(:defaults)
unless User.current.pref.warn_on_leaving_unsaved == '0'
tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
end
tags
end
def favicon
"<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
@ -937,11 +941,7 @@ module ApplicationHelper
return self
end
def link_to_remote_content_update(text, url_params)
link_to_remote(text,
{:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_params)}
)
def link_to_content_update(text, url_params = {}, html_options = {})
link_to(text, url_params, html_options)
end
end

@ -32,14 +32,6 @@ module CalendarsHelper
end
def link_to_month(link_name, year, month, options={})
project_id = options[:project].present? ? options[:project].to_param : nil
link_target = calendar_path(:year => year, :month => month, :project_id => project_id)
link_to_remote(link_name,
{:update => "content", :url => link_target, :method => :put},
{:href => link_target})
link_to_content_update(link_name, params.merge(:year => year, :month => month))
end
end

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -37,7 +37,7 @@ module CustomFieldsHelper
field_id = "#{name}_custom_field_values_#{custom_field.id}"
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
case field_format.edit_as
case field_format.try(:edit_as)
when "date"
text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
calendar_for(field_id)
@ -49,7 +49,7 @@ module CustomFieldsHelper
blank_option = custom_field.is_required? ?
(custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
'<option></option>'
select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value), :id => field_id)
else
text_field_tag(field_name, custom_value.value, :id => field_id)
end
@ -72,7 +72,7 @@ module CustomFieldsHelper
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}"
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
case field_format.edit_as
case field_format.try(:edit_as)
when "date"
text_field_tag(field_name, '', :id => field_id, :size => 10) +
calendar_for(field_id)
@ -83,7 +83,7 @@ module CustomFieldsHelper
[l(:general_text_yes), '1'],
[l(:general_text_no), '0']]), :id => field_id)
when "list"
select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values), :id => field_id)
select_tag(field_name, options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values_options), :id => field_id)
else
text_field_tag(field_name, '', :id => field_id)
end
@ -101,8 +101,8 @@ module CustomFieldsHelper
end
# Return an array of custom field formats which can be used in select_tag
def custom_field_formats_for_select
Redmine::CustomFieldFormat.as_select
def custom_field_formats_for_select(custom_field)
Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
end
# Renders the custom_values in api views

@ -21,29 +21,21 @@ module GanttHelper
case in_or_out
when :in
if gantt.zoom < 4
link_to_remote(l(:text_zoom_in),
{:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :method => :get, :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom+1))),
:class => 'icon icon-zoom-in'})
link_to_content_update l(:text_zoom_in),
params.merge(gantt.params.merge(:zoom => (gantt.zoom+1))),
:class => 'icon icon-zoom-in'
else
content_tag('span', l(:text_zoom_in), :class => 'icon icon-zoom-in')
end
when :out
if gantt.zoom > 1
link_to_remote(l(:text_zoom_out),
{:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :method => :get, :update => 'content'},
{:href => url_for(gantt.params.merge(:zoom => (gantt.zoom-1))),
:class => 'icon icon-zoom-out'})
link_to_content_update l(:text_zoom_out),
params.merge(gantt.params.merge(:zoom => (gantt.zoom-1))),
:class => 'icon icon-zoom-out'
else
content_tag('span', l(:text_zoom_out), :class => 'icon icon-zoom-out')
end
end
end
def number_of_issues_on_versions(gantt)
versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact
versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count}
end
end

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -57,11 +57,12 @@ module IssuesHelper
def render_issue_subject_with_tree(issue)
s = ''
issue.ancestors.each do |ancestor|
ancestors = issue.root? ? [] : issue.ancestors.all
ancestors.each do |ancestor|
s << '<div>' + content_tag('p', link_to_issue(ancestor))
end
s << '<div>' + content_tag('h3', h(issue.subject))
s << '</div>' * (issue.ancestors.size + 1)
s << '</div>' * (ancestors.size + 1)
s
end
@ -106,13 +107,32 @@ module IssuesHelper
# Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all,
:select => 'id, name',
:select => 'id, name, is_public',
:order => "name ASC",
:conditions => visible.conditions)
end
@sidebar_queries
end
def query_links(title, queries)
# links to #index on issues/show
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
content_tag('h3', title) +
queries.collect {|query|
link_to(h(query.name), url_params.merge(:query_id => query))
}.join('<br />')
end
def render_sidebar_queries
out = ''
queries = sidebar_queries.select {|q| !q.is_public?}
out << query_links(l(:label_my_queries), queries) if queries.any?
queries = sidebar_queries.select {|q| q.is_public?}
out << query_links(l(:label_query_plural), queries) if queries.any?
out
end
def show_detail(detail, no_html=false)
case detail.property
when 'attr'
@ -164,7 +184,16 @@ module IssuesHelper
end
end
if !detail.value.blank?
if detail.property == 'attr' && detail.prop_key == 'description'
s = l(:text_journal_changed_no_detail, :label => label)
unless no_html
diff_link = link_to 'diff',
{:controller => 'journals', :action => 'diff', :id => detail.journal_id, :detail_id => detail.id},
:title => l(:label_view_diff)
s << " (#{ diff_link })"
end
s
elsif !detail.value.blank?
case detail.property
when 'attr', 'cf'
if !detail.old_value.blank?

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -78,16 +78,16 @@ module QueriesHelper
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields]
if params[:fields] || params[:f]
@query.filters = {}
@query.add_filters(params[:fields], params[:operators], params[:values])
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
@query.group_by = params[:group_by]
@query.column_names = params[:query] && params[:query][:column_names]
@query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]

@ -117,13 +117,24 @@ module RepositoriesHelper
end
def to_utf8(str)
return str if str.nil?
str = to_utf8_internal(str)
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
end
str
end
def to_utf8_internal(str)
return str if str.nil?
if str.respond_to?(:force_encoding)
str.force_encoding('ASCII-8BIT')
end
return str if str.empty?
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
return str if str.valid_encoding?
else
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
end
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@encodings.each do |encoding|
begin
@ -132,24 +143,56 @@ module RepositoriesHelper
# do nothing here and try the next encoding
end
end
str = replace_invalid_utf8(str)
end
private :to_utf8_internal
def replace_invalid_utf8(str)
return str if str.nil?
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
if ! str.valid_encoding?
str = str.encode("US-ASCII", :invalid => :replace,
:undef => :replace, :replace => '?').encode("UTF-8")
end
else
# removes invalid UTF8 sequences
begin
str = Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
end
end
str
end
def repository_field_tags(form, repository)
def repository_field_tags(form, repository)
method = repository.class.name.demodulize.underscore + "_field_tags"
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
if repository.is_a?(Repository) &&
respond_to?(method) && method != 'repository_field_tags'
send(method, form, repository)
end
end
def scm_select_tag(repository)
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
Redmine::Scm::Base.all.each do |scm|
scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
if Setting.enabled_scm.include?(scm) ||
(repository && repository.class.name.demodulize == scm)
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
end
end
select_tag('repository_scm',
options_for_select(scm_options, repository.class.name.demodulize),
:disabled => (repository && !repository.new_record?),
:onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
:onchange => remote_function(
:url => {
:controller => 'repositories',
:action => 'edit',
:id => @project
},
:method => :get,
:with => "Form.serialize(this.form)")
)
end
@ -172,27 +215,47 @@ module RepositoriesHelper
end
def darcs_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => :label_darcs_path, :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
content_tag('p', form.text_field(:url, :label => :label_darcs_path, :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) +
content_tag('p', form.select(:log_encoding, [nil] + Setting::ENCODINGS,
:label => 'Commit messages encoding', :required => true))
end
def mercurial_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => :label_mercurial_path, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
content_tag('p', form.text_field(:url, :label => :label_mercurial_path, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />local repository (e.g. /hgrepo, c:\hgrepo)' ) +
content_tag('p', form.select(
:path_encoding, [nil] + Setting::ENCODINGS,
:label => 'Path encoding') +
'<br />Default: UTF-8')
end
def git_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => :label_git_path, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
content_tag('p', form.text_field(:url, :label => :label_git_path, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />a bare and local repository (e.g. /gitrepo, c:\gitrepo)') +
content_tag('p', form.select(
:path_encoding, [nil] + Setting::ENCODINGS,
:label => 'Path encoding') +
'<br />Default: UTF-8')
end
def cvs_field_tags(form, repository)
content_tag('p', form.text_field(:root_url, :label => :label_cvs_path, :size => 60, :required => true, :disabled => !repository.new_record?)) +
content_tag('p', form.text_field(:url, :label => :label_cvs_module, :size => 30, :required => true, :disabled => !repository.new_record?))
content_tag('p', form.text_field(:url, :label => :label_cvs_module, :size => 30, :required => true, :disabled => !repository.new_record?)) +
content_tag('p', form.select(:log_encoding, [nil] + Setting::ENCODINGS,
:label => 'Commit messages encoding', :required => true))
end
def bazaar_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => :label_bazaar_path, :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
content_tag('p', form.text_field(:url, :label => :label_bazaar_path, :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) +
content_tag('p', form.select(:log_encoding, [nil] + Setting::ENCODINGS,
:label => 'Commit messages encoding', :required => true))
end
def filesystem_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => :label_filesystem_path, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
content_tag('p', form.text_field(:url, :label => :label_filesystem_path, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) +
content_tag('p', form.select(:path_encoding, [nil] + Setting::ENCODINGS,
:label => 'Path encoding') +
'<br />Default: UTF-8')
end
end

@ -200,16 +200,12 @@ module SortHelper
caption = column.to_s.humanize unless caption
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
# don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
url_options = params.merge(sort_options)
# Add project_id to url_options
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
link_to_remote(caption,
{:update => "content", :url => url_options, :method => :get},
{:href => url_for(url_options),
:class => css})
link_to_content_update(caption, url_options, :class => css)
end
# Returns a table header <th> tag with a sort link for the named column

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -17,11 +17,10 @@
module VersionsHelper
STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to)
STATUS_BY_CRITERIAS = %w(category tracker status priority author assigned_to)
def render_issue_status_by(version, criteria)
criteria ||= 'category'
raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria)
criteria = 'category' unless STATUS_BY_CRITERIAS.include?(criteria)
h = Hash.new {|k,v| k[v] = [0, 0]}
begin

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -18,52 +18,18 @@
module WikiHelper
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
s = ''
pages.select {|p| p.parent == parent}.each do |page|
attrs = "value='#{page.id}'"
attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
wiki_page_options_for_select(pages, selected, page, level + 1)
end
s
end
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}
words_add = 0
words_del = 0
dels = 0
del_off = 0
wdiff.diff.diffs.each do |diff|
add_at = nil
add_to = nil
del_at = nil
deleted = ""
diff.each do |change|
pos = change[1]
if change[0] == "+"
add_at = pos + dels unless add_at
add_to = pos + dels
words_add += 1
else
del_at = pos unless del_at
deleted << ' ' + h(change[2])
words_del += 1
end
end
if add_at
words[add_at] = '<span class="diff_in">' + words[add_at]
words[add_to] = words[add_to] + '</span>'
end
if del_at
words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
dels += 1
del_off += words_del
words_del = 0
if pages.has_key?(parent)
pages[parent].each do |page|
attrs = "value='#{page.id}'"
attrs << " selected='selected'" if selected == page
indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
wiki_page_options_for_select(pages, selected, page, level + 1)
end
end
simple_format_without_paragraph(words.join(' '))
s
end
end

@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSource < ActiveRecord::Base
include Redmine::Ciphering
has_many :users
validates_presence_of :name
@ -31,6 +33,14 @@ class AuthSource < ActiveRecord::Base
def auth_method_name
"Abstract"
end
def account_password
read_ciphered_attribute(:account_password)
end
def account_password=(arg)
write_ciphered_attribute(:account_password, arg)
end
def allow_password_changes?
self.class.allow_password_changes?

@ -20,8 +20,8 @@ require 'iconv'
class AuthSourceLdap < AuthSource
validates_presence_of :host, :port, :attr_login
validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true
validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true
validates_length_of :name, :host, :maximum => 60, :allow_nil => true
validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_nil => true
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
validates_numericality_of :port, :only_integer => true

@ -56,10 +56,6 @@ class Changeset < ActiveRecord::Base
revision.to_s
end
end
def comments=(comment)
write_attribute(:comments, Changeset.normalize_comments(comment))
end
def committed_on=(date)
self.commit_date = date
@ -75,10 +71,6 @@ class Changeset < ActiveRecord::Base
end
end
def committer=(arg)
write_attribute(:committer, self.class.to_utf8(arg.to_s))
end
def project
repository.project
end
@ -88,20 +80,24 @@ class Changeset < ActiveRecord::Base
end
def before_create
self.user = repository.find_committer_user(committer)
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
self.comments = self.class.normalize_comments(self.comments, repository.repo_log_encoding)
self.user = repository.find_committer_user(self.committer)
end
def after_create
scan_comment_for_issue_ids
end
TIMELOG_RE = /
(
(\d+([.,]\d+)?)h?
((\d+)(h|hours?))((\d+)(m|min)?)?
|
((\d+)(h|hours?|m|min))
|
(\d+):(\d+)
|
((\d+)(h|hours?))?((\d+)(m|min)?)?
(\d+([\.,]\d+)?)h?
)
/x
@ -161,11 +157,6 @@ class Changeset < ActiveRecord::Base
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
end
# Strips and reencodes a commit log before insertion into the database
def self.normalize_comments(str)
to_utf8(str.to_s.strip)
end
# Creates a new Change from it's common parameters
def create_change(change)
Change.create(:changeset => self,
@ -174,7 +165,7 @@ class Changeset < ActiveRecord::Base
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
private
# Finds an issue that can be referenced by the commit message
@ -183,7 +174,7 @@ class Changeset < ActiveRecord::Base
return nil if id.blank?
issue = Issue.find_by_id(id.to_i, :include => :project)
if issue
unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
unless issue.project && (project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project))
issue = nil
end
end
@ -244,15 +235,17 @@ class Changeset < ActiveRecord::Base
return @short_comments, @long_comments
end
def self.to_utf8(str)
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
return str if str.valid_encoding?
else
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
end
public
# Strips and reencodes a commit log before insertion into the database
def self.normalize_comments(str, encoding)
Changeset.to_utf8(str.to_s.strip, encoding)
end
private
encoding = Setting.commit_logs_encoding.to_s.strip
def self.to_utf8(str, encoding)
return str if str.blank?
unless encoding.blank? || encoding == 'UTF-8'
begin
str = Iconv.conv('UTF-8', encoding, str)
@ -260,12 +253,20 @@ class Changeset < ActiveRecord::Base
# do nothing here
end
end
# removes invalid UTF8 sequences
begin
Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
str
if str.respond_to?(:force_encoding)
str.force_encoding('UTF-8')
if ! str.valid_encoding?
str = str.encode("US-ASCII", :invalid => :replace,
:undef => :replace, :replace => '?').encode("UTF-8")
end
else
# removes invalid UTF8 sequences
begin
str = Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
rescue Iconv::InvalidEncoding
# "UTF-8//IGNORE" is not supported on some OS
end
end
str
end
end

@ -0,0 +1,24 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public 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 CommentObserver < ActiveRecord::Observer
def after_create(comment)
if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added')
Mailer.deliver_news_comment_added(comment)
end
end
end

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -48,6 +48,33 @@ class CustomField < ActiveRecord::Base
errors.add(:default_value, :invalid) unless v.valid?
end
def possible_values_options(obj=nil)
case field_format
when 'user', 'version'
if obj.respond_to?(:project) && obj.project
case field_format
when 'user'
obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
when 'version'
obj.project.versions.sort.collect {|u| [u.to_s, u.id.to_s]}
end
else
[]
end
else
read_attribute :possible_values
end
end
def possible_values(obj=nil)
case field_format
when 'user'
possible_values_options(obj).collect(&:last)
else
read_attribute :possible_values
end
end
# Makes possible_values accept a multiline string
def possible_values=(arg)
if arg.is_a?(Array)
@ -71,6 +98,8 @@ class CustomField < ActiveRecord::Base
casted = value.to_i
when 'float'
casted = value.to_f
when 'user', 'version'
casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
end
end
casted

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-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,7 +60,7 @@ class Issue < ActiveRecord::Base
validates_numericality_of :estimated_hours, :allow_nil => true
named_scope :visible, lambda {|*args| { :include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
:conditions => Issue.visible_condition(args.first || User.current) } }
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
@ -68,11 +68,6 @@ class Issue < ActiveRecord::Base
named_scope :with_limit, lambda { |limit| { :limit => limit} }
named_scope :on_active_project, :include => [:status, :project, :tracker],
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
named_scope :for_gantt, lambda {
{
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
}
}
named_scope :without_version, lambda {
{
@ -91,6 +86,11 @@ class Issue < ActiveRecord::Base
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
after_destroy :update_parent_attributes
# Returns a SQL conditions string used to find all issues visible by the specified user
def self.visible_condition(user, options={})
Project.allowed_to_condition(user, :view_issues, options)
end
# Returns true if usr or current user is allowed to view the issue
def visible?(usr=nil)
(usr || User.current).allowed_to?(:view_issues, self.project)
@ -106,7 +106,7 @@ class Issue < ActiveRecord::Base
# 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 } : []
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
end
def copy_from(arg)
@ -422,7 +422,12 @@ class Issue < ActiveRecord::Base
# Returns an array of status that user is able to apply
def new_statuses_allowed_to(user, include_default=false)
statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
statuses = status.find_new_statuses_allowed_to(
user.roles_for_project(project),
tracker,
author == user,
assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
)
statuses << status unless statuses.empty?
statuses << IssueStatus.default if include_default
statuses = statuses.uniq.sort
@ -455,11 +460,11 @@ class Issue < ActiveRecord::Base
(relations_from + relations_to).sort
end
def all_dependent_issues(except=nil)
except ||= self
def all_dependent_issues(except=[])
except << self
dependencies = []
relations_from.each do |relation|
if relation.issue_to && relation.issue_to != except
if relation.issue_to && !except.include?(relation.issue_to)
dependencies << relation.issue_to
dependencies += relation.issue_to.all_dependent_issues(except)
end
@ -527,6 +532,8 @@ class Issue < ActiveRecord::Base
s = "issue status-#{status.position} priority-#{priority.position}"
s << ' closed' if closed?
s << ' overdue' if overdue?
s << ' child' if child?
s << ' parent' unless leaf?
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
s
@ -536,7 +543,7 @@ class Issue < ActiveRecord::Base
# Returns false if save fails
def save_issue_with_child_records(params, existing_time_entry=nil)
Issue.transaction do
if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
@time_entry = existing_time_entry || TimeEntry.new
@time_entry.project = project
@time_entry.issue = self
@ -824,7 +831,7 @@ class Issue < ActiveRecord::Base
def create_journal
if @current_journal
# attributes changes
(Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),

@ -50,10 +50,16 @@ class IssueStatus < ActiveRecord::Base
# Returns an array of all statuses the given role can switch to
# Uses association cache when called more than one time
def new_statuses_allowed_to(roles, tracker)
def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
if roles && tracker
role_ids = roles.collect(&:id)
new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
transitions = workflows.select do |w|
role_ids.include?(w.role_id) &&
w.tracker_id == tracker.id &&
(author || !w.author) &&
(assignee || !w.assignee)
end
transitions.collect{|w| w.new_status}.compact.sort
else
[]
end
@ -61,24 +67,19 @@ class IssueStatus < ActiveRecord::Base
# Same thing as above but uses a database query
# More efficient than the previous method if called just once
def find_new_statuses_allowed_to(roles, tracker)
def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
if roles && tracker
conditions = {:role_id => roles.collect(&:id), :tracker_id => tracker.id}
conditions[:author] = false unless author
conditions[:assignee] = false unless assignee
workflows.find(:all,
:include => :new_status,
:conditions => { :role_id => roles.collect(&:id),
:tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
:conditions => conditions).collect{|w| w.new_status}.compact.sort
else
[]
end
end
def new_status_allowed_to?(status, roles, tracker)
if status && roles && tracker
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
else
false
end
end
def <=>(status)
position <=> status.position

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -38,6 +38,11 @@ class Journal < ActiveRecord::Base
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
named_scope :visible, lambda {|*args| {
:include => {:issue => :project},
:conditions => Issue.visible_condition(args.first || User.current)
}}
def save(*args)
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -17,9 +17,4 @@
class JournalDetail < ActiveRecord::Base
belongs_to :journal
def before_save
self.value = value[0..254] if value && value.is_a?(String)
self.old_value = old_value[0..254] if old_value && old_value.is_a?(String)
end
end

@ -159,9 +159,10 @@ class MailHandler < ActionMailer::Base
# ignore CLI-supplied defaults for new issues
@@handler_options[:issue].clear
journal = issue.init_journal(user, cleaned_up_text_body)
journal = issue.init_journal(user)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
journal.notes = cleaned_up_text_body
add_attachments(issue)
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -88,7 +88,7 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_reminder, :count => issues.size, :days => days)
body :issues => issues,
:days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort => 'due_date:asc')
render_multipart('reminder', body)
end
@ -118,11 +118,11 @@ class Mailer < ActionMailer::Base
added_to_url = ''
case container.class.name
when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
added_to = "#{l(:label_project)}: #{container}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
added_to = "#{l(:label_version)}: #{container.name}"
recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
when 'Document'
@ -154,6 +154,24 @@ class Mailer < ActionMailer::Base
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
render_multipart('news_added', body)
end
# Builds a tmail object used to email recipients of a news' project when a news comment is added.
#
# Example:
# news_comment_added(comment) => tmail object
# Mailer.news_comment_added(comment) => sends an email to the news' project recipients
def news_comment_added(comment)
news = comment.commented
redmine_headers 'Project' => news.project.identifier
message_id comment
recipients news.recipients
cc news.watcher_recipients
subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:comment => comment,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
render_multipart('news_comment_added', body)
end
# Builds a tmail object used to email the recipients of the specified message that was posted.
#
@ -341,7 +359,7 @@ class Mailer < ActionMailer::Base
:conditions => s.conditions
).group_by(&:assigned_to)
issues_by_assignee.each do |assignee, issues|
deliver_reminder(assignee, issues, days) unless assignee.nil?
deliver_reminder(assignee, issues, days) if assignee && assignee.active?
end
end

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -28,6 +28,9 @@ class News < ActiveRecord::Base
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:author_key => :author_id
acts_as_watchable
after_create :add_author_as_watcher
named_scope :visible, lambda {|*args| {
:include => :project,
@ -42,4 +45,10 @@ class News < ActiveRecord::Base
def self.latest(user = User.current, count = 5)
find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
end
private
def add_author_as_watcher
Watcher.create(:watchable => self, :user => author)
end
end

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -43,7 +43,7 @@ class Project < ActiveRecord::Base
has_many :time_entries, :dependent => :delete_all
has_many :queries, :dependent => :delete_all
has_many :documents, :dependent => :destroy
has_many :news, :dependent => :delete_all, :include => :author
has_many :news, :dependent => :destroy, :include => :author
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
has_many :boards, :dependent => :destroy, :order => "position ASC"
has_one :repository, :dependent => :destroy
@ -56,7 +56,7 @@ class Project < ActiveRecord::Base
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
:association_foreign_key => 'custom_field_id'
acts_as_nested_set :order => 'name'
acts_as_nested_set :order => 'name', :dependent => :destroy
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
@ -79,7 +79,7 @@ class Project < ActiveRecord::Base
# reserved words
validates_exclusion_of :identifier, :in => %w( new )
before_destroy :delete_all_members, :destroy_children
before_destroy :delete_all_members
named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
@ -135,7 +135,6 @@ class Project < ActiveRecord::Base
end
def self.allowed_to_condition(user, permission, options={})
statements = []
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if perm = Redmine::AccessControl.permission(permission)
unless perm.project_module.nil?
@ -148,24 +147,31 @@ class Project < ActiveRecord::Base
project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
base_statement = "(#{project_statement}) AND (#{base_statement})"
end
if user.admin?
# no restriction
base_statement
else
statements << "1=0"
statement_by_role = {}
if user.logged?
if Role.non_member.allowed_to?(permission) && !options[:member]
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
statement_by_role[Role.non_member] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
end
user.projects_by_role.each do |role, projects|
if role.allowed_to?(permission)
statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
end
end
allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
else
if Role.anonymous.allowed_to?(permission) && !options[:member]
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
statement_by_role[Role.anonymous] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
end
end
if statement_by_role.empty?
"1=0"
else
"((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
end
end
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end
# Returns the Systemwide and project specific activities
@ -347,7 +353,7 @@ class Project < ActiveRecord::Base
# Returns an array of the trackers used by the project and its active sub projects
def rolled_up_trackers
@rolled_up_trackers ||=
Tracker.find(:all, :include => :projects,
Tracker.find(:all, :joins => :projects,
:select => "DISTINCT #{Tracker.table_name}.*",
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
:order => "#{Tracker.table_name}.position")
@ -373,15 +379,17 @@ class Project < ActiveRecord::Base
# Returns a scope of the Versions used by the project
def shared_versions
@shared_versions ||=
@shared_versions ||= begin
r = root? ? self : root
Version.scoped(:include => :project,
:conditions => "#{Project.table_name}.id = #{id}" +
" OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
" #{Version.table_name}.sharing = 'system'" +
" OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
" OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
"))")
end
end
# Returns a hash of project users grouped by role
@ -509,10 +517,7 @@ class Project < ActiveRecord::Base
def enabled_module_names=(module_names)
if module_names && module_names.is_a?(Array)
module_names = module_names.collect(&:to_s).reject(&:blank?)
# remove disabled modules
enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
# add new modules
module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
else
enabled_modules.clear
end
@ -621,13 +626,6 @@ class Project < ActiveRecord::Base
private
# Destroys children before destroying self
def destroy_children
children.each do |child|
child.destroy
end
end
# Copies wiki from +project+
def copy_wiki(project)
# Check that the source project has a wiki first

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -216,14 +216,19 @@ class Query < ActiveRecord::Base
if project
# project specific filters
unless @project.issue_categories.empty?
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
categories = @project.issue_categories.all
unless categories.empty?
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
end
unless @project.shared_versions.empty?
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
versions = @project.shared_versions.all
unless versions.empty?
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
end
unless @project.descendants.active.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
unless @project.leaf?
subprojects = @project.descendants.visible.all
unless subprojects.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
end
end
add_custom_fields_filters(@project.all_issue_custom_fields)
else
@ -411,7 +416,7 @@ class Query < ActiveRecord::Base
elsif project
project_clauses << "#{Project.table_name}.id = %d" % project.id
end
project_clauses << Project.allowed_to_condition(User.current, :view_issues)
project_clauses << Issue.visible_condition(User.current)
project_clauses.join(' AND ')
end
@ -636,6 +641,9 @@ 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 }
when "user", "version"
next unless project
options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
else
options = { :type => :string, :order => 20 }
end

@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Repository < ActiveRecord::Base
include Redmine::Ciphering
belongs_to :project
has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
has_many :changes, :through => :changesets
@ -24,29 +26,43 @@ class Repository < ActiveRecord::Base
# has_many :changesets, :dependent => :destroy is too slow for big repositories
before_destroy :clear_changesets
validates_length_of :password, :maximum => 255, :allow_nil => true
# Checks if the SCM is enabled when creating a repository
validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
# Removes leading and trailing whitespace
def url=(arg)
write_attribute(:url, arg ? arg.to_s.strip : nil)
end
# Removes leading and trailing whitespace
def root_url=(arg)
write_attribute(:root_url, arg ? arg.to_s.strip : nil)
end
def password
read_ciphered_attribute(:password)
end
def password=(arg)
write_ciphered_attribute(:password, arg)
end
def scm_adapter
self.class.scm_adapter_class
end
def scm
@scm ||= self.scm_adapter.new url, root_url, login, password
@scm ||= self.scm_adapter.new(url, root_url,
login, password, path_encoding)
update_attribute(:root_url, @scm.root_url) if root_url.blank?
@scm
end
def scm_name
self.class.scm_name
end
def supports_cat?
scm.supports_cat?
end
@ -54,6 +70,14 @@ class Repository < ActiveRecord::Base
def supports_annotate?
scm.supports_annotate?
end
def supports_all_revisions?
true
end
def supports_directory_revisions?
false
end
def entry(path=nil, identifier=nil)
scm.entry(path, identifier)
@ -74,15 +98,15 @@ class Repository < ActiveRecord::Base
def default_branch
scm.default_branch
end
def properties(path, identifier=nil)
scm.properties(path, identifier)
end
def cat(path, identifier=nil)
scm.cat(path, identifier)
end
def diff(path, rev, rev_to)
scm.diff(path, rev, rev_to)
end
@ -173,18 +197,27 @@ class Repository < ActiveRecord::Base
user
end
end
def repo_log_encoding
encoding = log_encoding.to_s.strip
encoding.blank? ? 'UTF-8' : encoding
end
# Fetches new changesets for all repositories of active projects
# Can be called periodically by an external script
# eg. ruby script/runner "Repository.fetch_changesets"
def self.fetch_changesets
Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
if project.repository
project.repository.fetch_changesets
begin
project.repository.fetch_changesets
rescue Redmine::Scm::Adapters::CommandFailed => e
logger.error "scm: error during fetching changesets: #{e.message}"
end
end
end
end
# scan changeset comments to find related and fixed issues for all repositories
def self.scan_changesets_for_issue_ids
find(:all).each(&:scan_changesets_for_issue_ids)
@ -197,16 +230,50 @@ class Repository < ActiveRecord::Base
def self.available_scm
subclasses.collect {|klass| [klass.scm_name, klass.name]}
end
def self.factory(klass_name, *args)
klass = "Repository::#{klass_name}".constantize
klass.new(*args)
rescue
nil
end
def self.scm_adapter_class
nil
end
def self.scm_command
ret = ""
begin
ret = self.scm_adapter_class.client_command if self.scm_adapter_class
rescue Redmine::Scm::Adapters::CommandFailed => e
logger.error "scm: error during get command: #{e.message}"
end
ret
end
def self.scm_version_string
ret = ""
begin
ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
rescue Redmine::Scm::Adapters::CommandFailed => e
logger.error "scm: error during get version string: #{e.message}"
end
ret
end
def self.scm_available
ret = false
begin
ret = self.scm_adapter_class.client_available if self.scm_adapter_class
rescue Redmine::Scm::Adapters::CommandFailed => e
logger.error "scm: error during get scm available: #{e.message}"
end
ret
end
private
def before_save
# Strips url and root_url
url.strip!

@ -19,16 +19,24 @@ require 'redmine/scm/adapters/bazaar_adapter'
class Repository::Bazaar < Repository
attr_protected :root_url
validates_presence_of :url
validates_presence_of :url, :log_encoding
def scm_adapter
ATTRIBUTE_KEY_NAMES = {
"url" => "Root directory",
"log_encoding" => "Commit messages encoding",
}
def self.human_attribute_name(attribute_key_name)
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
end
def self.scm_adapter_class
Redmine::Scm::Adapters::BazaarAdapter
end
def self.scm_name
'Bazaar'
end
def entries(path=nil, identifier=nil)
entries = scm.entries(path, identifier)
if entries

@ -19,16 +19,25 @@ require 'redmine/scm/adapters/cvs_adapter'
require 'digest/sha1'
class Repository::Cvs < Repository
validates_presence_of :url, :root_url
validates_presence_of :url, :root_url, :log_encoding
def scm_adapter
ATTRIBUTE_KEY_NAMES = {
"url" => "CVSROOT",
"root_url" => "Module",
"log_encoding" => "Commit messages encoding",
}
def self.human_attribute_name(attribute_key_name)
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
end
def self.scm_adapter_class
Redmine::Scm::Adapters::CvsAdapter
end
def self.scm_name
'CVS'
end
def entry(path=nil, identifier=nil)
rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
scm.entry(path, rev.nil? ? nil : rev.committed_on)
@ -39,13 +48,15 @@ class Repository::Cvs < Repository
entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
if entries
entries.each() do |entry|
unless entry.lastrev.nil? || entry.lastrev.identifier
change=changes.find_by_revision_and_path( entry.lastrev.revision, scm.with_leading_slash(entry.path) )
if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
change=changes.find_by_revision_and_path(
entry.lastrev.revision,
scm.with_leading_slash(entry.path) )
if change
entry.lastrev.identifier=change.changeset.revision
entry.lastrev.author=change.changeset.committer
entry.lastrev.revision=change.revision
entry.lastrev.branch=change.branch
entry.lastrev.identifier = change.changeset.revision
entry.lastrev.revision = change.changeset.revision
entry.lastrev.author = change.changeset.committer
# entry.lastrev.branch = change.branch
end
end
end
@ -107,10 +118,11 @@ class Repository::Cvs < Repository
tmp_time = revision.time.clone
unless changes.find_by_path_and_revision(
scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
cs = changesets.find(:first, :conditions=>{
:committed_on=>tmp_time - time_delta .. tmp_time + time_delta,
:committer=>revision.author,
:comments=>Changeset.normalize_comments(revision.message)
:comments=>cmt
})
# create a new changeset....

@ -18,16 +18,24 @@
require 'redmine/scm/adapters/darcs_adapter'
class Repository::Darcs < Repository
validates_presence_of :url
validates_presence_of :url, :log_encoding
def scm_adapter
ATTRIBUTE_KEY_NAMES = {
"url" => "Root directory",
"log_encoding" => "Commit messages encoding",
}
def self.human_attribute_name(attribute_key_name)
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
end
def self.scm_adapter_class
Redmine::Scm::Adapters::DarcsAdapter
end
def self.scm_name
'Darcs'
end
def entry(path=nil, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
scm.entry(path, patch.nil? ? nil : patch.scmid)

@ -24,14 +24,25 @@ class Repository::Filesystem < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
ATTRIBUTE_KEY_NAMES = {
"url" => "Root directory",
}
def self.human_attribute_name(attribute_key_name)
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
end
def self.scm_adapter_class
Redmine::Scm::Adapters::FilesystemAdapter
end
def self.scm_name
'Filesystem'
end
def supports_all_revisions?
false
end
def entries(path=nil, identifier=nil)
scm.entries(path, identifier)
end

@ -21,14 +21,29 @@ class Repository::Git < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
ATTRIBUTE_KEY_NAMES = {
"url" => "Path to repository",
}
def self.human_attribute_name(attribute_key_name)
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
end
def self.scm_adapter_class
Redmine::Scm::Adapters::GitAdapter
end
def self.scm_name
'Git'
end
def supports_directory_revisions?
true
end
def repo_log_encoding
'UTF-8'
end
# Returns the identifier for the given git changeset
def self.changeset_identifier(changeset)
changeset.scmid
@ -47,6 +62,13 @@ class Repository::Git < Repository
scm.tags
end
def find_changeset_by_name(name)
return nil if name.nil? || name.empty?
e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
return e if e
changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
end
# With SCM's that have a sequential commit numbering, redmine is able to be
# clever and only fetch changesets going forward from the most recent one
# it knows about. However, with git, you never know if people have merged
@ -59,7 +81,7 @@ class Repository::Git < Repository
c = changesets.find(:first, :order => 'committed_on DESC')
since = (c ? c.committed_on - 7.days : nil)
revisions = scm.revisions('', nil, nil, :all => true, :since => since)
revisions = scm.revisions('', nil, nil, {:all => true, :since => since, :reverse => true})
return if revisions.nil? || revisions.empty?
recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
@ -72,7 +94,28 @@ class Repository::Git < Repository
revisions.reject!{|r| recent_revisions.include?(r.scmid)}
# Save the remaining ones to the database
revisions.each{|r| r.save(self)} unless revisions.nil?
unless revisions.nil?
revisions.each do |rev|
transaction do
changeset = Changeset.new(
:repository => self,
:revision => rev.identifier,
:scmid => rev.scmid,
:committer => rev.author,
:committed_on => rev.time,
:comments => rev.message)
if changeset.save
rev.paths.each do |file|
Change.create(
:changeset => changeset,
:action => file[:action],
:path => file[:path])
end
end
end
end
end
end
def latest_changesets(path,rev,limit=10)

@ -24,7 +24,16 @@ class Repository::Mercurial < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
FETCH_AT_ONCE = 100 # number of changesets to fetch at once
ATTRIBUTE_KEY_NAMES = {
"url" => "Root directory",
}
def self.human_attribute_name(attribute_key_name)
ATTRIBUTE_KEY_NAMES[attribute_key_name] || super
end
def self.scm_adapter_class
Redmine::Scm::Adapters::MercurialAdapter
end
@ -32,6 +41,14 @@ class Repository::Mercurial < Repository
'Mercurial'
end
def supports_directory_revisions?
true
end
def repo_log_encoding
'UTF-8'
end
# Returns the readable identifier for the given mercurial changeset
def self.format_changeset_identifier(changeset)
"#{changeset.revision}:#{changeset.scmid}"
@ -46,29 +63,6 @@ class Repository::Mercurial < Repository
super(cs, cs_to, ' ')
end
def entries(path=nil, identifier=nil)
entries=scm.entries(path, identifier)
if entries
entries.each do |entry|
next unless entry.is_file?
# Set the filesize unless browsing a specific revision
if identifier.nil?
full_path = File.join(root_url, entry.path)
entry.size = File.stat(full_path).size if File.file?(full_path)
end
# Search the DB for the entry's last change
change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
if change
entry.lastrev.identifier = change.changeset.revision
entry.lastrev.name = change.changeset.revision
entry.lastrev.author = change.changeset.committer
entry.lastrev.revision = change.revision
end
end
end
entries
end
# Finds and returns a revision with a number or the beginning of a hash
def find_changeset_by_name(name)
return nil if name.nil? || name.empty?
@ -82,50 +76,70 @@ class Repository::Mercurial < Repository
end
# Returns the latest changesets for +path+; sorted by revision number
#
# Because :order => 'id DESC' is defined at 'has_many',
# there is no need to set 'order'.
# But, MySQL test fails.
# Sqlite3 and PostgreSQL pass.
# Is this MySQL bug?
def latest_changesets(path, rev, limit=10)
if path.blank?
changesets.find(:all, :include => :user, :limit => limit)
else
changes.find(:all, :include => {:changeset => :user},
:conditions => ["path = ?", path.with_leading_slash],
:order => "#{Changeset.table_name}.id DESC",
:limit => limit).collect(&:changeset)
changesets.find(:all, :include => :user,
:conditions => latest_changesets_cond(path, rev, limit),
:limit => limit, :order => "#{Changeset.table_name}.id DESC")
end
def latest_changesets_cond(path, rev, limit)
cond, args = [], []
if scm.branchmap.member? rev
# Mercurial named branch is *stable* in each revision.
# So, named branch can be stored in database.
# Mercurial provides *bookmark* which is equivalent with git branch.
# But, bookmark is not implemented.
cond << "#{Changeset.table_name}.scmid IN (?)"
# Revisions in root directory and sub directory are not equal.
# So, in order to get correct limit, we need to get all revisions.
# But, it is very heavy.
# Mercurial does not treat direcotry.
# So, "hg log DIR" is very heavy.
branch_limit = path.blank? ? limit : ( limit * 5 )
args << scm.nodes_in_branch(rev, :limit => branch_limit)
elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
cond << "#{Changeset.table_name}.id <= ?"
args << last.id
end
unless path.blank?
cond << "EXISTS (SELECT * FROM #{Change.table_name}
WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
AND (#{Change.table_name}.path = ?
OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
args << path.with_leading_slash
args << "#{path.with_leading_slash.gsub(/[%_\\]/) { |s| "\\#{s}" }}/%" << '\\'
end
[cond.join(' AND '), *args] unless cond.empty?
end
private :latest_changesets_cond
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
# latest revision in the repository
latest_revision = scm_info.lastrev
return if latest_revision.nil?
scm_revision = latest_revision.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
while (identifier_from <= scm_revision)
# loads changesets by batches of 100
identifier_to = [identifier_from + 99, scm_revision].min
revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
transaction do
revisions.each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
changeset.create_change(change)
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1
scm_rev = scm.info.lastrev.revision.to_i
db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
return unless db_rev < scm_rev # already up-to-date
logger.debug "Fetching changesets for repository #{url}" if logger
(db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
transaction do
scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
cs = Changeset.create(:repository => self,
:revision => re.revision,
:scmid => re.scmid,
:committer => re.author,
:committed_on => re.time,
:comments => re.message)
re.paths.each { |e| cs.create_change(e) }
end
end
end
self
end
end

@ -22,14 +22,22 @@ class Repository::Subversion < Repository
validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
def scm_adapter
def self.scm_adapter_class
Redmine::Scm::Adapters::SubversionAdapter
end
def self.scm_name
'Subversion'
end
def supports_directory_revisions?
true
end
def repo_log_encoding
'UTF-8'
end
def latest_changesets(path, rev, limit=10)
revisions = scm.revisions(path, rev, nil, :limit => limit)
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []

@ -65,6 +65,7 @@ class Setting < ActiveRecord::Base
UTF-16LE
EUC-JP
Shift_JIS
CP932
GB18030
GBK
ISCII91

@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -38,6 +38,11 @@ class TimeEntry < ActiveRecord::Base
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true, :message => :invalid
validates_length_of :comments, :maximum => 255, :allow_nil => true
named_scope :visible, lambda {|*args| {
:include => :project,
:conditions => Project.allowed_to_condition(args.first || User.current, :view_time_entries)
}}
def after_initialize
if new_record? && self.activity.nil?
@ -79,7 +84,9 @@ class TimeEntry < ActiveRecord::Base
(usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
end
# TODO: remove this method in 1.3.0
def self.visible_by(usr)
ActiveSupport::Deprecation.warn "TimeEntry.visible_by is deprecated and will be removed in Redmine 1.3.0. Use the visible scope instead."
with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
yield
end

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -74,6 +74,15 @@ class User < Principal
validates_confirmation_of :password, :allow_nil => true
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
named_scope :in_group, lambda {|group|
group_id = group.is_a?(Group) ? group.id : group.to_i
{ :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
}
named_scope :not_in_group, lambda {|group|
group_id = group.is_a?(Group) ? group.id : group.to_i
{ :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
}
def before_create
self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
true
@ -81,11 +90,14 @@ class User < Principal
def before_save
# update hashed_password if password was set
self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
if self.password && self.auth_source_id.blank?
salt_password(password)
end
end
def reload(*args)
@name = nil
@projects_by_role = nil
super
end
@ -119,7 +131,7 @@ class User < Principal
return nil unless user.auth_source.authenticate(login, password)
else
# authentication with local password
return nil unless User.hash_password(password) == user.hashed_password
return nil unless user.check_password?(password)
end
else
# user is not yet registered, try to authenticate with available sources
@ -198,13 +210,21 @@ class User < Principal
update_attribute(:status, STATUS_LOCKED)
end
# Returns true if +clear_password+ is the correct user's password, otherwise false
def check_password?(clear_password)
if auth_source_id.present?
auth_source.authenticate(self.login, clear_password)
else
User.hash_password(clear_password) == self.hashed_password
User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
end
end
# Generates a random salt and computes hashed_password for +clear_password+
# The hashed password is stored in the following form: SHA1(salt + SHA1(password))
def salt_password(clear_password)
self.salt = User.generate_salt
self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
end
# Does the backend storage allow this user to change their password?
def change_password_allowed?
@ -348,6 +368,23 @@ class User < Principal
!roles_for_project(project).detect {|role| role.member?}.nil?
end
# Returns a hash of user's projects grouped by roles
def projects_by_role
return @projects_by_role if @projects_by_role
@projects_by_role = Hash.new {|h,k| h[k]=[]}
memberships.each do |membership|
membership.roles.each do |role|
@projects_by_role[role] << membership.project if membership.project
end
end
@projects_by_role.each do |role, projects|
projects.uniq!
end
@projects_by_role
end
# Return true if the user is allowed to do the specified action on a specific context
# Action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
@ -470,6 +507,20 @@ class User < Principal
end
anonymous_user
end
# Salts all existing unsalted passwords
# It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
# This method is used in the SaltPasswords migration and is to be kept as is
def self.salt_unsalted_passwords!
transaction do
User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
next if user.hashed_password.blank?
salt = User.generate_salt
hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
end
end
end
protected
@ -486,6 +537,12 @@ class User < Principal
def self.hash_password(clear_password)
Digest::SHA1.hexdigest(clear_password || "")
end
# Returns a 128bits random salt as a hex string (32 chars long)
def self.generate_salt
ActiveSupport::SecureRandom.hex(16)
end
end
class AnonymousUser < User

@ -51,4 +51,7 @@ class UserPreference < ActiveRecord::Base
def comments_sorting; self[:comments_sorting] end
def comments_sorting=(order); self[:comments_sorting]=order end
def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
end

@ -46,10 +46,10 @@ class Wiki < ActiveRecord::Base
def find_page(title, options = {})
title = start_page if title.blank?
title = Wiki.titleize(title)
page = pages.first(:conditions => ["LOWER(title) LIKE LOWER(?)", title])
page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
if !page && !(options[:with_redirect] == false)
# search for a redirect
redirect = redirects.first(:conditions => ["LOWER(title) LIKE LOWER(?)", title])
redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
end
page

@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2009 Jean-Philippe Lang
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -41,6 +41,12 @@ class WikiPage < ActiveRecord::Base
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
validates_associated :content
# eager load information about last updates, without loading text
named_scope :with_updated_on, {
:select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
}
# Wiki pages that are protected by default
DEFAULT_PROTECTED_PAGES = %w(sidebar)
@ -121,6 +127,18 @@ class WikiPage < ActiveRecord::Base
content.text if content
end
def updated_on
unless @updated_on
if time = read_attribute(:updated_on)
# content updated_on was eager loaded with the page
@updated_on = Time.parse(time) rescue nil
else
@updated_on = content && content.updated_on
end
end
@updated_on
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)
@ -149,17 +167,13 @@ class WikiPage < ActiveRecord::Base
end
end
class WikiDiff
attr_reader :diff, :words, :content_to, :content_from
class WikiDiff < Redmine::Helpers::Diff
attr_reader :content_to, :content_from
def initialize(content_to, content_from)
@content_to = content_to
@content_from = content_from
@words = content_to.text.split(/(\s+)/)
@words = @words.select {|word| word != ' '}
words_from = content_from.text.split(/(\s+)/)
words_from = words_from.select {|word| word != ' '}
@diff = words_from.diff @words
super(content_to.text, content_from.text)
end
end

@ -21,16 +21,14 @@
<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
<div style="float:left;">
<%= link_to_remote(('&#171; ' + l(:label_previous)),
{:update => "content", :url => params.merge(:from => @date_to - @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
{:href => url_for(params.merge(:from => @date_to - @days - 1)),
:title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))}) %>
<%= link_to_content_update('&#171; ' + l(:label_previous),
params.merge(:from => @date_to - @days - 1),
:title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))) %>
</div>
<div style="float:right;">
<%= link_to_remote((l(:label_next) + ' &#187;'),
{:update => "content", :url => params.merge(:from => @date_to + @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
{:href => url_for(params.merge(:from => @date_to + @days - 1)),
:title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))}) unless @date_to >= Date.today %>
<%= link_to_content_update(l(:label_next) + ' &#187;',
params.merge(:from => @date_to + @days - 1),
:title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))) unless @date_to >= Date.today %>
</div>
&nbsp;
<% other_formats_links do |f| %>

@ -11,6 +11,7 @@
<label><%= l(:label_project) %>:</label>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
<%= link_to l(:button_clear), {:controller => 'admin', :action => 'projects'}, :class => 'icon icon-reload' %>
</fieldset>
<% end %>
&nbsp;

@ -69,4 +69,5 @@
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@project}: #{@board}") %>
<%= stylesheet_link_tag 'scm' %>
<% end %>

@ -1,10 +1,10 @@
<h2><%= l(:label_calendar) %></h2>
<h2><%= @query.new_record? ? l(:label_calendar) : h(@query.name) %></h2>
<% form_tag(calendar_path, :method => :put, :id => 'query_form') do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project%>
<fieldset id="filters" class="collapsible">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
</div>
</fieldset>
@ -20,16 +20,16 @@
<%= select_year(@year, :prefix => "year", :discard_type => true) %>
<%= link_to_remote l(:button_apply),
{ :url => { :set_filter => (@query.new_record? ? 1 : nil) },
{ :url => { :set_filter => 1 },
:update => "content",
:with => "Form.serialize('query_form')"
}, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear),
{ :url => { :project_id => @project, :set_filter => (@query.new_record? ? 1 : nil) },
{ :url => { :project_id => @project, :set_filter => 1 },
:method => :put,
:update => "content",
}, :class => 'icon icon-reload' if @query.new_record? %>
}, :class => 'icon icon-reload' %>
</p>
<% end %>

@ -1,66 +1,56 @@
<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%>
<% diff.each do |table_file| -%>
<div class="autoscroll">
<% if diff_type == 'sbs' -%>
<% if diff.diff_type == 'sbs' -%>
<table class="filecontent">
<thead>
<tr><th colspan="4" class="filename"><%=to_utf8 table_file.file_name %></th></tr>
</thead>
<tbody>
<% prev_line_left, prev_line_right = nil, nil -%>
<% table_file.keys.sort.each do |key| -%>
<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
<% table_file.each_line do |spacing, line| -%>
<% if spacing -%>
<tr class="spacing">
<th class="line-num">...</th><td></td><th class="line-num">...</th><td></td>
<th class="line-num">...</th><td></td><th class="line-num">...</th><td></td>
</tr>
<% end -%>
<tr>
<th class="line-num"><%= table_file[key].nb_line_left %></th>
<td class="line-code <%= table_file[key].type_diff_left %>">
<pre><%=to_utf8 table_file[key].line_left %></pre>
<th class="line-num"><%= line.nb_line_left %></th>
<td class="line-code <%= line.type_diff_left %>">
<pre><%=to_utf8 line.html_line_left %></pre>
</td>
<th class="line-num"><%= table_file[key].nb_line_right %></th>
<td class="line-code <%= table_file[key].type_diff_right %>">
<pre><%=to_utf8 table_file[key].line_right %></pre>
<th class="line-num"><%= line.nb_line_right %></th>
<td class="line-code <%= line.type_diff_right %>">
<pre><%=to_utf8 line.html_line_right %></pre>
</td>
</tr>
<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%>
<% end -%>
</tbody>
</table>
<% else -%>
<table class="filecontent syntaxhl">
<table class="filecontent">
<thead>
<tr><th colspan="3" class="filename"><%=to_utf8 table_file.file_name %></th></tr>
</thead>
<tbody>
<% prev_line_left, prev_line_right = nil, nil -%>
<% table_file.keys.sort.each do |key, line| %>
<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
<% table_file.each_line do |spacing, line| %>
<% if spacing -%>
<tr class="spacing">
<th class="line-num">...</th><th class="line-num">...</th><td></td>
<th class="line-num">...</th><th class="line-num">...</th><td></td>
</tr>
<% end -%>
<tr>
<th class="line-num"><%= table_file[key].nb_line_left %></th>
<th class="line-num"><%= table_file[key].nb_line_right %></th>
<% if table_file[key].line_left.empty? -%>
<td class="line-code <%= table_file[key].type_diff_right %>">
<pre><%=to_utf8 table_file[key].line_right %></pre>
<th class="line-num"><%= line.nb_line_left %></th>
<th class="line-num"><%= line.nb_line_right %></th>
<td class="line-code <%= line.type_diff %>">
<pre><%=to_utf8 line.html_line %></pre>
</td>
<% else -%>
<td class="line-code <%= table_file[key].type_diff_left %>">
<pre><%=to_utf8 table_file[key].line_left %></pre>
</td>
<% end -%>
</tr>
<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%>
<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%>
<% end -%>
</tbody>
</table>
<% end -%>
</div>
<% end -%>

@ -18,33 +18,41 @@ function toggle_custom_field_format() {
Element.hide(p_length.parentNode);
Element.hide(p_regexp.parentNode);
if (p_searchable) Element.show(p_searchable.parentNode);
Element.show(p_values);
Element.show(p_values.parentNode);
break;
case "bool":
p_default.setAttribute('type','checkbox');
Element.hide(p_length.parentNode);
Element.hide(p_regexp.parentNode);
if (p_searchable) Element.hide(p_searchable.parentNode);
Element.hide(p_values);
Element.hide(p_values.parentNode);
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);
Element.hide(p_values.parentNode);
break;
case "float":
case "int":
Element.show(p_length.parentNode);
Element.show(p_regexp.parentNode);
if (p_searchable) Element.hide(p_searchable.parentNode);
Element.hide(p_values);
Element.hide(p_values.parentNode);
break;
case "user":
case "version":
Element.hide(p_length.parentNode);
Element.hide(p_regexp.parentNode);
if (p_searchable) Element.hide(p_searchable.parentNode);
Element.hide(p_values.parentNode);
Element.hide(p_default.parentNode);
break;
default:
Element.show(p_length.parentNode);
Element.show(p_regexp.parentNode);
if (p_searchable) Element.show(p_searchable.parentNode);
Element.hide(p_values);
Element.hide(p_values.parentNode);
break;
}
}
@ -54,16 +62,16 @@ function toggle_custom_field_format() {
<div class="box">
<p><%= f.text_field :name, :required => true %></p>
<p><%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();",
<p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :onchange => "toggle_custom_field_format();",
:disabled => !@custom_field.new_record? %></p>
<p><label for="custom_field_min_length"><%=l(:label_min_max_length)%></label>
<%= f.text_field :min_length, :size => 5, :no_label => true %> -
<%= f.text_field :max_length, :size => 5, :no_label => true %><br>(<%=l(:text_min_max_length_info)%>)</p>
<p><%= f.text_field :regexp, :size => 50 %><br>(<%=l(:text_regexp_info)%>)</p>
<p id="custom_field_possible_values"><%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"),
:cols => 20,
:rows => 15 %>
<br /><em><%= l(:text_custom_field_possible_values_info) %></em></p>
<p>
<%= f.text_area :possible_values, :value => @custom_field.possible_values.to_a.join("\n"), :rows => 15 %>
<br /><em><%= l(:text_custom_field_possible_values_info) %></em>
</p>
<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
<%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
</div>

@ -1,11 +1,11 @@
<% @gantt.view = self %>
<h2><%= l(:label_gantt) %></h2>
<h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
<% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %>
<%= hidden_field_tag('project_id', @project.to_param) if @project%>
<fieldset id="filters" class="collapsible">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
<div>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
<%= render :partial => 'queries/filters', :locals => {:query => @query} %>
</div>
</fieldset>
@ -23,16 +23,16 @@
<%= hidden_field_tag 'zoom', @gantt.zoom %>
<%= link_to_remote l(:button_apply),
{ :url => { :set_filter => (@query.new_record? ? 1 : nil) },
{ :url => { :set_filter => 1 },
:update => "content",
:with => "Form.serialize('query_form')"
}, :class => 'icon icon-checked' %>
<%= link_to_remote l(:button_clear),
{ :url => { :project_id => @project, :set_filter => (@query.new_record? ? 1 : nil) },
{ :url => { :project_id => @project, :set_filter => 1 },
:method => :put,
:update => "content",
}, :class => 'icon icon-reload' if @query.new_record? %>
}, :class => 'icon icon-reload' %>
</p>
<% end %>
@ -60,7 +60,7 @@ end
# Width of the entire chart
g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
@gantt.render(:top => headers_height + 8, :zoom => zoom, :g_width => g_width)
@gantt.render(:top => headers_height + 8, :zoom => zoom, :g_width => g_width, :subject_width => subject_width)
g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max
t_height = g_height + headers_height
@ -178,8 +178,8 @@ if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %>
<table width="100%">
<tr>
<td align="left"><%= link_to_remote ('&#171; ' + l(:label_previous)), {:url => @gantt.params_previous, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %></td>
<td align="right"><%= link_to_remote (l(:label_next) + ' &#187;'), {:url => @gantt.params_next, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %></td>
<td align="left"><%= link_to_content_update('&#171; ' + l(:label_previous), params.merge(@gantt.params_previous)) %></td>
<td align="right"><%= link_to_content_update(l(:label_next) + ' &#187;', params.merge(@gantt.params_next)) %></td>
</tr>
</table>

@ -24,7 +24,7 @@
</div>
<div class="splitcontentright">
<% users = User.active.find(:all, :limit => 100) - @group.users %>
<% users = User.active.not_in_group(@group).all(:limit => 100) %>
<% if users.any? %>
<% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
<fieldset><legend><%=l(:label_user_new)%></legend>

@ -3,7 +3,7 @@
<p><%= link_to_revision(changeset, changeset.project,
:text => "#{l(:label_revision)} #{changeset.format_identifier}") %><br />
<span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p>
<div class="changeset-changes">
<div class="wiki">
<%= textilizable(changeset, :comments) %>
</div>
</div>

@ -17,3 +17,5 @@
</div>
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
<% end %>
<% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, issue.project) || User.current.allowed_to?(:edit_own_issue_notes, issue.project) %>

@ -20,6 +20,7 @@
<td colspan="<%= query.columns.size + 2 %>">
<span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
<%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %>
</td>
</tr>
<% previous_group = group %>

@ -13,11 +13,5 @@
<% end %>
<%= call_hook(:view_issues_sidebar_planning_bottom) %>
<% unless sidebar_queries.empty? -%>
<h3><%= l(:label_query_plural) %></h3>
<% sidebar_queries.each do |query| -%>
<%= link_to(h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query) %><br />
<% end -%>
<%= render_sidebar_queries %>
<%= call_hook(:view_issues_sidebar_queries_bottom) %>
<% end -%>

@ -55,6 +55,14 @@
</div>
<div class="splitcontentright">
<% if @project && User.current.allowed_to?(:manage_subtasks, @project) %>
<p>
<label><%= l(:field_parent_issue) %></label>
<%= text_field_tag 'issue[parent_issue_id]', '', :size => 10 %>
</p>
<div id="parent_issue_candidates" class="autocomplete"></div>
<%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:project_id => @project) }')" %>
<% end %>
<p>
<label><%= l(:field_start_date) %></label>
<%= text_field_tag 'issue[start_date]', '', :size => 10 %><%= calendar_for('issue_start_date') %>

@ -1,8 +1,21 @@
<% 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) %>
<%= text_area_tag :notes, @journal.notes,
:id => "journal_#{@journal.id}_notes",
:class => 'wiki-edit',
:rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
<%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
<p><%= submit_tag l(:button_save) %>
<%= link_to_remote l(:label_preview),
{ :url => preview_issue_path(:project_id => @project, :id => @journal.issue),
:method => 'post',
:update => "journal_#{@journal.id}_preview",
:with => "Form.serialize('journal-#{@journal.id}-form')",
:complete => "Element.scrollTo('journal_#{@journal.id}_preview')"
}, :accesskey => accesskey(:preview) %>
|
<%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
"Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
<div id="journal_<%= @journal.id %>_preview" class="wiki"></div>
<% end %>
<%= wikitoolbar_for "journal_#{@journal.id}_notes" %>

@ -0,0 +1,10 @@
<h2><%=h @issue.tracker %> #<%= @issue.id %></h2>
<p><%= authoring @journal.created_on, @journal.user, :label => :label_updated_time_by %></p>
<div class="text-diff">
<%= simple_format_without_paragraph @diff.to_html %>
</div>
<p><%= link_to l(:button_back), issue_path(@issue), :onclick => 'history.back(); return false;' %></p>
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>

@ -5,12 +5,12 @@
<title><%=h html_title %></title>
<meta name="description" content="<%= Redmine::Info.app_name %>" />
<meta name="keywords" content="issue,bug,tracker" />
<%= csrf_meta_tag %>
<%= favicon %>
<%= stylesheet_link_tag 'application', :media => 'all' %>
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
<%= javascript_include_tag :defaults %>
<%= javascript_heads %>
<%= heads_for_theme %>
<%= heads_for_wiki_formatter %>
<!--[if IE 6]>
<style type="text/css">
* html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
@ -21,7 +21,7 @@
<!-- page specific tags -->
<%= yield :header_tags -%>
</head>
<body class="<%= body_css_classes %>">
<body class="<%=h body_css_classes %>">
<div id="wrapper">
<div id="wrapper2">
<div id="top-menu">
@ -29,10 +29,11 @@
<%= render_menu :account_menu -%>
</div>
<%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
<%= render_menu :top_menu -%>
<%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
</div>
<div id="header">
<% if User.current.logged? || !Setting.login_required? %>
<div id="quick-search">
<% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
<%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
@ -41,6 +42,7 @@
<% end %>
<%= render_project_jump_box %>
</div>
<% end %>
<h1><%= page_header_title %></h1>

@ -7,7 +7,7 @@
<li><%=l(:field_assigned_to)%>: <%=h issue.assigned_to %></li>
<li><%=l(:field_category)%>: <%=h issue.category %></li>
<li><%=l(:field_fixed_version)%>: <%=h issue.fixed_version %></li>
<% issue.custom_values.each do |c| %>
<% issue.custom_field_values.each do |c| %>
<li><%=h c.custom_field.name %>: <%=h show_value(c) %></li>
<% end %>
</ul>

@ -7,7 +7,7 @@
<%=l(:field_assigned_to)%>: <%= issue.assigned_to %>
<%=l(:field_category)%>: <%= issue.category %>
<%=l(:field_fixed_version)%>: <%= issue.fixed_version %>
<% issue.custom_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %>
<% issue.custom_field_values.each do |c| %><%= c.custom_field.name %>: <%= show_value(c) %>
<% end %>
<%= issue.description %>

@ -0,0 +1,5 @@
<h1><%= link_to(h(@news.title), @news_url) %></h1>
<p><%= l(:text_user_wrote, :value => h(@comment.author)) %></p>
<%= textilizable @comment, :comments, :only_path => false %>

@ -0,0 +1,6 @@
<%= @news.title %>
<%= @news_url %>
<%= l(:text_user_wrote, :value => @comment.author) %>
<%= @comment.comments %>

@ -12,3 +12,7 @@
}, :accesskey => accesskey(:preview) %>
<% end %>
<div id="preview" class="wiki"></div>
<% content_for :header_tags do %>
<%= stylesheet_link_tag 'scm' %>
<% end %>

@ -1,4 +1,5 @@
<div class="contextual">
<%= watcher_tag(@news, User.current) %>
<%= link_to(l(:button_edit),
edit_news_path(@news),
:class => 'icon icon-edit',

@ -64,7 +64,7 @@
</div>
<% content_for :sidebar do %>
<% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>
<% if @total_hours.present? %>
<h3><%= l(:label_spent_time) %></h3>
<p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
<p>

@ -10,7 +10,7 @@
<input type="button" value="&#8592;"
onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
</td>
<td><%= select_tag 'query[column_names][]',
<td><%= select_tag 'c[]',
options_for_select(query.columns.collect {|column| [column.caption, column.name]}),
:id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px" %>
</td>

@ -21,10 +21,14 @@ function toggle_filter(field) {
if (check_box.checked) {
Element.show("operators_" + field);
Form.Element.enable("operators_" + field);
Form.Element.enable("values_" + field);
toggle_operator(field);
} else {
Element.hide("operators_" + field);
Element.hide("div_values_" + field);
Form.Element.disable("operators_" + field);
Form.Element.disable("values_" + field);
}
}
@ -77,26 +81,26 @@ Event.observe(document,"dom:loaded", apply_filters_observer);
options = filter[1] %>
<tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
<td style="width:200px;">
<%= check_box_tag 'fields[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
<%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
<label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
</td>
<td style="width:150px;">
<%= select_tag "operators[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %>
<%= select_tag "op[#{field}]", options_for_select(operators_for_select(options[:type]), query.operator_for(field)), :id => "operators_#{field}", :onchange => "toggle_operator('#{field}');", :class => "select-small", :style => "vertical-align: top;" %>
</td>
<td>
<div id="div_values_<%= field %>" style="display:none;">
<% case options[:type]
when :list, :list_optional, :list_status, :list_subprojects %>
<select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="values[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;">
<select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="v[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;">
<%= options_for_select options[:values], query.values_for(field) %>
</select>
<%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %>
<% when :date, :date_past %>
<%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %>
<%= text_field_tag "v[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %>
<% when :string, :text %>
<%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %>
<%= text_field_tag "v[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 30, :class => "select-small" %>
<% when :integer %>
<%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %>
<%= text_field_tag "v[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %>
<% end %>
</div>
<script type="text/javascript">toggle_filter('<%= field %>');</script>
@ -114,4 +118,4 @@ Event.observe(document,"dom:loaded", apply_filters_observer);
</td>
</tr>
</table>
<%= hidden_field_tag 'fields[]', '' %>
<%= hidden_field_tag 'f[]', '' %>

@ -10,12 +10,19 @@ dirs.each do |dir|
link_path << '/' unless link_path.empty?
link_path << "#{dir}"
%>
/ <%= link_to h(dir), :action => 'show', :id => @project, :path => to_path_param(link_path), :rev => @rev %>
/ <%= link_to h(dir), :action => 'show', :id => @project,
:path => to_path_param(link_path), :rev => @rev %>
<% end %>
<% if filename %>
/ <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
/ <%= link_to h(filename),
:action => 'changes', :id => @project,
:path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
<% end %>
<%= "@ #{h format_revision(@changeset)}" if @changeset %>
<%
# @rev is revsion or Git and Mercurial branch or tag.
# For Mercurial *tip*, @rev and @changeset are nil.
rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
%>
<%= "@ #{h rev_text}" unless rev_text.blank? %>
<% html_title(with_leading_slash(path)) -%>

@ -1,25 +1,27 @@
<% @entries.each do |entry| %>
<% tr_id = Digest::MD5.hexdigest(entry.path)
depth = params[:depth].to_i %>
<% ent_path = replace_invalid_utf8(entry.path) %>
<% ent_name = replace_invalid_utf8(entry.name) %>
<tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= entry.kind %>">
<td style="padding-left: <%=18 * depth%>px;" class="filename">
<% if entry.is_dir? %>
<span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
:method => :get,
<span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(ent_path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
:method => :get,
:update => { :success => tr_id },
:position => :after,
:success => "scmEntryLoaded('#{tr_id}')",
:condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span>
<% end %>
<%= link_to h(entry.name),
{:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev},
:class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(entry.name)}")%>
<%= link_to h(ent_name),
{:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :path => to_path_param(ent_path), :rev => @rev},
:class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
</td>
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
<% changeset = @project.repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
<td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
<td class="author"><%= changeset.nil? ? h(replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : changeset.author if entry.lastrev %></td>
<td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
</tr>
<% end %>

@ -12,23 +12,49 @@
<%= render_properties(@properties) %>
<% if @changesets && !@changesets.empty? && authorize_for('repositories', 'revisions') %>
<% if authorize_for('repositories', 'revisions') %>
<% if @changesets && !@changesets.empty? %>
<h3><%= l(:label_latest_revision_plural) %></h3>
<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => nil }%>
<%= render :partial => 'revisions',
:locals => {:project => @project, :path => @path,
:revisions => @changesets, :entry => nil }%>
<% end %>
<p>
<%
has_branches = (!@repository.branches.nil? && @repository.branches.length > 0)
sep = ''
%>
<% if @repository.supports_all_revisions? && @path.blank? %>
<%= link_to l(:label_view_all_revisions), :action => 'revisions', :id => @project %>
<% sep = '|' %>
<% end %>
<%
if @repository.supports_directory_revisions? &&
( has_branches || !@path.blank? || !@rev.blank? )
%>
<%= sep %>
<%=
link_to l(:label_view_revisions),
:action => 'changes',
:path => to_path_param(@path),
:id => @project,
:rev => @rev
%>
<% end %>
</p>
<% if @path.blank? %>
<p><%= link_to l(:label_view_all_revisions), :action => 'revisions', :id => @project %></p>
<% else %>
<p><%= link_to l(:label_view_revisions), :action => 'changes', :path => to_path_param(@path), :id => @project %></p>
<% end %>
<% if true # @path.blank? %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(
:atom, params.merge(
{:format => 'atom', :action => 'revisions',
:id => @project, :page => nil, :key => User.current.rss_key})) %>
<% end %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :action => 'revisions', :id => @project, :page => nil, :key => User.current.rss_key})) %>
<% end %>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => {:action => 'revisions', :id => @project, :key => User.current.rss_key} %>
<% end %>
<% other_formats_links do |f| %>
<%= f.link_to 'Atom', :url => {:action => 'revisions', :id => @project, :key => User.current.rss_key} %>
<% end %>
<% end %>
<% end %>
<% content_for :header_tags do %>

@ -2,6 +2,7 @@
<% form_tag({:action => 'report'}, :id => 'permissions_form') do %>
<%= hidden_field_tag 'permissions[0]', '', :id => nil %>
<div class="autoscroll">
<table class="list">
<thead>
<tr>
@ -21,7 +22,7 @@
<% unless mod.blank? %>
<tr class="group open">
<td colspan="<%= @roles.size + 1 %>">
<span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
<span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
<%= l_or_humanize(mod, :prefix => 'project_module_') %>
</td>
</tr>
@ -45,6 +46,7 @@
<% end %>
</tbody>
</table>
</div>
<p><%= check_all_links 'permissions_form' %></p>
<p><%= submit_tag l(:button_save) %></p>
<% end %>

@ -35,16 +35,12 @@
<p><center>
<% if @pagination_previous_date %>
<%= link_to_remote ('&#171; ' + l(:label_previous)),
{:update => :content,
:url => params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))
}, :href => url_for(params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
<%= link_to_content_update('&#171; ' + l(:label_previous),
params.merge(:previous => 1, :offset => @pagination_previous_date.strftime("%Y%m%d%H%M%S"))) %>&nbsp;
<% end %>
<% if @pagination_next_date %>
<%= link_to_remote (l(:label_next) + ' &#187;'),
{:update => :content,
:url => params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))
}, :href => url_for(params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
<%= link_to_content_update(l(:label_next) + ' &#187;',
params.merge(:previous => nil, :offset => @pagination_next_date.strftime("%Y%m%d%H%M%S"))) %>
<% end %>
</center></p>

@ -5,7 +5,7 @@
<p><%= setting_select :default_language, lang_options_for_select(false) %></p>
<p><%= setting_select :start_of_week, [[day_name(1),'1'], [day_name(7),'7']], :blank => :label_language_based %></p>
<p><%= setting_select :start_of_week, [[day_name(1),'1'], [day_name(6),'6'], [day_name(7),'7']], :blank => :label_language_based %></p>
<p><%= setting_select :date_format, Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, :blank => :label_language_based %></p>
@ -15,7 +15,7 @@
<p><%= setting_check_box :gravatar_enabled %></p>
<p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", "retro"]], :blank => :label_none %></p>
<p><%= setting_select :gravatar_default, [["Wavatars", 'wavatar'], ["Identicons", 'identicon'], ["Monster ids", 'monsterid'], ["Retro", 'retro'], ["Mystery man", 'mm']], :blank => :label_none %></p>
</div>
<%= submit_tag l(:button_save) %>

@ -18,8 +18,6 @@
<p><%= setting_text_field :repositories_encodings, :size => 60 %><br />
<em><%= l(:text_comma_separated) %></em></p>
<p><%= setting_select :commit_logs_encoding, Setting::ENCODINGS %></p>
<p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
</div>

@ -6,13 +6,10 @@
<h2><%= l(:label_spent_time) %></h2>
<% form_remote_tag(:url => {}, :html => {:method => :get, :id => 'query_form'}, :method => :get, :update => 'content') do %>
<% form_tag({:controller => 'time_entry_reports', :action => 'report', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
<% @criterias.each do |criteria| %>
<%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
<% end %>
<%# TODO: get rid of the project_id field, that should already be in the URL %>
<%= hidden_field_tag('project_id', params[:project_id]) if @project %>
<%= hidden_field_tag('issue_id', params[:issue_id]) if @issue %>
<%= render :partial => 'timelog/date_range' %>
<p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
@ -22,14 +19,11 @@
:onchange => "this.form.onsubmit();" %>
<%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l_or_humanize(@available_criterias[k][:label]), k]}),
:onchange => "this.form.onsubmit();",
:onchange => "this.form.submit();",
:style => 'width: 200px',
:id => nil,
:disabled => (@criterias.length >= 3)) %>
<%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
:method => :get,
:update => 'content'
}, :class => 'icon icon-reload' %></p>
<%= link_to l(:button_clear), {:project_id => @project, :issue_id => @issue, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns}, :class => 'icon icon-reload' %></p>
<% end %>
<% unless @criterias.empty? %>

@ -2,27 +2,24 @@
<legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
<div>
<p>
<%= radio_button_tag 'period_type', '1', !@free_period %>
<%= radio_button_tag 'period_type', '1', !@free_period, :onclick => 'Form.Element.disable("from");Form.Element.disable("to");Form.Element.enable("period");' %>
<%= select_tag 'period', options_for_period_select(params[:period]),
:onchange => 'this.form.onsubmit();',
:onfocus => '$("period_type_1").checked = true;' %>
:onchange => 'this.form.submit();',
:onfocus => '$("period_type_1").checked = true;',
:disabled => @free_period %>
</p>
<p>
<%= radio_button_tag 'period_type', '2', @free_period %>
<%= radio_button_tag 'period_type', '2', @free_period, :onclick => 'Form.Element.enable("from");Form.Element.enable("to");Form.Element.disable("period");' %>
<span onclick="$('period_type_2').checked = true;">
<%= l(:label_date_from_to, :start => (text_field_tag('from', @from, :size => 10) + calendar_for('from')),
:end => (text_field_tag('to', @to, :size => 10) + calendar_for('to'))) %>
<%= l(:label_date_from_to, :start => (text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
:end => (text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))) %>
</span>
</p>
</div>
</fieldset>
<p class="buttons">
<%= link_to_remote l(:button_apply),
{ :url => { },
:update => "content",
:with => "Form.serialize('query_form')",
:method => :get
}, :class => 'icon icon-checked' %>
<%= link_to_function l(:button_apply), '$("query_form").submit()', :class => 'icon icon-checked' %>
<%= link_to l(:button_clear), {:controller => controller_name, :action => action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
</p>
<div class="tabs">

@ -6,11 +6,7 @@
<h2><%= l(:label_spent_time) %></h2>
<% form_remote_tag( :url => {}, :html => {:method => :get, :id => 'query_form'}, :method => :get, :update => 'content' ) do %>
<%# TOOD: remove the project_id and issue_id hidden fields, that information is
already in the URI %>
<%= hidden_field_tag('project_id', params[:project_id]) if @project %>
<%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
<% form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
<%= render :partial => 'date_range' %>
<% end %>

@ -2,5 +2,6 @@
<p><%= pref_fields.check_box :hide_mail %></p>
<p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
<p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
<p><%= pref_fields.check_box :warn_on_leaving_unsaved %></p>
<% end %>

@ -8,9 +8,16 @@
<fieldset><legend><%= l(:label_filter_plural) %></legend>
<label><%= l(:field_status) %>:</label>
<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
<% if @groups.present? %>
<label><%= l(:label_group) %>:</label>
<%= select_tag 'group_id', '<option></option>' + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;" %>
<% end %>
<label><%= l(:label_user) %>:</label>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
<%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
</fieldset>
<% end %>
&nbsp;

@ -19,8 +19,8 @@
:action => 'index',
:project_id => version.project,
:set_filter => 1,
:fixed_version_id => version,
"#{criteria}_id" => count[:group]} %>
:status_id => '*',
:fixed_version_id => version}.merge("#{criteria}_id".to_sym => count[:group]) %>
</td>
<td width="240px">
<%= progress_bar((count[:closed].to_f / count[:total])*100,

@ -12,6 +12,6 @@
<em>(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>)</em>
</p>
<hr />
<%= html_diff(@diff) %>
<div class="text-diff">
<%= simple_format_without_paragraph @diff.to_html %>
</div>

@ -8,7 +8,7 @@
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>
<%= render_page_hierarchy(@pages_by_parent_id) %>
<%= render_page_hierarchy(@pages_by_parent_id, nil, :timestamp => true) %>
<% content_for :sidebar do %>
<%= render :partial => 'sidebar' %>

@ -0,0 +1,40 @@
<table class="list transitions-<%= name %>">
<thead>
<tr>
<th align="left">
<%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
<%=l(:label_current_status)%>
</th>
<th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
</tr>
<tr>
<td></td>
<% for new_status in @statuses %>
<td width="<%= 75 / @statuses.size %>%" align="center">
<%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
<%=h new_status.name %>
</td>
<% end %>
</tr>
</thead>
<tbody>
<% for old_status in @statuses %>
<tr class="<%= cycle("odd", "even") %>">
<td>
<%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
<%=h old_status.name %>
</td>
<% for new_status in @statuses -%>
<td align="center">
<%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id},
:class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
</td>
<% end -%>
</tr>
<% end %>
</tbody>
</table>

@ -20,54 +20,31 @@
</p>
<% end %>
<% if @tracker && @role && @statuses.any? %>
<% form_tag({}, :id => 'workflow_form' ) do %>
<%= hidden_field_tag 'tracker_id', @tracker.id %>
<%= hidden_field_tag 'role_id', @role.id %>
<div class="autoscroll">
<table class="list">
<thead>
<tr>
<th align="left"><%=l(:label_current_status)%></th>
<th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
</tr>
<tr>
<td></td>
<% for new_status in @statuses %>
<td width="<%= 75 / @statuses.size %>%" align="center">
<%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.new-status-#{new_status.id}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
<%= new_status.name %>
</td>
<% end %>
</tr>
</thead>
<tbody>
<% for old_status in @statuses %>
<tr class="<%= cycle("odd", "even") %>">
<td>
<%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.old-status-#{old_status.id}')",
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
<%= old_status.name %>
</td>
<% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
<% for new_status in @statuses -%>
<td align="center">
<%= check_box_tag "issue_status[#{ old_status.id }][]", new_status.id, new_status_ids_allowed.include?(new_status.id),
:class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
</td>
<% end -%>
</tr>
<% end %>
</tbody>
</table>
</div>
<p><%= check_all_links 'workflow_form' %></p>
<%= submit_tag l(:button_save) %>
<% end %>
<% form_tag({}, :id => 'workflow_form' ) do %>
<%= hidden_field_tag 'tracker_id', @tracker.id %>
<%= hidden_field_tag 'role_id', @role.id %>
<div class="autoscroll">
<%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
<fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
<legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
<div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
<%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
</div>
</fieldset>
<%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
<fieldset class="collapsible" style="padding: 0;">
<legend onclick="toggleFieldset(this);"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
<div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
<%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
</div>
</fieldset>
<%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
</div>
<%= submit_tag l(:button_save) %>
<% end %>
<% end %>
<% html_title(l(:label_workflow)) -%>

@ -24,7 +24,7 @@
<td><%= h tracker %></td>
<% roles.each do |role, count| -%>
<td align="center">
<%= link_to((count > 1 ? count : image_tag('false.png')), {:action => 'edit', :role_id => role, :tracker_id => tracker}, :title => l(:button_edit)) %>
<%= link_to((count > 0 ? count : image_tag('false.png')), {:action => 'edit', :role_id => role, :tracker_id => tracker}, :title => l(:button_edit)) %>
</td>
<% end -%>
</tr>

@ -136,6 +136,20 @@ default:
scm_bazaar_command:
scm_darcs_command:
# Key used to encrypt sensitive data in the database (SCM and LDAP passwords).
# If you don't want to enable data encryption, just leave it blank.
# WARNING: losing/changing this key will make encrypted data unreadable.
#
# If you want to encrypt existing passwords in your database:
# * set the cipher key here in your configuration file
# * encrypt data using 'rake db:encrypt RAILS_ENV=production'
#
# If you have encrypted data and want to change this key, you have to:
# * decrypt data using 'rake db:decrypt RAILS_ENV=production' first
# * change the cipher key here in your configuration file
# * encrypt data using 'rake db:encrypt RAILS_ENV=production'
database_cipher_key:
# specific configuration options for production environment
# that overrides the default ones
production:

@ -5,7 +5,7 @@
# ENV['RAILS_ENV'] ||= 'production'
# Specifies gem version of Rails to use when vendor/rails is not present
RAILS_GEM_VERSION = '2.3.5' unless defined? RAILS_GEM_VERSION
RAILS_GEM_VERSION = '2.3.11' unless defined? RAILS_GEM_VERSION
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
@ -24,7 +24,7 @@ Rails::Initializer.run do |config|
# config.frameworks -= [ :action_web_service, :action_mailer ]
# Add additional load paths for sweepers
config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )
config.autoload_paths += %W( #{RAILS_ROOT}/app/sweepers )
# Force all environments to use the same logger level
# (by default production uses :info, the others :debug)
@ -36,7 +36,7 @@ Rails::Initializer.run do |config|
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector
config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer
config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
# Make Active Record use UTC-base instead of local time
# config.active_record.default_timezone = :utc

@ -79,16 +79,12 @@ end
ActionMailer::Base.send :include, AsynchronousMailer
# TODO: Hack to support i18n 4.x on Rails 2.3.5. Remove post 2.3.6.
# See http://www.redmine.org/issues/6428 and http://www.redmine.org/issues/5608
module I18n
module Backend
module Base
def warn_syntax_deprecation!(*args)
return if @skip_syntax_deprecation
ActiveSupport::Deprecation.warn "The {{key}} interpolation syntax in I18n messages is deprecated and will be removed in ChiliProject 2.0. Please use %{key} instead. See the notice at https://www.chiliproject.org/boards/2/topics/243 for more information."
@skip_syntax_deprecation = true
end
# TMail::Unquoter.convert_to_with_fallback_on_iso_8859_1 introduced in TMail 1.2.7
# triggers a test failure in test_add_issue_with_japanese_keywords(MailHandlerTest)
module TMail
class Unquoter
class << self
alias_method :convert_to, :convert_to_without_fallback_on_iso_8859_1
end
end
end

@ -302,6 +302,7 @@ bg:
field_assigned_to_role: Assignee's role
field_text: Текстово поле
field_visible: Видим
field_warn_on_leaving_unsaved: Предупреди ме, когато напускам страница с незаписано съдържание
setting_app_title: Заглавие
setting_app_subtitle: Описание
@ -535,6 +536,7 @@ bg:
label_news_latest: Последни новини
label_news_view_all: Виж всички
label_news_added: Добавена новина
label_news_comment_added: Добавен коментар към новина
label_settings: Настройки
label_overview: Общ изглед
label_version: Версия
@ -593,6 +595,7 @@ bg:
label_query: Потребителска справка
label_query_plural: Потребителски справки
label_query_new: Нова заявка
label_my_queries: Моите заявки
label_filter_add: Добави филтър
label_filter_plural: Филтри
label_equals: е
@ -857,6 +860,7 @@ bg:
text_are_you_sure: Сигурни ли сте?
text_are_you_sure_with_children: Изтриване на задачата и нейните подзадачи?
text_journal_changed: "%{label} променен от %{old} на %{new}"
text_journal_changed_no_detail: "%{label} променен"
text_journal_set_to: "%{label} установен на %{value}"
text_journal_deleted: "%{label} изтрит (%{old})"
text_journal_added: "Добавено %{label} %{value}"
@ -907,6 +911,7 @@ bg:
text_own_membership_delete_confirmation: "Вие сте на път да премахнете някои или всички ваши разрешения и е възможно след това да не можете да редактирате този проект.\nСигурен ли сте, че искате да продължите?"
text_zoom_in: Увеличаване
text_zoom_out: Намаляване
text_warn_on_leaving_unsaved: Страницата съдържа незаписано съдържание, което може да бъде загубено, ако я напуснете.
default_role_manager: Мениджър
default_role_developer: Разработчик
@ -945,3 +950,8 @@ bg:
label_cvs_path: CVSROOT
label_git_path: Path to .git directory
label_mercurial_path: Root directory
label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
button_expand_all: Expand all
button_collapse_all: Collapse all
label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
field_effective_date: Due date

@ -959,3 +959,13 @@ bs:
label_cvs_path: CVSROOT
label_git_path: Path to .git directory
label_mercurial_path: Root directory
label_my_queries: My custom queries
label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
text_journal_changed_no_detail: "%{label} updated"
button_expand_all: Expand all
button_collapse_all: Collapse all
label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
field_effective_date: Due date
label_news_comment_added: Comment added to a news
field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text
text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save