summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2008-04-28 10:36:12 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2008-04-28 10:36:12 +0000
commit10191813ec9b5671ffd58008fcc832379f811009 (patch)
tree1238124192c80d6d53ccb907580394c5bd1b9f96
parentbe071deae29935ebb4dbcd3a7e445e290061b883 (diff)
downloadredmine-10191813ec9b5671ffd58008fcc832379f811009.tar.gz
redmine-10191813ec9b5671ffd58008fcc832379f811009.zip
Merged r1307 to r1369 from trunk.
git-svn-id: http://redmine.rubyforge.org/svn/branches/0.7-stable@1370 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r--app/controllers/account_controller.rb2
-rw-r--r--app/controllers/application.rb1
-rw-r--r--app/controllers/issues_controller.rb18
-rw-r--r--app/controllers/projects_controller.rb17
-rw-r--r--app/controllers/queries_controller.rb37
-rw-r--r--app/controllers/repositories_controller.rb27
-rw-r--r--app/controllers/timelog_controller.rb175
-rw-r--r--app/controllers/users_controller.rb3
-rw-r--r--app/helpers/application_helper.rb23
-rw-r--r--app/helpers/issues_helper.rb13
-rw-r--r--app/helpers/repositories_helper.rb7
-rw-r--r--app/helpers/timelog_helper.rb56
-rw-r--r--app/helpers/users_helper.rb12
-rw-r--r--app/models/attachment.rb97
-rw-r--r--app/models/changeset.rb4
-rw-r--r--app/models/issue.rb12
-rw-r--r--app/models/journal.rb1
-rw-r--r--app/models/message.rb1
-rw-r--r--app/models/message_observer.rb2
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/query.rb19
-rw-r--r--app/models/repository/cvs.rb17
-rw-r--r--app/models/repository/darcs.rb3
-rw-r--r--app/models/repository/mercurial.rb9
-rw-r--r--app/models/time_entry.rb6
-rw-r--r--app/models/user.rb44
-rw-r--r--app/models/wiki_content.rb1
-rw-r--r--app/views/account/register.rhtml7
-rw-r--r--app/views/common/_calendar.rhtml7
-rw-r--r--app/views/issues/_form.rhtml7
-rw-r--r--app/views/issues/_history.rhtml2
-rw-r--r--app/views/issues/_sidebar.rhtml11
-rw-r--r--app/views/issues/bulk_edit.rhtml7
-rw-r--r--app/views/issues/index.rhtml4
-rw-r--r--app/views/issues/new.rhtml2
-rw-r--r--app/views/issues/show.rhtml2
-rw-r--r--app/views/messages/show.rhtml2
-rw-r--r--app/views/news/show.rhtml2
-rw-r--r--app/views/projects/_form.rhtml8
-rw-r--r--app/views/projects/activity.rhtml2
-rw-r--r--app/views/projects/destroy.rhtml16
-rw-r--r--app/views/projects/gantt.rhtml10
-rw-r--r--app/views/projects/list_files.rhtml8
-rw-r--r--app/views/queries/_filters.rhtml12
-rw-r--r--app/views/queries/_form.rhtml11
-rw-r--r--app/views/repositories/revision.rhtml2
-rw-r--r--app/views/timelog/_date_range.rhtml28
-rw-r--r--app/views/timelog/_list.rhtml4
-rw-r--r--app/views/timelog/_report_criteria.rhtml8
-rw-r--r--app/views/timelog/details.rhtml26
-rw-r--r--app/views/timelog/edit.rhtml8
-rw-r--r--app/views/timelog/report.rhtml62
-rw-r--r--app/views/users/_form.rhtml7
-rw-r--r--app/views/users/list.rhtml12
-rw-r--r--app/views/versions/_form.rhtml7
-rw-r--r--config/environment.rb3
-rw-r--r--doc/CHANGELOG32
-rw-r--r--extra/svn/Redmine.pm41
-rw-r--r--lang/bg.yml165
-rw-r--r--lang/cs.yml35
-rw-r--r--lang/da.yml1
-rw-r--r--lang/de.yml45
-rw-r--r--lang/en.yml1
-rw-r--r--lang/es.yml125
-rw-r--r--lang/fi.yml7
-rw-r--r--lang/fr.yml3
-rw-r--r--lang/he.yml201
-rw-r--r--lang/it.yml1
-rw-r--r--lang/ja.yml1
-rw-r--r--lang/ko.yml1
-rw-r--r--lang/lt.yml1
-rw-r--r--lang/nl.yml1
-rw-r--r--lang/no.yml19
-rw-r--r--lang/pl.yml1
-rw-r--r--lang/pt-br.yml1
-rw-r--r--lang/pt.yml1
-rw-r--r--lang/ro.yml1
-rw-r--r--lang/ru.yml5
-rw-r--r--lang/sr.yml1
-rw-r--r--lang/sv.yml1
-rw-r--r--lang/uk.yml1
-rw-r--r--lang/zh-tw.yml1
-rw-r--r--lang/zh.yml1
-rw-r--r--lib/redcloth.rb2
-rw-r--r--lib/redmine.rb1
-rw-r--r--lib/redmine/core_ext.rb1
-rw-r--r--lib/redmine/core_ext/string.rb5
-rw-r--r--lib/redmine/core_ext/string/conversions.rb40
-rw-r--r--lib/redmine/scm/adapters/abstract_adapter.rb13
-rw-r--r--lib/redmine/scm/adapters/bazaar_adapter.rb12
-rw-r--r--lib/redmine/scm/adapters/cvs_adapter.rb13
-rw-r--r--lib/redmine/scm/adapters/darcs_adapter.rb11
-rw-r--r--lib/redmine/scm/adapters/git_adapter.rb10
-rw-r--r--lib/redmine/scm/adapters/mercurial_adapter.rb66
-rw-r--r--lib/redmine/scm/adapters/subversion_adapter.rb21
-rw-r--r--lib/redmine/wiki_formatting/macros.rb29
-rw-r--r--public/images/attachment.pngbin259 -> 995 bytes
-rw-r--r--public/images/changeset.pngbin0 -> 512 bytes
-rw-r--r--public/images/comments.pngbin0 -> 557 bytes
-rw-r--r--public/images/document.pngbin0 -> 458 bytes
-rw-r--r--public/images/message.pngbin0 -> 521 bytes
-rw-r--r--public/images/news.pngbin0 -> 658 bytes
-rw-r--r--public/images/ticket.pngbin0 -> 500 bytes
-rw-r--r--public/images/ticket_checked.pngbin0 -> 598 bytes
-rw-r--r--public/images/ticket_edit.pngbin0 -> 731 bytes
-rw-r--r--public/images/wiki_edit.pngbin0 -> 626 bytes
-rw-r--r--public/javascripts/context_menu.js65
-rw-r--r--public/stylesheets/application.css37
-rw-r--r--public/stylesheets/context_menu.css10
-rw-r--r--test/fixtures/attachments.yml13
-rw-r--r--test/fixtures/custom_fields.yml2
-rw-r--r--test/fixtures/queries.yml53
-rw-r--r--test/fixtures/roles.yml2
-rw-r--r--test/fixtures/time_entries.yml4
-rw-r--r--test/fixtures/wiki_contents.yml16
-rw-r--r--test/fixtures/wiki_pages.yml5
-rw-r--r--test/functional/issues_controller_test.rb15
-rw-r--r--test/functional/messages_controller_test.rb12
-rw-r--r--test/functional/projects_controller_test.rb2
-rw-r--r--test/functional/queries_controller_test.rb211
-rw-r--r--test/functional/repositories_bazaar_controller_test.rb137
-rw-r--r--test/functional/repositories_cvs_controller_test.rb25
-rw-r--r--test/functional/repositories_darcs_controller_test.rb11
-rw-r--r--test/functional/repositories_git_controller_test.rb20
-rw-r--r--test/functional/repositories_mercurial_controller_test.rb22
-rw-r--r--test/functional/repositories_subversion_controller_test.rb21
-rw-r--r--test/functional/timelog_controller_test.rb67
-rw-r--r--test/functional/wiki_controller_test.rb8
-rw-r--r--test/unit/changeset_test.rb12
-rw-r--r--test/unit/helpers/application_helper_test.rb25
-rw-r--r--test/unit/issue_test.rb9
-rw-r--r--test/unit/project_test.rb9
-rw-r--r--test/unit/query_test.rb36
-rw-r--r--test/unit/repository_bazaar_test.rb4
-rw-r--r--test/unit/repository_darcs_test.rb4
-rw-r--r--test/unit/repository_mercurial_test.rb4
-rw-r--r--test/unit/repository_subversion_test.rb4
-rw-r--r--test/unit/time_entry_test.rb46
-rw-r--r--vendor/plugins/acts_as_event/lib/acts_as_event.rb19
139 files changed, 1907 insertions, 845 deletions
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
index e719e8c9b..b9224c158 100644
--- a/app/controllers/account_controller.rb
+++ b/app/controllers/account_controller.rb
@@ -56,6 +56,8 @@ class AccountController < ApplicationController
flash.now[:error] = l(:notice_account_invalid_creditentials)
end
end
+ rescue User::OnTheFlyCreationFailure
+ flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.'
end
# Log out current user and redirect to welcome page
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index 98cb4a827..abf621641 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -150,6 +150,7 @@ class ApplicationController < ActionController::Base
def render_feed(items, options={})
@items = items || []
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
+ @items = @items.slice(0, Setting.feeds_limit.to_i)
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index dbc3161d7..84b95741e 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -73,6 +73,8 @@ class IssuesController < ApplicationController
# Send html if the query is not valid
render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
end
+ rescue ActiveRecord::RecordNotFound
+ render_404
end
def changes
@@ -87,6 +89,8 @@ class IssuesController < ApplicationController
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
+ rescue ActiveRecord::RecordNotFound
+ render_404
end
def show
@@ -136,7 +140,9 @@ class IssuesController < ApplicationController
requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
- @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
+ @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
+ :customized => @issue,
+ :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
@issue.custom_values = @custom_values
if @issue.save
attach_files(@issue, params[:attachments])
@@ -338,8 +344,8 @@ class IssuesController < ApplicationController
end
def preview
- issue = @project.issues.find_by_id(params[:id])
- @attachements = issue.attachments if issue
+ @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
+ @attachements = @issue.attachments if @issue
@text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
render :partial => 'common/preview'
end
@@ -384,7 +390,10 @@ private
# Retrieve query from session or build a new query
def retrieve_query
if !params[:query_id].blank?
- @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)})
+ cond = "project_id IS NULL"
+ cond << " OR project_id = #{@project.id}" if @project
+ @query = Query.find(params[:query_id], :conditions => cond)
+ @query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
else
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
@@ -404,6 +413,7 @@ private
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
+ @query.project = @project
end
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 199b2f0c5..b71ec1ecd 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -66,20 +66,20 @@ class ProjectsController < ApplicationController
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
:order => 'name')
@project = Project.new(params[:project])
- @project.enabled_module_names = Redmine::AccessControl.available_project_modules
if request.get?
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
+ @project.enabled_module_names = Redmine::AccessControl.available_project_modules
else
@project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@project.custom_values = @custom_values
+ @project.enabled_module_names = params[:enabled_modules]
if @project.save
- @project.enabled_module_names = params[:enabled_modules]
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
- end
+ end
end
end
@@ -204,7 +204,10 @@ class ProjectsController < ApplicationController
end
def list_files
- @versions = @project.versions.sort.reverse
+ sort_init "#{Attachment.table_name}.filename", "asc"
+ sort_update
+ @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
+ render :layout => !request.xhr?
end
# Show changelog for @project
@@ -338,8 +341,9 @@ class ProjectsController < ApplicationController
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
) unless @selected_tracker_ids.empty?
+ events += Version.find(:all, :include => :project,
+ :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
end
- events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
render :layout => false if request.xhr?
@@ -383,8 +387,9 @@ class ProjectsController < ApplicationController
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
) unless @selected_tracker_ids.empty?
+ @events += Version.find(:all, :include => :project,
+ :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
end
- @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
@events.sort! {|x,y| x.start_date <=> y.start_date }
if params[:format]=='pdf'
diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb
index 0a762eee0..da2c4a2c8 100644
--- a/app/controllers/queries_controller.rb
+++ b/app/controllers/queries_controller.rb
@@ -18,19 +18,14 @@
class QueriesController < ApplicationController
layout 'base'
menu_item :issues
- before_filter :find_project, :authorize
-
- def index
- @queries = @project.queries.find(:all,
- :order => "name ASC",
- :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
- end
+ before_filter :find_query, :except => :new
+ before_filter :find_optional_project, :only => :new
def new
@query = Query.new(params[:query])
- @query.project = @project
+ @query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current
- @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
+ @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
params[:fields].each do |field|
@@ -52,7 +47,8 @@ class QueriesController < ApplicationController
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.attributes = params[:query]
- @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
+ @query.project = nil if params[:query_is_for_all]
+ @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
if @query.save
@@ -64,18 +60,21 @@ class QueriesController < ApplicationController
def destroy
@query.destroy if request.post?
- redirect_to :controller => 'queries', :project_id => @project
+ redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
end
private
- def find_project
- if params[:id]
- @query = Query.find(params[:id])
- @project = @query.project
- render_403 unless @query.editable_by?(User.current)
- else
- @project = Project.find(params[:project_id])
- end
+ def find_query
+ @query = Query.find(params[:id])
+ @project = @query.project
+ render_403 unless @query.editable_by?(User.current)
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def find_optional_project
+ @project = Project.find(params[:project_id]) if params[:project_id]
+ User.current.allowed_to?(:save_queries, @project, :global => true)
rescue ActiveRecord::RecordNotFound
render_404
end
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
index 10c235d65..64eb05793 100644
--- a/app/controllers/repositories_controller.rb
+++ b/app/controllers/repositories_controller.rb
@@ -19,8 +19,8 @@ require 'SVG/Graph/Bar'
require 'SVG/Graph/BarHorizontal'
require 'digest/sha1'
-class ChangesetNotFound < Exception
-end
+class ChangesetNotFound < Exception; end
+class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
layout 'base'
@@ -51,8 +51,8 @@ class RepositoriesController < ApplicationController
def show
# check if new revisions have been committed in the repository
@repository.fetch_changesets if Setting.autofetch_changesets?
- # get entries for the browse frame
- @entries = @repository.entries('')
+ # root entries
+ @entries = @repository.entries('', @rev)
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error_not_found unless @entries || @changesets.any?
@@ -65,7 +65,8 @@ class RepositoriesController < ApplicationController
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
- show_error_not_found unless @entries
+ show_error_not_found and return unless @entries
+ render :action => 'browse'
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
@@ -95,6 +96,12 @@ class RepositoriesController < ApplicationController
end
def entry
+ @entry = @repository.scm.entry(@path, @rev)
+ show_error_not_found and return unless @entry
+
+ # If the entry is a dir, show the browser
+ browse and return if @entry.is_dir?
+
@content = @repository.scm.cat(@path, @rev)
show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data?
@@ -135,7 +142,6 @@ class RepositoriesController < ApplicationController
end
def diff
- @rev_to = params[:rev_to]
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
@@ -180,6 +186,8 @@ private
render_404
end
+ REV_PARAM_RE = %r{^[a-f0-9]*$}
+
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
@@ -187,8 +195,12 @@ private
@path = params[:path].join('/') unless params[:path].nil?
@path ||= ''
@rev = params[:rev]
+ @rev_to = params[:rev_to]
+ raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
rescue ActiveRecord::RecordNotFound
render_404
+ rescue InvalidRevisionParam
+ show_error_not_found
end
def show_error_not_found
@@ -255,6 +267,9 @@ private
commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
+ # Remove email adress in usernames
+ fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
+
graph = SVG::Graph::BarHorizontal.new(
:height => 300,
:width => 500,
diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb
index 8cfe225d1..29c2635d6 100644
--- a/app/controllers/timelog_controller.rb
+++ b/app/controllers/timelog_controller.rb
@@ -26,6 +26,8 @@ class TimelogController < ApplicationController
include SortHelper
helper :issues
include TimelogHelper
+ helper :custom_fields
+ include CustomFieldsHelper
def report
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
@@ -45,37 +47,40 @@ class TimelogController < ApplicationController
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => Enumeration,
- :label => :label_activity}
+ :label => :label_activity},
+ 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
+ :klass => Issue,
+ :label => :label_issue}
}
+ # Add list and boolean custom fields as available criterias
+ @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+ @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
+ :format => cf.field_format,
+ :label => cf.name}
+ end
+
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
- @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
+ @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
- if params[:date_from]
- begin; @date_from = params[:date_from].to_date; rescue; end
- end
- if params[:date_to]
- begin; @date_to = params[:date_to].to_date; rescue; end
- end
- @date_from ||= Date.civil(Date.today.year, 1, 1)
- @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1
+ retrieve_date_range
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
- sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
+ sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
- sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
- sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
+ sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
+ sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@@ -87,90 +92,51 @@ class TimelogController < ApplicationController
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
+ when 'day'
+ row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
- end
-
- @periods = []
- date_from = @date_from
- # 100 columns max
- while date_from < @date_to && @periods.length < 100
- case @columns
- when 'year'
- @periods << "#{date_from.year}"
- date_from = date_from >> 12
- when 'month'
- @periods << "#{date_from.year}-#{date_from.month}"
- date_from = date_from >> 1
- when 'week'
- @periods << "#{date_from.year}-#{date_from.cweek}"
- date_from = date_from + 7
+
+ @periods = []
+ # Date#at_beginning_of_ not supported in Rails 1.2.x
+ date_from = @from.to_time
+ # 100 columns max
+ while date_from <= @to.to_time && @periods.length < 100
+ case @columns
+ when 'year'
+ @periods << "#{date_from.year}"
+ date_from = (date_from + 1.year).at_beginning_of_year
+ when 'month'
+ @periods << "#{date_from.year}-#{date_from.month}"
+ date_from = (date_from + 1.month).at_beginning_of_month
+ when 'week'
+ @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
+ date_from = (date_from + 7.day).at_beginning_of_week
+ when 'day'
+ @periods << "#{date_from.to_date}"
+ date_from = date_from + 1.day
+ end
end
end
- render :layout => false if request.xhr?
+ respond_to do |format|
+ format.html { render :layout => !request.xhr? }
+ format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
+ end
end
def details
sort_init 'spent_on', 'desc'
sort_update
-
- @free_period = false
- @from, @to = nil, nil
-
- if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
- case params[:period].to_s
- when 'today'
- @from = @to = Date.today
- when 'yesterday'
- @from = @to = Date.today - 1
- when 'current_week'
- @from = Date.today - (Date.today.cwday - 1)%7
- @to = @from + 6
- when 'last_week'
- @from = Date.today - 7 - (Date.today.cwday - 1)%7
- @to = @from + 6
- when '7_days'
- @from = Date.today - 7
- @to = Date.today
- when 'current_month'
- @from = Date.civil(Date.today.year, Date.today.month, 1)
- @to = (@from >> 1) - 1
- when 'last_month'
- @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
- @to = (@from >> 1) - 1
- when '30_days'
- @from = Date.today - 30
- @to = Date.today
- when 'current_year'
- @from = Date.civil(Date.today.year, 1, 1)
- @to = Date.civil(Date.today.year, 12, 31)
- end
- elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
- begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
- begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
- @free_period = true
- else
- # default
- end
-
- @from, @to = @to, @from if @from && @to && @from > @to
cond = ARCondition.new
cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
- if @from
- if @to
- cond << ['spent_on BETWEEN ? AND ?', @from, @to]
- else
- cond << ['spent_on >= ?', @from]
- end
- elsif @to
- cond << ['spent_on <= ?', @to]
- end
+ retrieve_date_range
+ cond << ['spent_on BETWEEN ? AND ?', @from, @to]
TimeEntry.visible_by(User.current) do
respond_to do |format|
@@ -185,6 +151,7 @@ class TimelogController < ApplicationController
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
@total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
+
render :layout => !request.xhr?
}
format.csv {
@@ -205,7 +172,7 @@ class TimelogController < ApplicationController
@time_entry.attributes = params[:time_entry]
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
- redirect_to :action => 'details', :project_id => @time_entry.project
+ redirect_to(params[:back_url] || {:action => 'details', :project_id => @time_entry.project})
return
end
@activities = Enumeration::get_values('ACTI')
@@ -238,4 +205,50 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
+
+ # Retrieves the date range based on predefined ranges or specific from/to param dates
+ def retrieve_date_range
+ @free_period = false
+ @from, @to = nil, nil
+
+ if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
+ case params[:period].to_s
+ when 'today'
+ @from = @to = Date.today
+ when 'yesterday'
+ @from = @to = Date.today - 1
+ when 'current_week'
+ @from = Date.today - (Date.today.cwday - 1)%7
+ @to = @from + 6
+ when 'last_week'
+ @from = Date.today - 7 - (Date.today.cwday - 1)%7
+ @to = @from + 6
+ when '7_days'
+ @from = Date.today - 7
+ @to = Date.today
+ when 'current_month'
+ @from = Date.civil(Date.today.year, Date.today.month, 1)
+ @to = (@from >> 1) - 1
+ when 'last_month'
+ @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
+ @to = (@from >> 1) - 1
+ when '30_days'
+ @from = Date.today - 30
+ @to = Date.today
+ when 'current_year'
+ @from = Date.civil(Date.today.year, 1, 1)
+ @to = Date.civil(Date.today.year, 12, 31)
+ end
+ elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
+ begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
+ begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
+ @free_period = true
+ else
+ # default
+ end
+
+ @from, @to = @to, @from if @from && @to && @from > @to
+ @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
+ @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
+ end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index ceb70ab92..48fc6fade 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -83,7 +83,8 @@ class UsersController < ApplicationController
end
if @user.update_attributes(params[:user])
flash[:notice] = l(:notice_successful_update)
- redirect_to :action => 'list'
+ # Give a string to redirect_to otherwise it would use status param as the response code
+ redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
end
end
@auth_sources = AuthSource.find(:all)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 77019eba3..47a251053 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -207,8 +207,10 @@ module ApplicationHelper
rf = Regexp.new(filename, Regexp::IGNORECASE)
# search for the picture in attachments
if found = attachments.detect { |att| att.filename =~ rf }
- image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found.id
- "!#{style}#{image_url}!"
+ image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
+ desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
+ alt = desc.blank? ? nil : "(#{desc})"
+ "!#{style}#{image_url}#{alt}!"
else
"!#{style}#{filename}!"
end
@@ -425,6 +427,10 @@ module ApplicationHelper
form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
end
+ def back_url_hidden_field_tag
+ hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER'])
+ end
+
def check_all_links(form_name)
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
" | " +
@@ -463,9 +469,22 @@ module ApplicationHelper
end
def calendar_for(field_id)
+ include_calendar_headers_tags
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
end
+
+ def include_calendar_headers_tags
+ unless @calendar_headers_tags_included
+ @calendar_headers_tags_included = true
+ content_for :header_tags do
+ javascript_include_tag('calendar/calendar') +
+ javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
+ javascript_include_tag('calendar/calendar-setup') +
+ stylesheet_link_tag('calendar')
+ end
+ end
+ end
def wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 17889fadd..6013f1ec8 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -32,6 +32,19 @@ module IssuesHelper
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
+
+ def sidebar_queries
+ unless @sidebar_queries
+ # User can see public queries and his own queries
+ visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
+ # Project specific queries and global queries
+ visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
+ @sidebar_queries = Query.find(:all,
+ :order => "name ASC",
+ :conditions => visible.conditions)
+ end
+ @sidebar_queries
+ end
def show_detail(detail, no_html=false)
case detail.property
diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
index 31daf1bd8..22bdec9df 100644
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -58,8 +58,11 @@ module RepositoriesHelper
end
def with_leading_slash(path)
- path ||= ''
- path.starts_with?('/') ? path : "/#{path}"
+ path.to_s.starts_with?('/') ? path : "/#{path}"
+ end
+
+ def without_leading_slash(path)
+ path.gsub(%r{^/+}, '')
end
def subversion_field_tags(form, repository)
diff --git a/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb
index 05b55907c..db13556a1 100644
--- a/app/helpers/timelog_helper.rb
+++ b/app/helpers/timelog_helper.rb
@@ -76,4 +76,60 @@ module TimelogHelper
export.rewind
export
end
+
+ def format_criteria_value(criteria, value)
+ value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
+ end
+
+ def report_to_csv(criterias, periods, hours)
+ export = StringIO.new
+ CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
+ # Column headers
+ headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
+ headers += periods
+ headers << l(:label_total)
+ csv << headers.collect {|c| to_utf8(c) }
+ # Content
+ report_criteria_to_csv(csv, criterias, periods, hours)
+ # Total row
+ row = [ l(:label_total) ] + [''] * (criterias.size - 1)
+ total = 0
+ periods.each do |period|
+ sum = sum_hours(select_hours(hours, @columns, period.to_s))
+ total += sum
+ row << (sum > 0 ? "%.2f" % sum : '')
+ end
+ row << "%.2f" %total
+ csv << row
+ end
+ export.rewind
+ export
+ end
+
+ def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
+ hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
+ hours_for_value = select_hours(hours, criterias[level], value)
+ next if hours_for_value.empty?
+ row = [''] * level
+ row << to_utf8(format_criteria_value(criterias[level], value))
+ row += [''] * (criterias.length - level - 1)
+ total = 0
+ periods.each do |period|
+ sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
+ total += sum
+ row << (sum > 0 ? "%.2f" % sum : '')
+ end
+ row << "%.2f" %total
+ csv << row
+
+ if criterias.length > level + 1
+ report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
+ end
+ end
+ end
+
+ def to_utf8(s)
+ @ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ begin; @ic.iconv(s.to_s); rescue; s.to_s; end
+ end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 7bd137161..250ed8ce8 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -22,4 +22,16 @@ module UsersHelper
[l(:status_registered), 2],
[l(:status_locked), 3]], selected)
end
+
+ def change_status_link(user)
+ url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
+
+ if user.locked?
+ link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
+ elsif user.registered?
+ link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
+ else
+ link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
+ end
+ end
end
diff --git a/app/models/attachment.rb b/app/models/attachment.rb
index cdcb8d231..08f440816 100644
--- a/app/models/attachment.rb
+++ b/app/models/attachment.rb
@@ -35,48 +35,48 @@ class Attachment < ActiveRecord::Base
errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
end
- def file=(incomming_file)
- unless incomming_file.nil?
- @temp_file = incomming_file
- if @temp_file.size > 0
- self.filename = sanitize_filename(@temp_file.original_filename)
- self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename
- self.content_type = @temp_file.content_type.to_s.chomp
- self.filesize = @temp_file.size
- end
- end
- end
-
- def file
- nil
- end
-
- # Copy temp file to its final location
- def before_save
- if @temp_file && (@temp_file.size > 0)
- logger.debug("saving '#{self.diskfile}'")
- File.open(diskfile, "wb") do |f|
- f.write(@temp_file.read)
- end
- self.digest = Digest::MD5.hexdigest(File.read(diskfile))
- end
- # Don't save the content type if it's longer than the authorized length
- if self.content_type && self.content_type.length > 255
- self.content_type = nil
- end
- end
-
- # Deletes file on the disk
- def after_destroy
- if self.filename?
- File.delete(diskfile) if File.exist?(diskfile)
- end
- end
+ def file=(incoming_file)
+ unless incoming_file.nil?
+ @temp_file = incoming_file
+ if @temp_file.size > 0
+ self.filename = sanitize_filename(@temp_file.original_filename)
+ self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename
+ self.content_type = @temp_file.content_type.to_s.chomp
+ self.filesize = @temp_file.size
+ end
+ end
+ end
- # Returns file's location on disk
- def diskfile
- "#{@@storage_path}/#{self.disk_filename}"
- end
+ def file
+ nil
+ end
+
+ # Copy temp file to its final location
+ def before_save
+ if @temp_file && (@temp_file.size > 0)
+ logger.debug("saving '#{self.diskfile}'")
+ File.open(diskfile, "wb") do |f|
+ f.write(@temp_file.read)
+ end
+ self.digest = Digest::MD5.hexdigest(File.read(diskfile))
+ end
+ # Don't save the content type if it's longer than the authorized length
+ if self.content_type && self.content_type.length > 255
+ self.content_type = nil
+ end
+ end
+
+ # Deletes file on the disk
+ def after_destroy
+ if self.filename?
+ File.delete(diskfile) if File.exist?(diskfile)
+ end
+ end
+
+ # Returns file's location on disk
+ def diskfile
+ "#{@@storage_path}/#{self.disk_filename}"
+ end
def increment_download
increment!(:downloads)
@@ -87,18 +87,17 @@ class Attachment < ActiveRecord::Base
end
def image?
- self.filename =~ /\.(jpeg|jpg|gif|png)$/i
+ self.filename =~ /\.(jpe?g|gif|png)$/i
end
private
def sanitize_filename(value)
- # get only the filename, not the whole path
- just_filename = value.gsub(/^.*(\\|\/)/, '')
- # NOTE: File.basename doesn't work right with Windows paths on Unix
- # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
+ # get only the filename, not the whole path
+ just_filename = value.gsub(/^.*(\\|\/)/, '')
+ # NOTE: File.basename doesn't work right with Windows paths on Unix
+ # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
- # Finally, replace all non alphanumeric, underscore or periods with underscore
- @filename = just_filename.gsub(/[^\w\.\-]/,'_')
+ # Finally, replace all non alphanumeric, hyphens or periods with underscore
+ @filename = just_filename.gsub(/[^\w\.\-]/,'_')
end
-
end
diff --git a/app/models/changeset.rb b/app/models/changeset.rb
index ce9ea28ca..3e95ce111 100644
--- a/app/models/changeset.rb
+++ b/app/models/changeset.rb
@@ -35,6 +35,10 @@ class Changeset < ActiveRecord::Base
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
+ def revision=(r)
+ write_attribute :revision, (r.nil? ? nil : r.to_s)
+ end
+
def comments=(comment)
write_attribute(:comments, comment.strip)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1d25a4604..8082e43b7 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -93,7 +93,11 @@ class Issue < ActiveRecord::Base
self.priority = nil
write_attribute(:priority_id, pid)
end
-
+
+ def estimated_hours=(h)
+ write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
+ end
+
def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :activerecord_error_not_a_date
@@ -153,6 +157,8 @@ class Issue < ActiveRecord::Base
# Close duplicates if the issue was closed
if @issue_before_change && !@issue_before_change.closed? && self.closed?
duplicates.each do |duplicate|
+ # Reload is need in case the duplicate was updated by a previous duplicate
+ duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
@@ -237,4 +243,8 @@ class Issue < ActiveRecord::Base
yield
end
end
+
+ def to_s
+ "#{tracker} ##{id}: #{subject}"
+ end
end
diff --git a/app/models/journal.rb b/app/models/journal.rb
index 7c5e3d3bf..1376d349e 100644
--- a/app/models/journal.rb
+++ b/app/models/journal.rb
@@ -33,6 +33,7 @@ class Journal < ActiveRecord::Base
acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') },
:description => :notes,
:author => :user,
+ :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
def save
diff --git a/app/models/message.rb b/app/models/message.rb
index 12b1cd990..a18d126c9 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -28,6 +28,7 @@ class Message < ActiveRecord::Base
:date_column => 'created_on'
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
+ :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
attr_protected :locked, :sticky
diff --git a/app/models/message_observer.rb b/app/models/message_observer.rb
index c26805c1b..043988172 100644
--- a/app/models/message_observer.rb
+++ b/app/models/message_observer.rb
@@ -21,6 +21,8 @@ class MessageObserver < ActiveRecord::Observer
recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?}
# send notification to the board watchers
recipients += message.board.watcher_recipients
+ # send notification to project members who want to be notified
+ recipients += message.board.project.recipients
recipients = recipients.compact.uniq
Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a223b35f0..964469649 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -33,7 +33,7 @@ class Project < ActiveRecord::Base
has_many :documents, :dependent => :destroy
has_many :news, :dependent => :delete_all, :include => :author
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
- has_many :boards, :order => "position ASC"
+ has_many :boards, :dependent => :destroy, :order => "position ASC"
has_one :repository, :dependent => :destroy
has_many :changesets, :through => :repository
has_one :wiki, :dependent => :destroy
@@ -75,12 +75,14 @@ class Project < ActiveRecord::Base
conditions = nil
if include_subprojects && !active_children.empty?
ids = [id] + active_children.collect {|c| c.id}
- conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
+ conditions = ["#{Project.table_name}.id IN (#{ids.join(',')})"]
end
- conditions ||= ["#{Issue.table_name}.project_id = ?", id]
+ conditions ||= ["#{Project.table_name}.id = ?", id]
# Quick and dirty fix for Rails 2 compatibility
Issue.send(:with_scope, :find => { :conditions => conditions }) do
- yield
+ Version.send(:with_scope, :find => { :conditions => conditions }) do
+ yield
+ end
end
end
diff --git a/app/models/query.rb b/app/models/query.rb
index 99d13aa6a..641c0d17b 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -116,6 +116,11 @@ class Query < ActiveRecord::Base
set_language_if_valid(User.current.language)
end
+ def after_initialize
+ # Store the fact that project is nil (used in #editable_by?)
+ @is_for_all = project.nil?
+ end
+
def validate
filters.each_key do |field|
errors.add label_for(field), :activerecord_error_blank unless
@@ -128,8 +133,10 @@ class Query < ActiveRecord::Base
def editable_by?(user)
return false unless user
- return true if !is_public && self.user_id == user.id
- is_public && user.allowed_to?(:manage_public_queries, project)
+ # Admin can edit them all and regular users can edit their private queries
+ return true if user.admin? || (!is_public && self.user_id == user.id)
+ # Members can not edit public queries that are for all project (only admin is allowed to)
+ is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
end
def available_filters
@@ -139,7 +146,7 @@ class Query < ActiveRecord::Base
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
- "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } },
+ "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"subject" => { :type => :text, :order => 8 },
"created_on" => { :type => :date_past, :order => 9 },
"updated_on" => { :type => :date_past, :order => 10 },
@@ -294,7 +301,7 @@ class Query < ActiveRecord::Base
# custom field
db_table = CustomValue.table_name
db_field = 'value'
- sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND "
+ sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
else
# regular field
db_table = Issue.table_name
@@ -313,9 +320,9 @@ class Query < ActiveRecord::Base
when "!"
sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
- sql = sql + "#{db_table}.#{db_field} IS NULL"
+ sql = sql + "#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} = ''"
when "*"
- sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
+ sql = sql + "#{db_table}.#{db_field} IS NOT NULL AND #{db_table}.#{db_field} <> ''"
when ">="
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
when "<="
diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb
index a78b60806..c2d8be977 100644
--- a/app/models/repository/cvs.rb
+++ b/app/models/repository/cvs.rb
@@ -35,7 +35,8 @@ class Repository::Cvs < Repository
end
def entries(path=nil, identifier=nil)
- entries=scm.entries(path, identifier)
+ rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
if entries
entries.each() do |entry|
unless entry.lastrev.nil? || entry.lastrev.identifier
@@ -137,12 +138,18 @@ class Repository::Cvs < Repository
end
# Renumber new changesets in chronological order
- c = changesets.find(:first, :order => 'committed_on DESC, id DESC', :conditions => "revision NOT LIKE '_%'")
- next_rev = c.nil? ? 1 : (c.revision.to_i + 1)
changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
- changeset.update_attribute :revision, next_rev
- next_rev += 1
+ changeset.update_attribute :revision, next_revision_number
end
end # transaction
end
+
+ private
+
+ # Returns the next revision number to assign to a CVS changeset
+ def next_revision_number
+ # Need to retrieve existing revision numbers to sort them as integers
+ @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
+ @current_revision_number += 1
+ end
end
diff --git a/app/models/repository/darcs.rb b/app/models/repository/darcs.rb
index cc608d370..c7c14a397 100644
--- a/app/models/repository/darcs.rb
+++ b/app/models/repository/darcs.rb
@@ -29,7 +29,8 @@ class Repository::Darcs < Repository
end
def entries(path=nil, identifier=nil)
- entries=scm.entries(path, identifier)
+ patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
if entries
entries.each do |entry|
# Search the DB for the entry's last change
diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
index 27a8eaea9..18cbc9495 100644
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -34,6 +34,11 @@ class Repository::Mercurial < Repository
if entries
entries.each do |entry|
next unless entry.is_file?
+ # Set the filesize unless browsing a specific revision
+ if identifier.nil?
+ full_path = File.join(root_url, entry.path)
+ entry.size = File.stat(full_path).size if File.file?(full_path)
+ end
# Search the DB for the entry's last change
change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
if change
@@ -53,7 +58,9 @@ class Repository::Mercurial < Repository
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
# latest revision in the repository
- scm_revision = scm_info.lastrev.identifier.to_i
+ latest_revision = scm_info.lastrev
+ return if latest_revision.nil?
+ scm_revision = latest_revision.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb
index bcf6d1223..ddaff2b60 100644
--- a/app/models/time_entry.rb
+++ b/app/models/time_entry.rb
@@ -1,5 +1,5 @@
# redMine - project management software
-# Copyright (C) 2006-2007 Jean-Philippe Lang
+# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -39,6 +39,10 @@ class TimeEntry < ActiveRecord::Base
errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
end
+ def hours=(h)
+ write_attribute :hours, (h.is_a?(String) ? h.to_hours : h)
+ end
+
# tyear, tmonth, tweek assigned where setting spent_on attributes
# these attributes make time aggregations easier
def spent_on=(date)
diff --git a/app/models/user.rb b/app/models/user.rb
index ae81d46d2..a67a08567 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -18,6 +18,9 @@
require "digest/sha1"
class User < ActiveRecord::Base
+
+ class OnTheFlyCreationFailure < Exception; end
+
# Account statuses
STATUS_ANONYMOUS = 0
STATUS_ACTIVE = 1
@@ -105,15 +108,17 @@ class User < ActiveRecord::Base
onthefly.language = Setting.default_language
if onthefly.save
user = find(:first, :conditions => ["login=?", login])
- logger.info("User '#{user.login}' created on the fly.") if logger
+ logger.info("User '#{user.login}' created from the LDAP") if logger
+ else
+ logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger
+ raise OnTheFlyCreationFailure.new
end
end
end
user.update_attribute(:last_login_on, Time.now) if user
user
-
- rescue => text
- raise text
+ rescue => text
+ raise text
end
# Return user's full name for display
@@ -222,17 +227,26 @@ class User < ActiveRecord::Base
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
- def allowed_to?(action, project)
- # No action allowed on archived projects
- return false unless project.active?
- # No action allowed on disabled modules
- return false unless project.allows_to?(action)
- # Admin users are authorized for anything else
- return true if admin?
-
- role = role_for_project(project)
- return false unless role
- role.allowed_to?(action) && (project.is_public? || role.member?)
+ def allowed_to?(action, project, options={})
+ if project
+ # No action allowed on archived projects
+ return false unless project.active?
+ # No action allowed on disabled modules
+ return false unless project.allows_to?(action)
+ # Admin users are authorized for anything else
+ return true if admin?
+
+ role = role_for_project(project)
+ return false unless role
+ role.allowed_to?(action) && (project.is_public? || role.member?)
+
+ elsif options[:global]
+ # authorize if user has at least one role that has this permission
+ roles = memberships.collect {|m| m.role}.uniq
+ roles.detect {|r| r.allowed_to?(action)}
+ else
+ false
+ end
end
def self.current=(user)
diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb
index 13915c274..724354ad6 100644
--- a/app/models/wiki_content.rb
+++ b/app/models/wiki_content.rb
@@ -32,6 +32,7 @@ class WikiContent < ActiveRecord::Base
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
:description => :comments,
:datetime => :updated_on,
+ :type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
def text=(plain)
diff --git a/app/views/account/register.rhtml b/app/views/account/register.rhtml
index c1425a380..7cf4b6da3 100644
--- a/app/views/account/register.rhtml
+++ b/app/views/account/register.rhtml
@@ -35,10 +35,3 @@
<%= submit_tag l(:button_submit) %>
<% end %>
-
-<% content_for :header_tags do %>
-<%= javascript_include_tag 'calendar/calendar' %>
-<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
-<%= javascript_include_tag 'calendar/calendar-setup' %>
-<%= stylesheet_link_tag 'calendar' %>
-<% end %>
diff --git a/app/views/common/_calendar.rhtml b/app/views/common/_calendar.rhtml
index 7534a1223..1095cd501 100644
--- a/app/views/common/_calendar.rhtml
+++ b/app/views/common/_calendar.rhtml
@@ -19,12 +19,15 @@ while day <= calendar.enddt %>
elsif day == i.due_date
image_tag('arrow_to.png')
end %>
- <%= h("#{i.project.name} -") unless @project && @project == i.project %>
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %>
<span class="tip"><%= render_issue_tooltip i %></span>
</div>
<% else %>
- <%= link_to_version i, :class => "icon icon-package" %>
+ <span class="icon icon-package">
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <%= link_to_version i%>
+ </span>
<% end %>
<% end %>
</td>
diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml
index 5034e0a29..9bb74fd34 100644
--- a/app/views/issues/_form.rhtml
+++ b/app/views/issues/_form.rhtml
@@ -49,10 +49,3 @@
<% end %>
<%= wikitoolbar_for 'issue_description' %>
-
-<% content_for :header_tags do %>
- <%= javascript_include_tag 'calendar/calendar' %>
- <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
- <%= javascript_include_tag 'calendar/calendar-setup' %>
- <%= stylesheet_link_tag 'calendar' %>
-<% end %>
diff --git a/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml
index 373758874..f29a44daf 100644
--- a/app/views/issues/_history.rhtml
+++ b/app/views/issues/_history.rhtml
@@ -1,5 +1,5 @@
<% for journal in journals %>
- <div id="change-<%= journal.id %>">
+ <div id="change-<%= journal.id %>" class="journal">
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
<%= content_tag('a', '', :name => "note-#{journal.indice}")%>
<%= format_time(journal.created_on) %> - <%= journal.user.name %></h4>
diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml
index 4a1b7e9bc..e94d4180b 100644
--- a/app/views/issues/_sidebar.rhtml
+++ b/app/views/issues/_sidebar.rhtml
@@ -1,13 +1,14 @@
<h3><%= l(:label_issue_plural) %></h3>
<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
+<% if @project %>
<%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %>
+<% end %>
+<% unless sidebar_queries.empty? -%>
<h3><%= l(:label_query_plural) %></h3>
-<% queries = @project.queries.find(:all,
- :order => "name ASC",
- :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
- queries.each do |query| %>
+<% sidebar_queries.each do |query| -%>
<%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
-<% end %>
+<% end -%>
+<% end -%>
diff --git a/app/views/issues/bulk_edit.rhtml b/app/views/issues/bulk_edit.rhtml
index 31ed7ee56..86bc76765 100644
--- a/app/views/issues/bulk_edit.rhtml
+++ b/app/views/issues/bulk_edit.rhtml
@@ -47,10 +47,3 @@
<p><%= submit_tag l(:button_submit) %>
<% end %>
-
-<% content_for :header_tags do %>
- <%= javascript_include_tag 'calendar/calendar' %>
- <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
- <%= javascript_include_tag 'calendar/calendar-setup' %>
- <%= stylesheet_link_tag 'calendar' %>
-<% end %>
diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml
index 094c49ff2..027f3f006 100644
--- a/app/views/issues/index.rhtml
+++ b/app/views/issues/index.rhtml
@@ -18,7 +18,7 @@
:update => "content",
}, :class => 'icon icon-reload' %>
- <% if current_role && current_role.allowed_to?(:save_queries) %>
+ <% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
<%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
<% end %>
</p>
@@ -54,7 +54,7 @@
<% content_for :sidebar do %>
<%= render :partial => 'issues/sidebar' %>
-<% end if @project%>
+<% end %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %>
diff --git a/app/views/issues/new.rhtml b/app/views/issues/new.rhtml
index 7b9dde899..280e2009b 100644
--- a/app/views/issues/new.rhtml
+++ b/app/views/issues/new.rhtml
@@ -8,7 +8,7 @@
</div>
<%= submit_tag l(:button_create) %>
<%= link_to_remote l(:label_preview),
- { :url => { :controller => 'issues', :action => 'preview', :project_id => @project, :id => @issue },
+ { :url => { :controller => 'issues', :action => 'preview', :project_id => @project },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('issue-form')",
diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml
index 77d9ce640..f788d0ec8 100644
--- a/app/views/issues/show.rhtml
+++ b/app/views/issues/show.rhtml
@@ -13,7 +13,7 @@
<h3><%=h @issue.subject %></h3>
<p class="author">
<%= authoring @issue.created_on, @issue.author %>.
- <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) if @issue.created_on != @issue.updated_on %>.
+ <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
</p>
<table width="100%">
diff --git a/app/views/messages/show.rhtml b/app/views/messages/show.rhtml
index ef7db71ef..251b7c7a5 100644
--- a/app/views/messages/show.rhtml
+++ b/app/views/messages/show.rhtml
@@ -26,7 +26,7 @@
</div>
<div class="message reply">
<h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
- <div class="wiki"><%= textilizable message.content %></div>
+ <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
<%= link_to_attachments message.attachments, :no_author => true %>
</div>
<% end %>
diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml
index 6de8aa86e..a55b56f0b 100644
--- a/app/views/news/show.rhtml
+++ b/app/views/news/show.rhtml
@@ -25,7 +25,7 @@
<div id="preview" class="wiki"></div>
</div>
-<p><em><% unless @news.summary.empty? %><%=h @news.summary %><br /><% end %>
+<p><em><% unless @news.summary.blank? %><%=h @news.summary %><br /><% end %>
<span class="author"><%= authoring @news.created_on, @news.author %></span></em></p>
<div class="wiki">
<%= textilizable(@news.description) %>
diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml
index a810369d4..32e4dcd44 100644
--- a/app/views/projects/_form.rhtml
+++ b/app/views/projects/_form.rhtml
@@ -46,11 +46,3 @@
</fieldset>
<% end %>
<!--[eoform:project]-->
-
-
-<% content_for :header_tags do %>
-<%= javascript_include_tag 'calendar/calendar' %>
-<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
-<%= javascript_include_tag 'calendar/calendar-setup' %>
-<%= stylesheet_link_tag 'calendar' %>
-<% end %>
diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml
index 0cf7a5000..c2f2f9ebd 100644
--- a/app/views/projects/activity.rhtml
+++ b/app/views/projects/activity.rhtml
@@ -6,7 +6,7 @@
<h3><%= format_activity_day(day) %></h3>
<dl>
<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
- <dt class="<%= e.class.name.downcase %>"><span class="time"><%= format_time(e.event_datetime, false) %></span>
+ <dt class="<%= e.event_type %>"><span class="time"><%= format_time(e.event_datetime, false) %></span>
<%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %></dt>
<dd><% unless e.event_description.blank? -%>
<span class="description"><%= format_activity_description(e.event_description) %></span><br />
diff --git a/app/views/projects/destroy.rhtml b/app/views/projects/destroy.rhtml
index 4531cb845..a1913c115 100644
--- a/app/views/projects/destroy.rhtml
+++ b/app/views/projects/destroy.rhtml
@@ -1,14 +1,16 @@
<h2><%=l(:label_confirmation)%></h2>
-<div class="box">
-<center>
-<p><strong><%=h @project_to_destroy.name %></strong><br />
-<%=l(:text_project_destroy_confirmation)%></p>
+<div class="warning">
+<p><strong><%=h @project_to_destroy %></strong><br />
+<%=l(:text_project_destroy_confirmation)%>
+<% if @project_to_destroy.children.any? %>
+<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %>
+<% end %>
+</p>
<p>
<% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %>
- <%= hidden_field_tag "confirm", 1 %>
+ <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
<%= submit_tag l(:button_delete) %>
<% end %>
</p>
-</center>
-</div> \ No newline at end of file
+</div>
diff --git a/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml
index 05bd4b9bc..d941d2777 100644
--- a/app/views/projects/gantt.rhtml
+++ b/app/views/projects/gantt.rhtml
@@ -70,10 +70,13 @@ top = headers_height + 8
@events.each do |i| %>
<div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:4px;overflow:hidden;"><small>
<% if i.is_a? Issue %>
- <%= h("#{i.project.name} -") unless @project && @project == i.project %>
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_issue i %>: <%=h i.subject %>
<% else %>
- <%= link_to_version i, :class => "icon icon-package" %>
+ <span class="icon icon-package">
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <%= link_to_version i %>
+ </span>
<% end %>
</small></div>
<% top = top + 20
@@ -197,7 +200,8 @@ top = headers_height + 10
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
<div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
- <strong><%= i.name %></strong>
+ <%= h("#{i.project} -") unless @project && @project == i.project %>
+ <strong><%=h i %></strong>
</div>
<% end %>
<% top = top + 20
diff --git a/app/views/projects/list_files.rhtml b/app/views/projects/list_files.rhtml
index ec4a3619b..f385229ae 100644
--- a/app/views/projects/list_files.rhtml
+++ b/app/views/projects/list_files.rhtml
@@ -9,10 +9,10 @@
<table class="list">
<thead><tr>
<th><%=l(:field_version)%></th>
- <th><%=l(:field_filename)%></th>
- <th><%=l(:label_date)%></th>
- <th><%=l(:field_filesize)%></th>
- <th><%=l(:label_downloads_abbr)%></th>
+ <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
+ <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
+ <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
+ <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
<th>MD5</th>
<% if delete_allowed %><th></th><% end %>
</tr></thead>
diff --git a/app/views/queries/_filters.rhtml b/app/views/queries/_filters.rhtml
index 458d7139e..ec9d4fef6 100644
--- a/app/views/queries/_filters.rhtml
+++ b/app/views/queries/_filters.rhtml
@@ -59,19 +59,19 @@ function toggle_multi_select(field) {
<table width="100%">
<tr>
<td>
-<table style="padding:0;">
+<table>
<% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
<% field = filter[0]
options = filter[1] %>
- <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>">
- <td valign="top" style="width:200px;">
+ <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}" %>
<label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
</td>
- <td valign="top" style="width:150px;">
+ <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;" %>
</td>
- <td valign="top">
+ <td>
<div id="div_values_<%= field %>" style="display:none;">
<% case options[:type]
when :list, :list_optional, :list_status, :list_subprojects %>
@@ -93,7 +93,7 @@ function toggle_multi_select(field) {
<% end %>
</table>
</td>
-<td align="right" valign="top">
+<td class="add-filter">
<%= l(:label_filter_add) %>:
<%= select_tag 'add_filter_select', options_for_select([["",""]] + query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.collect{|field| [ field[1][:name] || l(("field_"+field[0].to_s.gsub(/\_id$/, "")).to_sym), field[0]] unless query.has_filter?(field[0])}.compact), :onchange => "add_filter();", :class => "select-small" %>
</td>
diff --git a/app/views/queries/_form.rhtml b/app/views/queries/_form.rhtml
index 2d4b96fd1..8da264032 100644
--- a/app/views/queries/_form.rhtml
+++ b/app/views/queries/_form.rhtml
@@ -6,11 +6,16 @@
<p><label for="query_name"><%=l(:field_name)%></label>
<%= text_field 'query', 'name', :size => 80 %></p>
-<% if current_role.allowed_to?(:manage_public_queries) %>
- <p><label for="query_is_public"><%=l(:field_is_public)%></label>
- <%= check_box 'query', 'is_public' %></p>
+<% if User.current.admin? || (@project && current_role.allowed_to?(:manage_public_queries)) %>
+<p><label for="query_is_public"><%=l(:field_is_public)%></label>
+<%= check_box 'query', 'is_public',
+ :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p>
<% end %>
+<p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
+<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
+ :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
+
<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
<%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
:onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
diff --git a/app/views/repositories/revision.rhtml b/app/views/repositories/revision.rhtml
index 5a7ef1fd5..f1e176669 100644
--- a/app/views/repositories/revision.rhtml
+++ b/app/views/repositories/revision.rhtml
@@ -49,7 +49,7 @@
<td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td>
<td align="right">
<% if change.action == "M" %>
-<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => change.path, :rev => @changeset.revision %>
+<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.path), :rev => @changeset.revision %>
<% end %>
</td>
</tr>
diff --git a/app/views/timelog/_date_range.rhtml b/app/views/timelog/_date_range.rhtml
new file mode 100644
index 000000000..ed84b16cf
--- /dev/null
+++ b/app/views/timelog/_date_range.rhtml
@@ -0,0 +1,28 @@
+<fieldset id="filters"><legend><%= l(:label_date_range) %></legend>
+<p>
+<%= radio_button_tag 'period_type', '1', !@free_period %>
+<%= select_tag 'period', options_for_period_select(params[:period]),
+ :onchange => 'this.form.onsubmit();',
+ :onfocus => '$("period_type_1").checked = true;' %>
+</p>
+<p>
+<%= radio_button_tag 'period_type', '2', @free_period %>
+<span onclick="$('period_type_2').checked = true;">
+<%= l(:label_date_from) %>
+<%= text_field_tag 'from', @from, :size => 10 %> <%= calendar_for('from') %>
+<%= l(:label_date_to) %>
+<%= text_field_tag 'to', @to, :size => 10 %> <%= calendar_for('to') %>
+</span>
+<%= submit_tag l(:button_apply), :name => nil %>
+</p>
+</fieldset>
+
+<div class="tabs">
+<% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
+<ul>
+ <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project }),
+ :class => (@controller.action_name == 'details' ? 'selected' : nil)) %></li>
+ <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project}),
+ :class => (@controller.action_name == 'report' ? 'selected' : nil)) %></li>
+</ul>
+</div>
diff --git a/app/views/timelog/_list.rhtml b/app/views/timelog/_list.rhtml
index 67e3c67d5..189f4f5e8 100644
--- a/app/views/timelog/_list.rhtml
+++ b/app/views/timelog/_list.rhtml
@@ -1,5 +1,6 @@
<table class="list time-entries">
<thead>
+<tr>
<%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
<%= sort_header_tag('user_id', :caption => l(:label_member)) %>
<%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
@@ -8,6 +9,7 @@
<th><%= l(:field_comments) %></th>
<%= sort_header_tag('hours', :caption => l(:field_hours)) %>
<th></th>
+</tr>
</thead>
<tbody>
<% entries.each do |entry| -%>
@@ -35,5 +37,5 @@
</td>
</tr>
<% end -%>
-</tbdoy>
+</tbody>
</table>
diff --git a/app/views/timelog/_report_criteria.rhtml b/app/views/timelog/_report_criteria.rhtml
index b048c874a..94f3d20f9 100644
--- a/app/views/timelog/_report_criteria.rhtml
+++ b/app/views/timelog/_report_criteria.rhtml
@@ -1,14 +1,16 @@
-<% @hours.collect {|h| h[criterias[level]]}.uniq.each do |value| %>
+<% @hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value| %>
<% hours_for_value = select_hours(hours, criterias[level], value) -%>
<% next if hours_for_value.empty? -%>
<tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
<%= '<td></td>' * level %>
-<td><%= value.nil? ? l(:label_none) : @available_criterias[criterias[level]][:klass].find_by_id(value) %></td>
+<td><%= format_criteria_value(criterias[level], value) %></td>
<%= '<td></td>' * (criterias.length - level - 1) -%>
+ <% total = 0 -%>
<% @periods.each do |period| -%>
- <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)) %>
+ <% sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s)); total += sum -%>
<td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
<% end -%>
+ <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
</tr>
<% if criterias.length > level+1 -%>
<%= render(:partial => 'report_criteria', :locals => {:criterias => criterias, :hours => hours_for_value, :level => (level + 1)}) %>
diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/details.rhtml
index cba1597d1..f02da9959 100644
--- a/app/views/timelog/details.rhtml
+++ b/app/views/timelog/details.rhtml
@@ -1,5 +1,4 @@
<div class="contextual">
-<%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}, :class => 'icon icon-report') %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
</div>
@@ -12,23 +11,7 @@
<% form_remote_tag( :url => {}, :method => :get, :update => 'content' ) do %>
<%= hidden_field_tag 'project_id', params[:project_id] %>
<%= hidden_field_tag 'issue_id', params[:issue_id] if @issue %>
-
-<fieldset><legend><%= l(:label_date_range) %></legend>
-<p>
-<%= radio_button_tag 'period_type', '1', !@free_period %>
-<%= select_tag 'period', options_for_period_select(params[:period]),
- :onchange => 'this.form.onsubmit();',
- :onfocus => '$("period_type_1").checked = true;' %>
-</p>
-<p>
-<%= radio_button_tag 'period_type', '2', @free_period %>
-<%= l(:label_date_from) %>
-<%= text_field_tag 'from', @from, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('from') %>
-<%= l(:label_date_to) %>
-<%= text_field_tag 'to', @to, :size => 10, :onfocus => '$("period_type_2").checked = true;' %> <%= calendar_for('to') %>
-<%= submit_tag l(:button_apply), :name => nil, :onclick => '$("period_type_2").checked = true;' %>
-</p>
-</fieldset>
+<%= render :partial => 'date_range' %>
<% end %>
<div class="total-hours">
@@ -45,9 +28,4 @@
</p>
<% end %>
-<% content_for :header_tags do %>
- <%= javascript_include_tag 'calendar/calendar' %>
- <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
- <%= javascript_include_tag 'calendar/calendar-setup' %>
- <%= stylesheet_link_tag 'calendar' %>
-<% end %>
+<% html_title l(:label_spent_time), l(:label_details) %>
diff --git a/app/views/timelog/edit.rhtml b/app/views/timelog/edit.rhtml
index 13d76f1ef..f9dae8a99 100644
--- a/app/views/timelog/edit.rhtml
+++ b/app/views/timelog/edit.rhtml
@@ -2,6 +2,7 @@
<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :project_id => @time_entry.project} do |f| %>
<%= error_messages_for 'time_entry' %>
+<%= back_url_hidden_field_tag %>
<div class="box">
<p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p>
@@ -14,10 +15,3 @@
<%= submit_tag l(:button_save) %>
<% end %>
-
-<% content_for :header_tags do %>
-<%= javascript_include_tag 'calendar/calendar' %>
-<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
-<%= javascript_include_tag 'calendar/calendar-setup' %>
-<%= stylesheet_link_tag 'calendar' %>
-<% end %> \ No newline at end of file
diff --git a/app/views/timelog/report.rhtml b/app/views/timelog/report.rhtml
index 8fc15a3b4..97251bc11 100644
--- a/app/views/timelog/report.rhtml
+++ b/app/views/timelog/report.rhtml
@@ -1,36 +1,32 @@
<div class="contextual">
-<%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}, :class => 'icon icon-details') %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time' %>
</div>
<h2><%= l(:label_spent_time) %></h2>
-<% form_remote_tag(:url => {:project_id => @project}, :update => 'content') do %>
+<% form_remote_tag(:url => {}, :update => 'content') do %>
<% @criterias.each do |criteria| %>
- <%= hidden_field_tag 'criterias[]', criteria %>
+ <%= hidden_field_tag 'criterias[]', criteria, :id => nil %>
<% end %>
- <fieldset><legend><%= l(:label_date_range) %></legend>
- <p>
- <%= l(:label_date_from) %>
- <%= text_field_tag 'date_from', @date_from, :size => 10 %><%= calendar_for('date_from') %>
- <%= l(:label_date_to) %>
- <%= text_field_tag 'date_to', @date_to, :size => 10 %><%= calendar_for('date_to') %>
- <%= l(:label_details) %>
- <%= select_tag 'period', options_for_select([[l(:label_year), 'year'],
- [l(:label_month), 'month'],
- [l(:label_week), 'week']], @columns) %>
- &nbsp;
- <%= submit_tag l(:button_apply) %>
- </p>
- </fieldset>
+ <%= hidden_field_tag 'project_id', params[:project_id] %>
+ <%= render :partial => 'date_range' %>
- <p><%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
+ <p><%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'],
+ [l(:label_month), 'month'],
+ [l(:label_week), 'week'],
+ [l(:label_day_plural).titleize, 'day']], @columns),
+ :onchange => "this.form.onsubmit();" %>
+
+ <%= l(:button_add) %>: <%= select_tag('criterias[]', options_for_select([[]] + (@available_criterias.keys - @criterias).collect{|k| [l(@available_criterias[k][:label]), k]}),
:onchange => "this.form.onsubmit();",
:style => 'width: 200px',
+ :id => nil,
:disabled => (@criterias.length >= 3)) %>
- <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :date_from => @date_from, :date_to => @date_to, :period => @columns}, :update => 'content'},
- :class => 'icon icon-reload' %></p>
-
+ <%= link_to_remote l(:button_clear), {:url => {:project_id => @project, :period_type => params[:period_type], :period => params[:period], :from => @from, :to => @to, :columns => @columns},
+ :update => 'content'
+ }, :class => 'icon icon-reload' %></p>
+<% end %>
+
<% unless @criterias.empty? %>
<div class="total-hours">
<p><%= l(:label_total) %>: <%= html_hours(lwr(:label_f_hour, @total_hours)) %></p>
@@ -41,11 +37,13 @@
<thead>
<tr>
<% @criterias.each do |criteria| %>
- <th width="15%"><%= l(@available_criterias[criteria][:label]) %></th>
+ <th><%= l(@available_criterias[criteria][:label]) %></th>
<% end %>
+<% columns_width = (40 / (@periods.length+1)).to_i %>
<% @periods.each do |period| %>
- <th width="<%= ((100 - @criterias.length * 15 - 15 ) / @periods.length).to_i %>%"><%= period %></th>
+ <th class="period" width="<%= columns_width %>%"><%= period %></th>
<% end %>
+ <th class="total" width="<%= columns_width %>%"><%= l(:label_total) %></th>
</tr>
</thead>
<tbody>
@@ -53,20 +51,22 @@
<tr class="total">
<td><%= l(:label_total) %></td>
<%= '<td></td>' * (@criterias.size - 1) %>
+ <% total = 0 -%>
<% @periods.each do |period| -%>
- <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)) %>
+ <% sum = sum_hours(select_hours(@hours, @columns, period.to_s)); total += sum -%>
<td class="hours"><%= html_hours("%.2f" % sum) if sum > 0 %></td>
<% end -%>
+ <td class="hours"><%= html_hours("%.2f" % total) if total > 0 %></td>
</tr>
</tbody>
</table>
-<% end %>
+
+<p class="other-formats">
+<%= l(:label_export_to) %>
+<span><%= link_to 'CSV', params.merge({:format => 'csv'}), :class => 'csv' %></span>
+</p>
<% end %>
<% end %>
-<% content_for :header_tags do %>
-<%= javascript_include_tag 'calendar/calendar' %>
-<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
-<%= javascript_include_tag 'calendar/calendar-setup' %>
-<%= stylesheet_link_tag 'calendar' %>
-<% end %>
+<% html_title l(:label_spent_time), l(:label_report) %>
+
diff --git a/app/views/users/_form.rhtml b/app/views/users/_form.rhtml
index d32399c60..ff4278c1f 100644
--- a/app/views/users/_form.rhtml
+++ b/app/views/users/_form.rhtml
@@ -30,10 +30,3 @@
</div>
</div>
<!--[eoform:user]-->
-
-<% content_for :header_tags do %>
-<%= javascript_include_tag 'calendar/calendar' %>
-<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
-<%= javascript_include_tag 'calendar/calendar-setup' %>
-<%= stylesheet_link_tag 'calendar' %>
-<% end %> \ No newline at end of file
diff --git a/app/views/users/list.rhtml b/app/views/users/list.rhtml
index e12aa3425..d89672d19 100644
--- a/app/views/users/list.rhtml
+++ b/app/views/users/list.rhtml
@@ -33,17 +33,7 @@
<td align="center"><%= image_tag('true.png') if user.admin? %></td>
<td class="created_on" align="center"><%= format_time(user.created_on) %></td>
<td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
- <td>
- <small>
- <% if user.locked? -%>
- <%= link_to l(:button_unlock), {:action => 'edit', :id => user, :user => {:status => User::STATUS_ACTIVE}}, :method => :post, :class => 'icon icon-unlock' %>
- <% elsif user.registered? -%>
- <%= link_to l(:button_activate), {:action => 'edit', :id => user, :user => {:status => User::STATUS_ACTIVE}}, :method => :post, :class => 'icon icon-unlock' %>
- <% else -%>
- <%= link_to l(:button_lock), {:action => 'edit', :id => user, :user => {:status => User::STATUS_LOCKED}}, :method => :post, :class => 'icon icon-lock' %>
- <% end -%>
- </small>
- </td>
+ <td><small><%= change_status_link(user) %></small></td>
</tr>
<% end -%>
</tbody>
diff --git a/app/views/versions/_form.rhtml b/app/views/versions/_form.rhtml
index e18f912bf..adc83b573 100644
--- a/app/views/versions/_form.rhtml
+++ b/app/views/versions/_form.rhtml
@@ -6,10 +6,3 @@
<p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
<p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
</div>
-
-<% content_for :header_tags do %>
-<%= javascript_include_tag 'calendar/calendar' %>
-<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
-<%= javascript_include_tag 'calendar/calendar-setup' %>
-<%= stylesheet_link_tag 'calendar' %>
-<% end %>
diff --git a/config/environment.rb b/config/environment.rb
index 2d581168b..7878eca47 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -4,6 +4,9 @@
# you don't control web/app server and can't set it the proper way
# ENV['RAILS_ENV'] ||= 'production'
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
+
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
diff --git a/doc/CHANGELOG b/doc/CHANGELOG
index 9ced8b4c0..b39185151 100644
--- a/doc/CHANGELOG
+++ b/doc/CHANGELOG
@@ -5,6 +5,38 @@ Copyright (C) 2006-2008 Jean-Philippe Lang
http://www.redmine.org/
+== 2008-04-28 v0.7.0
+
+* Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
+* Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
+* Add predefined date ranges to the time report
+* Time report can be done at issue level
+* Various timelog report enhancements
+* Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
+* Display the context menu above and/or to the left of the click if needed
+* Make the admin project files list sortable
+* Mercurial: display working directory files sizes unless browsing a specific revision
+* Preserve status filter and page number when using lock/unlock/activate links on the users list
+* Redmine.pm support for LDAP authentication
+* Better error message and AR errors in log for failed LDAP on-the-fly user creation
+* Redirected user to where he is coming from after logging hours
+* Warn user that subprojects are also deleted when deleting a project
+* Include subprojects versions on calendar and gantt
+* Notify project members when a message is posted if they want to receive notifications
+* Fixed: Feed content limit setting has no effect
+* Fixed: Priorities not ordered when displayed as a filter in issue list
+* Fixed: can not display attached images inline in message replies
+* Fixed: Boards are not deleted when project is deleted
+* Fixed: trying to preview a new issue raises an exception with postgresql
+* Fixed: single file 'View difference' links do not work because of duplicate slashes in url
+* Fixed: inline image not displayed when including a wiki page
+* Fixed: CVS duplicate key violation
+* Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
+* Fixed: custom field filters behaviour
+* Fixed: Postgresql 8.3 compatibility
+* Fixed: Links to repository directories don't work
+
+
== 2008-03-29 v0.7.0-rc1
* Overall activity view and feed added, link is available on the project list
diff --git a/extra/svn/Redmine.pm b/extra/svn/Redmine.pm
index b76622e3d..6f3ba4385 100644
--- a/extra/svn/Redmine.pm
+++ b/extra/svn/Redmine.pm
@@ -8,8 +8,8 @@ against redmine database
=head1 SYNOPSIS
This module allow anonymous users to browse public project and
-registred users to browse and commit their project. authentication is
-done on the redmine database.
+registred users to browse and commit their project. Authentication is
+done against the redmine database or the LDAP configured in redmine.
This method is far simpler than the one with pam_* and works with all
database without an hassle but you need to have apache/mod_perl on the
@@ -29,6 +29,11 @@ On debian/ubuntu you must do :
aptitude install libapache-dbi-perl libapache2-mod-perl2 libdbd-mysql-perl
+If your Redmine users use LDAP authentication, you will also need
+Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
+
+ aptitude install libauthen-simple-ldap-perl libio-socket-ssl-perl
+
=head1 CONFIGURATION
## if the module isn't in your perl path
@@ -90,6 +95,8 @@ use strict;
use DBI;
use Digest::SHA1;
+# optional module for LDAP authentication
+my $CanUseLDAPAuth = eval("use Authen::Simple::LDAP; 1");
use Apache2::Module;
use Apache2::Access;
@@ -140,7 +147,7 @@ sub is_public_project {
my $dbh = connect_database($r);
my $sth = $dbh->prepare(
- "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
+ "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
);
$sth->execute($project_id);
@@ -176,17 +183,37 @@ sub is_member {
my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
my $sth = $dbh->prepare(
- "SELECT hashed_password FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;"
+ "SELECT hashed_password, auth_source_id FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;"
);
$sth->execute($redmine_user, $project_id);
my $ret;
while (my @row = $sth->fetchrow_array) {
- if ($row[0] eq $pass_digest) {
- $ret = 1;
- last;
+ unless ($row[1]) {
+ if ($row[0] eq $pass_digest) {
+ $ret = 1;
+ last;
+ }
+ } elsif ($CanUseLDAPAuth) {
+ my $sthldap = $dbh->prepare(
+ "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
+ );
+ $sthldap->execute($row[1]);
+ while (my @rowldap = $sthldap->fetchrow_array) {
+ my $ldap = Authen::Simple::LDAP->new(
+ host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
+ port => $rowldap[1],
+ basedn => $rowldap[5],
+ binddn => $rowldap[3] ? $rowldap[3] : "",
+ bindpw => $rowldap[4] ? $rowldap[4] : "",
+ filter => "(".$rowldap[6]."=%s)"
+ );
+ $ret = 1 if ($ldap->authenticate($redmine_user, $redmine_pass));
+ }
+ $sthldap->finish();
}
}
+ $sth->finish();
$dbh->disconnect();
$ret;
diff --git a/lang/bg.yml b/lang/bg.yml
index a49365d20..b341d989f 100644
--- a/lang/bg.yml
+++ b/lang/bg.yml
@@ -48,8 +48,8 @@ general_text_no: 'не'
general_text_yes: 'да'
general_lang_name: 'Bulgarian'
general_csv_separator: ','
-general_csv_encoding: cp1251
-general_pdf_encoding: cp1251
+general_csv_encoding: UTF-8
+general_pdf_encoding: UTF-8
general_day_names: Понеделник,Вторник,СрÑда,Четвъртък,Петък,Събота,ÐеделÑ
general_first_day_of_week: '1'
@@ -57,11 +57,11 @@ notice_account_updated: Профилът е обновен уÑпешно.
notice_account_invalid_creditentials: Ðевалиден потребител или парола.
notice_account_password_updated: Паролата е уÑпешно променена.
notice_account_wrong_password: Грешна парола
-notice_account_register_done: Ðкаунтът е Ñъздаден уÑпешно.
-notice_account_unknown_email: Ðепознат потребител.
-notice_can_t_change_password: Този акаунт е Ñ Ð²ÑŠÐ½ÑˆÐµÐ½ метод за оторизациÑ. Ðевъзможна ÑмÑна на паролата.
+notice_account_register_done: Профилът е Ñъздаден уÑпешно.
+notice_account_unknown_email: Ðепознат e-mail.
+notice_can_t_change_password: Този профил е Ñ Ð²ÑŠÐ½ÑˆÐµÐ½ метод за оторизациÑ. Ðевъзможна ÑмÑна на паролата.
notice_account_lost_email_sent: Изпратен ви е e-mail Ñ Ð¸Ð½Ñтрукции за избор на нова парола.
-notice_account_activated: Ðкаунтът ви е активиран. Вече може да влезете.
+notice_account_activated: Профилът ви е активиран. Вече може да влезете в ÑиÑтемата.
notice_successful_create: УÑпешно Ñъздаване.
notice_successful_update: УÑпешно обновÑване.
notice_successful_delete: УÑпешно изтриване.
@@ -78,8 +78,8 @@ error_scm_command_failed: "Грешка при опит за комуникацÐ
mail_subject_lost_password: Вашата парола (%s)
mail_body_lost_password: 'За да Ñмените паролата Ñи, използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:'
-mail_subject_register: ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð½Ð° акаунт (%s)
-mail_body_register: 'За да активирате акаунта Ñи използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:'
+mail_subject_register: ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð½Ð° профил (%s)
+mail_body_register: 'За да активирате профила Ñи използвайте ÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð»Ð¸Ð½Ðº:'
gui_validation_error: 1 грешка
gui_validation_error_plural: %d грешки
@@ -113,11 +113,11 @@ field_notes: Бележка
field_is_closed: Затворена задача
field_is_default: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿Ð¾ подразбиране
field_tracker: Тракер
-field_subject: Тема
+field_subject: ОтноÑно
field_due_date: Крайна дата
field_assigned_to: Възложена на
field_priority: Приоритет
-field_fixed_version: Target version
+field_fixed_version: Планувана верÑиÑ
field_user: Потребител
field_role: РолÑ
field_homepage: Ðачална Ñтраница
@@ -138,7 +138,7 @@ field_version: ВерÑиÑ
field_type: Тип
field_host: ХоÑÑ‚
field_port: Порт
-field_account: Ðкаунт
+field_account: Профил
field_base_dn: Base DN
field_attr_login: Login attribute
field_attr_firstname: Firstname attribute
@@ -163,7 +163,7 @@ field_delay: ОтмеÑтване
field_assignable: Възможно е възлагане на задачи за тази ролÑ
field_redirect_existing_links: ПренаÑочване на ÑъщеÑтвуващи линкове
field_estimated_hours: ИзчиÑлено време
-field_default_value: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿Ð¾ подразбиране
+field_default_value: СтойноÑÑ‚ по подразбиране
setting_app_title: Заглавие
setting_app_subtitle: ОпиÑание
@@ -171,15 +171,15 @@ setting_welcome_text: Допълнителен текÑÑ‚
setting_default_language: Език по подразбиране
setting_login_required: ИзиÑкване за вход в ÑиÑтемата
setting_self_registration: РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð¾Ñ‚ потребители
-setting_attachment_max_size: МакÑимално голÑм приложен файл
+setting_attachment_max_size: МакÑимална големина на прикачен файл
setting_issues_export_limit: Лимит за екÑпорт на задачи
setting_mail_from: E-mail Ð°Ð´Ñ€ÐµÑ Ð·Ð° емиÑии
setting_host_name: ХоÑÑ‚
setting_text_formatting: Форматиране на текÑта
setting_wiki_compression: Wiki компреÑиране на иÑториÑта
setting_feeds_limit: Лимит на Feeds
-setting_autofetch_changesets: Ðвтоматично обработване на commits в хранилището
-setting_sys_api_enabled: Разрешаване на WS за управление на хранилището
+setting_autofetch_changesets: Ðвтоматично обработване на ревизиите
+setting_sys_api_enabled: Разрешаване на WS за управление
setting_commit_ref_keywords: ОтбелÑзващи ключови думи
setting_commit_fix_keywords: Приключващи ключови думи
setting_autologin: Ðвтоматичен вход
@@ -231,7 +231,7 @@ label_password_lost: Забравена парола
label_home: Ðачало
label_my_page: Лична Ñтраница
label_my_account: Профил
-label_my_projects: Моите проекти
+label_my_projects: Проекти, в които учаÑтвам
label_administration: ÐдминиÑтрациÑ
label_login: Вход
label_logout: Изход
@@ -375,8 +375,8 @@ label_f_hour_plural: %.2f чаÑа
label_time_tracking: ОтделÑне на време
label_change_plural: Промени
label_statistics: СтатиÑтики
-label_commits_per_month: Commits за меÑец
-label_commits_per_author: Commits за автор
+label_commits_per_month: Ревизии по меÑеци
+label_commits_per_author: Ревизии по автор
label_view_diff: Виж разликите
label_diff_inline: хоризонтално
label_diff_side_by_side: вертикално
@@ -389,7 +389,7 @@ label_applied_status: Промени ÑтатуÑа на
label_loading: Зареждане...
label_relation_new: Ðова релациÑ
label_relation_delete: Изтриване на релациÑ
-label_relates_to: Свързана ÑÑŠÑ
+label_relates_to: Ñвързана ÑÑŠÑ
label_duplicates: дублира
label_blocks: блокира
label_blocked_by: блокирана от
@@ -427,10 +427,10 @@ label_updated_time: Обновена преди %s
label_jump_to_a_project: Проект...
button_login: Вход
-button_submit: Приложи
+button_submit: Прикачване
button_save: ЗапиÑ
-button_check_all: Маркирай вÑички
-button_uncheck_all: ИзчиÑти вÑички
+button_check_all: Избор на вÑички
+button_uncheck_all: ИзчиÑтване на вÑички
button_delete: Изтриване
button_create: Създаване
button_test: ТеÑÑ‚
@@ -481,9 +481,9 @@ text_length_between: От %d до %d Ñимвола.
text_tracker_no_workflow: ÐÑма дефиниран работен Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð·Ð° този тракер
text_unallowed_characters: Ðепозволени Ñимволи
text_comma_separated: Позволено е изброÑване (Ñ Ñ€Ð°Ð·Ð´ÐµÐ»Ð¸Ñ‚ÐµÐ» запетаÑ).
-text_issues_ref_in_commit_messages: ОтбелÑзване и приключване на задачи от commit ÑъобщениÑ
-text_issue_added: Публикувана е нова задача Ñ Ð½Ð¾Ð¼ÐµÑ€ %s (by %s).
-text_issue_updated: Задача %s е обновена (by %s).
+text_issues_ref_in_commit_messages: ОтбелÑзване и приключване на задачи от ревизии
+text_issue_added: Публикувана е нова задача Ñ Ð½Ð¾Ð¼ÐµÑ€ %s (от %s).
+text_issue_updated: Задача %s е обновена (от %s).
text_wiki_destroy_confirmation: Сигурни ли Ñте, че иÑкате да изтриете това Wiki и цÑлото му Ñъдържание?
text_issue_category_destroy_question: Има задачи (%d) обвързани Ñ Ñ‚Ð°Ð·Ð¸ категориÑ. Какво ще изберете?
text_issue_category_destroy_assignments: Премахване на връзките Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñта
@@ -515,11 +515,11 @@ enumeration_issue_priorities: Приоритети на задачи
enumeration_doc_categories: Категории документи
enumeration_activities: ДейноÑти (time tracking)
label_file_plural: Файлове
-label_changeset_plural: Changesets
+label_changeset_plural: Ревизии
field_column_names: Колони
label_default_columns: По подразбиране
setting_issue_list_default_columns: Показвани колони по подразбиране
-setting_repositories_encodings: Кодови таблици на хранилищата
+setting_repositories_encodings: Кодови таблици
notice_no_issue_selected: "ÐÑма избрани задачи."
label_bulk_edit_selected_issues: Редактиране на задачи
label_no_change_option: (Без промÑна)
@@ -536,20 +536,20 @@ label_user_mail_option_none: "Само за наблюдавани или в кÐ
setting_emails_footer: ПодтекÑÑ‚ за e-mail
label_float: Дробно
button_copy: Копиране
-mail_body_account_information_external: Можете да използвате Ð²Ð°ÑˆÐ¸Ñ "%s" акаунт за вход.
-mail_body_account_information: ИнформациÑта за акаунта
+mail_body_account_information_external: Можете да използвате Ð²Ð°ÑˆÐ¸Ñ "%s" профил за вход.
+mail_body_account_information: ИнформациÑта за профила ви
setting_protocol: Протокол
label_user_mail_no_self_notified: "Ðе иÑкам извеÑÑ‚Ð¸Ñ Ð·Ð° извършени от мен промени"
setting_time_format: Формат на чаÑа
-label_registration_activation_by_email: активиране на акаунта по email
-mail_subject_account_activation_request: ЗаÑвка за активиране на акаунт в %s
+label_registration_activation_by_email: активиране на профила по email
+mail_subject_account_activation_request: ЗаÑвка за активиране на профил в %s
mail_body_account_activation_request: 'Има новорегиÑтриран потребител (%s), очакващ вашето одобрение:'
label_registration_automatic_activation: автоматично активиране
label_registration_manual_activation: ръчно активиране
-notice_account_pending: "Ðкаунтът Ви е Ñъздаден и очаква одобрение от админиÑтратор."
+notice_account_pending: "Профилът Ви е Ñъздаден и очаква одобрение от админиÑтратор."
field_time_zone: ЧаÑова зона
text_caracters_minimum: Минимум %d Ñимвола.
-setting_bcc_recipients: Blind carbon copy (bcc) получатели
+setting_bcc_recipients: Получатели на Ñкрито копие (bcc)
button_annotate: ÐнотациÑ
label_issues_by: Задачи по %s
field_searchable: С възможноÑÑ‚ за Ñ‚ÑŠÑ€Ñене
@@ -566,54 +566,55 @@ label_general: ОÑновни
label_repository_plural: Хранилища
label_associated_revisions: ÐÑоциирани ревизии
setting_user_format: ПотребителÑки формат
-text_status_changed_by_changeset: Applied in changeset %s.
-label_more: More
-text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
-label_scm: SCM
-text_select_project_modules: 'Select modules to enable for this project:'
-label_issue_added: Issue added
-label_issue_updated: Issue updated
-label_document_added: Document added
-label_message_posted: Message added
-label_file_added: File added
-label_news_added: News added
-project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+text_status_changed_by_changeset: Приложено Ñ Ñ€ÐµÐ²Ð¸Ð·Ð¸Ñ %s.
+label_more: Още
+text_issues_destroy_confirmation: 'Сигурни ли Ñте, че иÑкате да изтриете избраните задачи?'
+label_scm: SCM (СиÑтема за контрол на кода)
+text_select_project_modules: 'Изберете активните модули за този проект:'
+label_issue_added: Добавена задача
+label_issue_updated: Обновена задача
+label_document_added: Добавен документ
+label_message_posted: Добавено Ñъобщение
+label_file_added: Добавен файл
+label_news_added: Добавена новина
+project_module_boards: Форуми
+project_module_issue_tracking: Тракинг
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
-project_module_repository: Repository
-project_module_news: News
-project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
-text_rmagick_available: RMagick available (optional)
-button_configure: Configure
-label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
+project_module_files: Файлове
+project_module_documents: Документи
+project_module_repository: Хранилище
+project_module_news: Ðовини
+project_module_time_tracking: ОтделÑне на време
+text_file_repository_writable: ВъзможноÑÑ‚ за пиÑане в хранилището Ñ Ñ„Ð°Ð¹Ð»Ð¾Ð²Ðµ
+text_default_administrator_account_changed: Сменен Ñ„Ð°Ð±Ñ€Ð¸Ñ‡Ð½Ð¸Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸ÑтраторÑки профил
+text_rmagick_available: Ðаличен RMagick (по избор)
+button_configure: Конфигуриране
+label_plugins: Плъгини
+label_ldap_authentication: LDAP оторизациÑ
label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+label_this_month: Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ Ð¼ÐµÑец
+label_last_n_days: поÑледните %d дни
+label_all_time: вÑички
+label_this_year: текущата година
+label_date_range: Период
+label_last_week: поÑледната Ñедмица
+label_yesterday: вчера
+label_last_month: поÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð¼ÐµÑец
+label_add_another_file: ДобавÑне на друг файл
+label_optional_description: Ðезадължително опиÑание
+text_destroy_time_entries_question: %.02f чаÑа Ñа отделени на задачите, които иÑкате да изтриете. Какво избирате?
+error_issue_not_found_in_project: 'Задачата не е намерена или не принадлежи на този проект'
+text_assign_time_entries_to_project: ПрехвърлÑне на отделеното време към проект
+text_destroy_time_entries: Изтриване на отделеното време
+text_reassign_time_entries: 'ПрехвърлÑне на отделеното време към задача:'
+setting_activity_days_default: Брой дни показвани на таб ДейноÑÑ‚
+label_chronological_order: Хронологичен ред
+field_comments_sorting: Сортиране на коментарите
+label_reverse_chronological_order: Обратен хронологичен ред
+label_preferences: ПредпочитаниÑ
+setting_display_subprojects_issues: Показване на подпроектите в проектите по подразбиране
+label_overall_activity: ЦÑлоÑтна дейноÑÑ‚
+setting_default_projects_public: Ðовите проекти Ñа публични по подразбиране
+error_scm_annotate: "Обектът не ÑъщеÑтвува или не може да бъде анотиран."
+label_planning: Планиране
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/cs.yml b/lang/cs.yml
index 03702eaa2..250c602c2 100644
--- a/lang/cs.yml
+++ b/lang/cs.yml
@@ -10,16 +10,16 @@ actionview_datehelper_select_month_prefix:
actionview_datehelper_select_year_prefix:
actionview_datehelper_time_in_words_day: 1 den
actionview_datehelper_time_in_words_day_plural: %d dny
-actionview_datehelper_time_in_words_hour_about: asi hodinu
-actionview_datehelper_time_in_words_hour_about_plural: asi %d hodin
-actionview_datehelper_time_in_words_hour_about_single: asi hodinu
-actionview_datehelper_time_in_words_minute: 1 minuta
-actionview_datehelper_time_in_words_minute_half: půl minuty
-actionview_datehelper_time_in_words_minute_less_than: méně než minutu
-actionview_datehelper_time_in_words_minute_plural: %d minut
-actionview_datehelper_time_in_words_minute_single: 1 minuta
-actionview_datehelper_time_in_words_second_less_than: méně než sekunda
-actionview_datehelper_time_in_words_second_less_than_plural: méně než %d sekund
+actionview_datehelper_time_in_words_hour_about: asi hodinou
+actionview_datehelper_time_in_words_hour_about_plural: asi %d hodinami
+actionview_datehelper_time_in_words_hour_about_single: asi hodinou
+actionview_datehelper_time_in_words_minute: 1 minutou
+actionview_datehelper_time_in_words_minute_half: půl minutou
+actionview_datehelper_time_in_words_minute_less_than: méně než minutou
+actionview_datehelper_time_in_words_minute_plural: %d minutami
+actionview_datehelper_time_in_words_minute_single: 1 minutou
+actionview_datehelper_time_in_words_second_less_than: méně než sekundou
+actionview_datehelper_time_in_words_second_less_than_plural: méně než %d sekundami
actionview_instancetag_blank_option: Prosím vyberte
activerecord_error_inclusion: není zahrnuto v seznamu
@@ -98,7 +98,7 @@ mail_body_account_activation_request: Byl zaregistrován nový uživatel "%s". A
gui_validation_error: 1 chyba
gui_validation_error_plural: %d chyb(y)
-field_name: Jméno
+field_name: Název
field_description: Popis
field_summary: Přehled
field_is_required: Povinné pole
@@ -306,7 +306,7 @@ label_attribute: Atribut
label_attribute_plural: Atributy
label_download: %d Download
label_download_plural: %d Downloads
-label_no_data: Žádná data k zobrazení
+label_no_data: Žádné položky
label_change_status: Změnit stav
label_history: Historie
label_attachment: Soubor
@@ -480,8 +480,8 @@ label_sort_by: Seřadit podle %s
label_send_test_email: Poslat testovací email
label_feeds_access_key_created_on: Přístupový klÃ­Ä pro RSS byl vytvoÅ™en pÅ™ed %s
label_module_plural: Moduly
-label_added_time_by: 'Přidáno před: %s %s'
-label_updated_time: 'Aktualizováno před: %s'
+label_added_time_by: 'Přidáno uživatelem %s před %s'
+label_updated_time: 'Aktualizováno před %s'
label_jump_to_a_project: Zvolit projekt...
label_file_plural: Soubory
label_changeset_plural: Changesety
@@ -586,7 +586,7 @@ text_no_configuration_data: "Role, fronty, stavy úkolů ani workflow nebyly zat
text_load_default_configuration: Nahrát výchozí konfiguraci
text_status_changed_by_changeset: Použito v changesetu %s.
text_issues_destroy_confirmation: 'Opravdu si přejete odstranit všechny zvolené úkoly?'
-text_select_project_modules: 'Zvolte moduly aktivní v tomto projektu:'
+text_select_project_modules: 'Aktivní moduly v tomto projektu:'
text_default_administrator_account_changed: Výchozí nastavení administrátorského úÄtu zmÄ›nÄ›no
text_file_repository_writable: Povolen zápis do repository
text_rmagick_available: RMagick k dispozici (volitelné)
@@ -620,5 +620,6 @@ default_activity_development: Vývoj
enumeration_issue_priorities: Priority úkolů
enumeration_doc_categories: Kategorie dokumentů
enumeration_activities: Aktivity (sledování Äasu)
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+error_scm_annotate: "Položka neexistuje nebo nemůže být komentována."
+label_planning: Plánování
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/da.yml b/lang/da.yml
index b0a6764bb..ff2ed982d 100644
--- a/lang/da.yml
+++ b/lang/da.yml
@@ -619,3 +619,4 @@ label_overall_activity: Overordnet aktivitet
setting_default_projects_public: Nye projekter er offentlige som default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planlægning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/de.yml b/lang/de.yml
index 17c6ac617..77184cf88 100644
--- a/lang/de.yml
+++ b/lang/de.yml
@@ -71,7 +71,7 @@ notice_locking_conflict: Datum wurde von einem anderen Benutzer geändert.
notice_not_authorized: Sie sind nicht berechtigt, auf diese Seite zuzugreifen.
notice_email_sent: Eine E-Mail wurde an %s gesendet.
notice_email_error: Beim Senden einer E-Mail ist ein Fehler aufgetreten (%s).
-notice_feeds_access_key_reseted: Ihr RSS-Zugriffsschlüssel wurde zurückgesetzt.
+notice_feeds_access_key_reseted: Ihr Atom-Zugriffsschlüssel wurde zurückgesetzt.
notice_failed_to_save_issues: "%d von %d ausgewählten Tickets konnte(n) nicht gespeichert werden: %s."
notice_no_issue_selected: "Kein Ticket ausgewählt! Bitte wählen Sie die Tickets, die Sie bearbeiten möchten."
notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Genehmigung des Administrators."
@@ -80,6 +80,7 @@ notice_default_data_loaded: Die Standard-Konfiguration wurde erfolgreich geladen
error_can_t_load_default_data: "Die Standard-Konfiguration konnte nicht geladen werden: %s"
error_scm_not_found: Eintrag und/oder Revision besteht nicht im Projektarchiv.
error_scm_command_failed: "Beim Zugriff auf das Projektarchiv ist ein Fehler aufgetreten: %s"
+error_scm_annotate: "Der Eintrag existiert nicht oder kann nicht annotiert werden."
error_issue_not_found_in_project: 'Das Ticket wurde nicht gefunden oder gehört nicht zu diesem Projekt.'
mail_subject_lost_password: Ihr %s Kennwort
@@ -120,8 +121,8 @@ field_project: Projekt
field_issue: Ticket
field_status: Status
field_notes: Kommentare
-field_is_closed: Problem erledigt
-field_is_default: Default
+field_is_closed: Ticket geschlossen
+field_is_default: Standardeinstellung
field_tracker: Tracker
field_subject: Thema
field_due_date: Abgabedatum
@@ -133,8 +134,8 @@ field_role: Rolle
field_homepage: Projekt-Homepage
field_is_public: Öffentlich
field_parent: Unterprojekt von
-field_is_in_chlog: Ansicht im Change-Log
-field_is_in_roadmap: Ansicht in der Roadmap
+field_is_in_chlog: Im Change-Log anzeigen
+field_is_in_roadmap: In der Roadmap anzeigen
field_login: Mitgliedsname
field_mail_notification: Mailbenachrichtigung
field_admin: Administrator
@@ -177,6 +178,7 @@ field_column_names: Spalten
field_time_zone: Zeitzone
field_searchable: Durchsuchbar
field_default_value: Standardwert
+field_comments_sorting: Kommentare anzeigen
setting_app_title: Applikations-Titel
setting_app_subtitle: Applikations-Untertitel
@@ -191,7 +193,8 @@ setting_bcc_recipients: E-Mails als Blindkopie (BCC) senden
setting_host_name: Hostname
setting_text_formatting: Textformatierung
setting_wiki_compression: Wiki-Historie komprimieren
-setting_feeds_limit: Feed-Inhalt begrenzen
+setting_feeds_limit: Max. Anzahl Einträge pro Atom-Feed
+setting_default_projects_public: Neue Projekte sind standardmäßig öffentlich
setting_autofetch_changesets: Changesets automatisch abrufen
setting_sys_api_enabled: Webservice zur Verwaltung der Projektarchive benutzen
setting_commit_ref_keywords: Schlüsselwörter (Beziehungen)
@@ -206,6 +209,8 @@ setting_emails_footer: E-Mail-Fußzeile
setting_protocol: Protokoll
setting_per_page_options: Objekte pro Seite
setting_user_format: Benutzer-Anzeigeformat
+setting_activity_days_default: Anzahl Tage pro Seite der Projekt-Aktivität
+setting_display_subprojects_issues: Tickets von Unterprojekten im Hauptprojekt anzeigen
project_module_issue_tracking: Ticket-Verfolgung
project_module_time_tracking: Zeiterfassung
@@ -227,7 +232,7 @@ label_project_latest: Neueste Projekte
label_issue: Ticket
label_issue_new: Neues Ticket
label_issue_plural: Tickets
-label_issue_view_all: Alle Tickets ansehen
+label_issue_view_all: Alle Tickets anzeigen
label_issues_by: Tickets von %s
label_issue_added: Ticket hinzugefügt
label_issue_updated: Ticket aktualisiert
@@ -277,6 +282,7 @@ label_last_updates: zuletzt aktualisiert
label_last_updates_plural: %d zuletzt aktualisierten
label_registered_on: Angemeldet am
label_activity: Aktivität
+label_overall_activity: Aktivität aller Projekte anzeigen
label_new: Neu
label_logged_as: Angemeldet als
label_environment: Environment
@@ -320,7 +326,7 @@ label_version: Version
label_version_new: Neue Version
label_version_plural: Versionen
label_confirmation: Bestätigung
-label_export_to: Export zu
+label_export_to: "Auch abrufbar als:"
label_read: Lesen...
label_public_projects: Öffentliche Projekte
label_open_issues: offen
@@ -345,7 +351,7 @@ label_months_from: Monate ab
label_gantt: Gantt
label_internal: Intern
label_last_changes: %d letzte Änderungen
-label_change_view_all: Alle Änderungen ansehen
+label_change_view_all: Alle Änderungen anzeigen
label_personalize_page: Diese Seite anpassen
label_comment: Kommentar
label_comment_plural: Kommentare
@@ -469,7 +475,7 @@ label_date_to: Bis
label_language_based: Sprachabhängig
label_sort_by: Sortiert nach %s
label_send_test_email: Test-E-Mail senden
-label_feeds_access_key_created_on: RSS-Zugriffsschlüssel vor %s erstellt
+label_feeds_access_key_created_on: Atom-Zugriffsschlüssel vor %s erstellt
label_module_plural: Module
label_added_time_by: Von %s vor %s hinzugefügt
label_updated_time: Vor %s aktualisiert
@@ -500,6 +506,10 @@ label_ldap_authentication: LDAP-Authentifizierung
label_downloads_abbr: D/L
label_optional_description: Beschreibung (optional)
label_add_another_file: Eine weitere Datei hinzufügen
+label_preferences: Präferenzen
+label_chronological_order: in zeitlicher Reihenfolge
+label_reverse_chronological_order: in umgekehrter zeitlicher Reihenfolge
+label_planning: Terminplanung
button_login: Anmelden
button_submit: OK
@@ -518,7 +528,7 @@ button_lock: Sperren
button_unlock: Entsperren
button_download: Download
button_list: Liste
-button_view: Ansehen
+button_view: Anzeigen
button_move: Verschieben
button_back: Zurück
button_cancel: Abbrechen
@@ -535,7 +545,7 @@ button_reset: Zurücksetzen
button_rename: Umbenennen
button_change_password: Kennwort ändern
button_copy: Kopieren
-button_annotate: Mit Anmerkungen versehen
+button_annotate: Annotieren
button_update: Aktualisieren
button_configure: Konfigurieren
@@ -608,13 +618,4 @@ default_activity_development: Entwicklung
enumeration_issue_priorities: Ticket-Prioritäten
enumeration_doc_categories: Dokumentenkategorien
enumeration_activities: Aktivitäten (Zeiterfassung)
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/en.yml b/lang/en.yml
index 8264ba908..e39aec301 100644
--- a/lang/en.yml
+++ b/lang/en.yml
@@ -557,6 +557,7 @@ text_select_mail_notifications: Select actions for which email notifications sho
text_regexp_info: eg. ^[A-Z0-9]+$
text_min_max_length_info: 0 means no restriction
text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
text_workflow_edit: Select a role and a tracker to edit the workflow
text_are_you_sure: Are you sure ?
text_journal_changed: changed from %s to %s
diff --git a/lang/es.yml b/lang/es.yml
index 5d5492833..c6eef021a 100644
--- a/lang/es.yml
+++ b/lang/es.yml
@@ -333,8 +333,8 @@ label_revision_plural: Revisiones
label_added: añadido
label_modified: modificado
label_deleted: suprimido
-label_latest_revision: La revisión más actual
-label_latest_revision_plural: Las revisiones más actuales
+label_latest_revision: Última revisión
+label_latest_revision_plural: Últimas revisiones
label_view_revisions: Ver las revisiones
label_max_size: Tamaño máximo
label_on: de
@@ -372,7 +372,7 @@ label_view_diff: Ver diferencias
label_diff_inline: en línea
label_diff_side_by_side: cara a cara
label_options: Opciones
-label_copy_workflow_from: Copiar workflow desde
+label_copy_workflow_from: Copiar flujo de trabajo desde
label_permissions_report: Informe de permisos
label_watched_issues: Peticiones monitorizadas
label_related_issues: Peticiones relacionadas
@@ -432,7 +432,7 @@ button_move: Mover
button_back: Atrás
button_cancel: Cancelar
button_activate: Activar
-button_sort: Clasificar
+button_sort: Ordenar
button_log_time: Tiempo dedicado
button_rollback: Volver a esta versión
button_watch: Monitorizar
@@ -448,7 +448,7 @@ status_locked: bloqueado
text_select_mail_notifications: Seleccionar los eventos a notificar
text_regexp_info: eg. ^[A-Z0-9]+$
text_min_max_length_info: 0 para ninguna restricción
-text_project_destroy_confirmation: ¿ Estás seguro de querer eliminar el proyecto ?
+text_project_destroy_confirmation: ¿Estás seguro de querer eliminar el proyecto?
text_workflow_edit: Seleccionar un flujo de trabajo para actualizar
text_are_you_sure: ¿ Estás seguro ?
text_journal_changed: cambiado de %s a %s
@@ -460,7 +460,7 @@ text_tip_task_begin_end_day: tarea que comienza y termina este día
text_project_identifier_info: 'Letras minúsculas (a-z), números y signos de puntuación permitidos.<br />Una vez guardado, el identificador no puede modificarse.'
text_caracters_maximum: %d carácteres como máximo.
text_length_between: Longitud entre %d y %d carácteres.
-text_tracker_no_workflow: No hay ningún workflow definido para este tracker
+text_tracker_no_workflow: No hay ningún flujo de trabajo definido para este tracker
text_unallowed_characters: Carácteres no permitidos
text_comma_separated: Múltiples valores permitidos (separados por coma).
text_issues_ref_in_commit_messages: Referencia y petición de corrección en los mensajes
@@ -559,64 +559,65 @@ field_searchable: Incluir en las búsquedas
label_display_per_page: 'Por página: %s'
setting_per_page_options: Objetos por página
label_age: Edad
-notice_default_data_loaded: Default configuration successfully loaded.
-text_load_default_configuration: Load the default configuration
-text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
-error_can_t_load_default_data: "Default configuration could not be loaded: %s"
-button_update: Update
-label_change_properties: Change properties
+notice_default_data_loaded: Configuración por defecto cargada correctamente.
+text_load_default_configuration: Cargar la configuración por defecto
+text_no_configuration_data: "Todavía no se han configurado roles, ni trackers, ni estados y flujo de trabajo asociado a peticiones. Se recomiendo encarecidamente cargar la configuración por defecto. Una vez cargada, podrá modificarla."
+error_can_t_load_default_data: "No se ha podido cargar la configuración por defecto: %s"
+button_update: Actualizar
+label_change_properties: Cambiar propiedades
label_general: General
-label_repository_plural: Repositories
-label_associated_revisions: Associated revisions
-setting_user_format: Users display format
-text_status_changed_by_changeset: Applied in changeset %s.
-label_more: More
-text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+label_repository_plural: Repositorios
+label_associated_revisions: Revisiones asociadas
+setting_user_format: Formato de nombre de usuario
+text_status_changed_by_changeset: Aplicado en los cambios %s
+label_more: Más
+text_issues_destroy_confirmation: '¿Seguro que quiere borrar las peticiones seleccionadas?'
label_scm: SCM
-text_select_project_modules: 'Select modules to enable for this project:'
-label_issue_added: Issue added
-label_issue_updated: Issue updated
-label_document_added: Document added
-label_message_posted: Message added
-label_file_added: File added
-label_news_added: News added
-project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+text_select_project_modules: 'Seleccione los módulos a activar para este proyecto:'
+label_issue_added: Petición añadida
+label_issue_updated: Petición actualizada
+label_document_added: Documento añadido
+label_message_posted: Mensaje añadido
+label_file_added: Fichero añadido
+label_news_added: Noticia añadida
+project_module_boards: Foros
+project_module_issue_tracking: Peticiones
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
-project_module_repository: Repository
-project_module_news: News
-project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
-text_rmagick_available: RMagick available (optional)
-button_configure: Configure
+project_module_files: Ficheros
+project_module_documents: Documentos
+project_module_repository: Repositorio
+project_module_news: Noticias
+project_module_time_tracking: Control de tiempo
+text_file_repository_writable: Se puede escribir en el repositorio
+text_default_administrator_account_changed: Cuenta de administrador por defecto modificada
+text_rmagick_available: RMagick disponible (opcional)
+button_configure: Configurar
label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
+label_ldap_authentication: Autenticación LDAP
label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+label_this_month: este mes
+label_last_n_days: últimos %d días
+label_all_time: todo el tiempo
+label_this_year: este año
+label_date_range: Rango de fechas
+label_last_week: última semana
+label_yesterday: ayer
+label_last_month: último mes
+label_add_another_file: Añadir otro fichero
+label_optional_description: Descripción opcional
+text_destroy_time_entries_question: Existen %.02f horas asignadas a la petición que quiere borrar. ¿Qué quiere hacer ?
+error_issue_not_found_in_project: 'La petición no se encuentra o no está asociada a este proyecto'
+text_assign_time_entries_to_project: Asignar las horas al proyecto
+text_destroy_time_entries: Borrar las horas
+text_reassign_time_entries: 'Reasignar las horas a esta petición:'
+setting_activity_days_default: Días a mostrar en la actividad de proyecto
+label_chronological_order: En orden cronológico
+field_comments_sorting: Mostrar comentarios
+label_reverse_chronological_order: En orden cronológico inverso
+label_preferences: Preferencias
+setting_display_subprojects_issues: Mostrar peticiones de un subproyecto en el proyecto padre por defecto
+label_overall_activity: Actividad global
+setting_default_projects_public: Los proyectos nuevos son públicos por defecto
+error_scm_annotate: "No existe la entrada o no ha podido ser anotada"
+label_planning: Planificación
+text_subprojects_destroy_warning: 'Sus subprojectos: %s también se eliminarán'
diff --git a/lang/fi.yml b/lang/fi.yml
index f345be139..68b6c20d7 100644
--- a/lang/fi.yml
+++ b/lang/fi.yml
@@ -614,6 +614,7 @@ field_comments_sorting: Näytä kommentit
label_reverse_chronological_order: Käänteisessä aikajärjestyksessä
label_preferences: Asetukset
setting_default_projects_public: Uudet projektit ovat oletuksena julkisia
-label_overall_activity: Kokonaisaktiviteetti
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+label_overall_activity: Kokonaishistoria
+error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä."
+label_planning: Suunnittelu
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/fr.yml b/lang/fr.yml
index 602b3c8e1..cbdda4f3d 100644
--- a/lang/fr.yml
+++ b/lang/fr.yml
@@ -556,7 +556,8 @@ status_locked: vérouillé
text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
text_regexp_info: ex. ^[A-Z0-9]+$
text_min_max_length_info: 0 pour aucune restriction
-text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et tout ce qui lui est rattaché ?
+text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
+text_subprojects_destroy_warning: 'Ses sous-projets: %s seront également supprimés.'
text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
text_are_you_sure: Etes-vous sûr ?
text_journal_changed: changé de %s à %s
diff --git a/lang/he.yml b/lang/he.yml
index 018281581..a611c8c39 100644
--- a/lang/he.yml
+++ b/lang/he.yml
@@ -76,7 +76,7 @@ notice_failed_to_save_issues: "נכשרת בשמירת %d נוש×\×™× ×‘ %d × 
notice_no_issue_selected: "×œ× × ×‘×—×¨ ××£ נוש×! בחר בבקשה ×ת הנוש××™× ×©×‘×¨×¦×•× ×š לערוך."
error_scm_not_found: כניסה ו\×ו ×’×™×¨×¡× ××™× × ×§×™×™×ž×™× ×‘×ž×גר.
-error_scm_command_failed: "An error occurred when trying to access the repository: %s"
+error_scm_command_failed: "×רעה שגי××” בעת ניסון גישה למ×גר: %s"
mail_subject_lost_password: סיסמת ה-%s שלך
mail_body_lost_password: 'לשינו סיסמת ×”-Redmine שלך,לחץ על הקישור הב×:'
@@ -98,7 +98,7 @@ field_filesize: גודל
field_downloads: הורדות
field_author: כותב
field_created_on: נוצר
-field_updated_on: עודגן
+field_updated_on: עודכן
field_field_format: פורמט
field_is_for_all: לכל הפרויקטי×
field_possible_values: ×¢×¨×›×™× ×פשריי×
@@ -119,7 +119,7 @@ field_subject: ×©× × ×•×©×
field_due_date: ת×ריך סיו×
field_assigned_to: מוצב ל
field_priority: עדיפות
-field_fixed_version: Target version
+field_fixed_version: גירס×ת יעד
field_user: מתשמש
field_role: תפקיד
field_homepage: דף הבית
@@ -140,7 +140,7 @@ field_version: גירס×
field_type: סוג
field_host: שרת
field_port: פורט
-field_account: חשבו×
+field_account: חשבון
field_base_dn: בסיס DN
field_attr_login: תכונת התחברות
field_attr_firstname: תכונת ×©× ×¤×¨×˜×™×
@@ -182,7 +182,7 @@ setting_text_formatting: עיצוב טקסט
setting_wiki_compression: כיווץ היסטורית WIKI
setting_feeds_limit: גבול תוכן הזנות
setting_autofetch_changesets: משיכה ×וטומתי של עידכוני×
-setting_sys_api_enabled: Enable WS for repository management
+setting_sys_api_enabled: ×פשר WS לניהול המ×גר
setting_commit_ref_keywords: מילות מפתח מקשרות
setting_commit_fix_keywords: מילות מפתח מתקנות
setting_autologin: חיבור ×וטומטי
@@ -233,7 +233,7 @@ label_information_plural: מידע
label_please_login: התחבר בבקשה
label_register: הרשמה
label_password_lost: ×בדה הסיסמה?
-label_home: דך הבית
+label_home: דף הבית
label_my_page: הדף שלי
label_my_account: השבון שלי
label_my_projects: ×”×¤×¨×•×™×§×˜×™× ×©×œ×™
@@ -259,7 +259,7 @@ label_subproject_plural: תת-פרויקטי×
label_min_max_length: ×ורך מינימ×לי - מקסימ×לי
label_list: רשימה
label_date: ת×ריך
-label_integer: מספר שלי×
+label_integer: מספר של×
label_boolean: ערך בולי×× ×™
label_string: טקסט
label_text: טקסט ×רוך
@@ -269,7 +269,7 @@ label_download: הורדה %d
label_download_plural: %d הורדות
label_no_data: ×ין מידע להציג
label_change_status: שנה מצב
-label_history: הידטוריה
+label_history: היסטוריה
label_attachment: קובץ
label_attachment_new: קובץ חדש
label_attachment_delete: מחק קובץ
@@ -279,7 +279,7 @@ label_report_plural: דו"חות
label_news: חדשות
label_news_new: הוסף חדשות
label_news_plural: חדשות
-label_news_latest: חדשות חדשות
+label_news_latest: חדשות ×חרונות
label_news_view_all: צפה בכל החדשות
label_change_log: דו"×— שינויי×
label_settings: הגדרות
@@ -419,7 +419,7 @@ label_reply_plural: השבות
label_send_information: שלח מידע על חשבון למשתמש
label_year: שנה
label_month: חודש
-label_week: שבו
+label_week: שבוע
label_date_from: מ×ת
label_date_to: ×ל
label_language_based: מבוסס שפה
@@ -444,7 +444,7 @@ button_save: שמור
button_check_all: בחר הכל
button_uncheck_all: בחר כלו×
button_delete: מחק
-button_create: צוק
+button_create: צור
button_test: בדוק
button_edit: ערוך
button_add: הוסף
@@ -454,13 +454,13 @@ button_clear: נקה
button_lock: נעל
button_unlock: בטל נעילה
button_download: הורד
-button_list: קשימה
+button_list: רשימה
button_view: צפה
button_move: ×”×–×–
button_back: הקוד×
button_cancel: בטח
button_activate: הפעל
-button_sort: מין
+button_sort: מיין
button_log_time: זמן לוג
button_rollback: חזור ×œ×’×™×¨×¡× ×–×•
button_watch: צפה
@@ -526,94 +526,95 @@ default_activity_development: פיתוח
enumeration_issue_priorities: עדיפות נוש××™×
enumeration_doc_categories: קטגוריות מסמכי×
enumeration_activities: פעילויות (מעקב ×חר זמני×)
-label_search_titles_only: Search titles only
-label_nobody: nobody
-button_change_password: Change password
-text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
-label_user_mail_option_selected: "For any event on the selected projects only..."
-label_user_mail_option_all: "For any event on all my projects"
-label_user_mail_option_none: "Only for things I watch or I'm involved in"
-setting_emails_footer: Emails footer
-label_float: Float
-button_copy: Copy
-mail_body_account_information_external: You can use your "%s" account to log in.
-mail_body_account_information: Your account information
-setting_protocol: Protocol
-label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
-setting_time_format: Time format
-label_registration_activation_by_email: account activation by email
-mail_subject_account_activation_request: %s account activation request
-mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:'
-label_registration_automatic_activation: automatic account activation
-label_registration_manual_activation: manual account activation
-notice_account_pending: "Your account was created and is now pending administrator approval."
-field_time_zone: Time zone
-text_caracters_minimum: Must be at least %d characters long.
-setting_bcc_recipients: Blind carbon copy recipients (bcc)
-button_annotate: Annotate
-label_issues_by: Issues by %s
-field_searchable: Searchable
-label_display_per_page: 'Per page: %s'
-setting_per_page_options: Objects per page options
-label_age: Age
-notice_default_data_loaded: Default configuration successfully loaded.
-text_load_default_configuration: Load the default configuration
-text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
-error_can_t_load_default_data: "Default configuration could not be loaded: %s"
-button_update: Update
-label_change_properties: Change properties
-label_general: General
-label_repository_plural: Repositories
-label_associated_revisions: Associated revisions
-setting_user_format: Users display format
-text_status_changed_by_changeset: Applied in changeset %s.
-label_more: More
-text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+label_search_titles_only: חפש בכותרות בלבד
+label_nobody: ××£ ×חד
+button_change_password: שנה סיסמ×
+text_user_mail_option: "×‘×¤×¨×•×™×§×˜×™× ×©×œ× ×‘×—×¨×ª, ×תה רק תקבל התרעות על ש×תה צופה ×ו קשור ××œ×™×”× (לדוגמ×:נוש××™× ×©×תה היוצר ×©×œ×”× ×ו ×ž×•×¦×‘×™× ×ליך)."
+label_user_mail_option_selected: "לכל ×ירוע ×‘×¤×¨×•×™×§×˜×™× ×©×‘×—×¨×ª×™ בלבד..."
+label_user_mail_option_all: "לכל ×ירוע בכל ×”×¤×¨×•×™×§×˜×™× ×©×œ×™"
+label_user_mail_option_none: "רק לנוש××™× ×©×× ×™ צופה ×ו קשור ×ליה×"
+setting_emails_footer: תחתית דו×"ל
+label_float: צף
+button_copy: העתק
+mail_body_account_information_external: ×תה יכול להשתמש בחשבון "%s" כדי להתחבר
+mail_body_account_information: פרטי החשבון שלך
+setting_protocol: פרוטוקול
+label_user_mail_no_self_notified: "×× ×™ ×œ× ×¨×•×¦×” שיודיעו לי על ×©×™× ×•×™×™× ×©×× ×™ מבצע"
+setting_time_format: פורמט זמן
+label_registration_activation_by_email: הפעל חשבון ב×מצעות דו×"ל
+mail_subject_account_activation_request: בקשת הפעלה לחשבון %s
+mail_body_account_activation_request: 'משתמש חדש (%s) נרש×. החשבון שלו מחכה ל×ישור שלך:'
+label_registration_automatic_activation: הפעלת חשבון ×וטומטית
+label_registration_manual_activation: הפעלת חשבון ידנית
+notice_account_pending: "החשבון שלך נוצר ועתה מחכה ל×ישור מנהל המערכת."
+field_time_zone: ×יזור זמן
+text_caracters_minimum: חייב להיות לפחות ב×ורך של %d תווי×.
+setting_bcc_recipients: מוסתר (bcc)
+button_annotate: הוסף תי×ור מסגרת
+label_issues_by: נוש××™× ×©×œ %s
+field_searchable: ניתן לחיפוש
+label_display_per_page: 'לכל דף: %s'
+setting_per_page_options: ×פשרויות ××•×‘×™×§×˜×™× ×œ×¤×™ דף
+label_age: גיל
+notice_default_data_loaded: ×פשרויות ברירת מחדל מופעלות.
+text_load_default_configuration: טען ×ת ×פשרויות ברירת המחדל
+text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. ×™×”×™×” ב×פשרותך לשנותו ל×חר שיטען."
+error_can_t_load_default_data: "×פשרויות ברירת המחדל ×œ× ×”×¦×œ×™×—×• להיטען: %s"
+button_update: עדכן
+label_change_properties: שנה מ×פייני×
+label_general: כללי
+label_repository_plural: מ×גרי×
+label_associated_revisions: ×©×™× ×•×™×™× ×§×©×•×¨×™×
+setting_user_format: פורמט הצגת משתמשי×
+text_status_changed_by_changeset: הוחל בסדרת ×”×©×™× ×•×™×™× %s.
+label_more: עוד
+text_issues_destroy_confirmation: '×”×× ×ת\×” בטוח שברצונך למחוק ×ת הנוש×\×™× ?'
label_scm: SCM
-text_select_project_modules: 'Select modules to enable for this project:'
-label_issue_added: Issue added
-label_issue_updated: Issue updated
-label_document_added: Document added
-label_message_posted: Message added
-label_file_added: File added
-label_news_added: News added
-project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+text_select_project_modules: 'בחר ×ž×•×“×•×œ×™× ×œ×”×—×™×œ על פקרויקט ×–×”:'
+label_issue_added: × ×•×©× ×”×•×¡×£
+label_issue_updated: × ×•×©× ×¢×•×“×›×Ÿ
+label_document_added: מוסמך הוסף
+label_message_posted: הודעה הוספה
+label_file_added: קובץ הוסף
+label_news_added: חדשות הוספו
+project_module_boards: לוחות
+project_module_issue_tracking: מעקב נוש××™×
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
-project_module_repository: Repository
-project_module_news: News
-project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
+project_module_files: קבצי×
+project_module_documents: מסמכי×
+project_module_repository: מ×גר
+project_module_news: חדשות
+project_module_time_tracking: מעקב ×חר זמני×
+text_file_repository_writable: מ×גר ×”×§×‘×¦×™× × ×™×ª×Ÿ לכתיבה
+text_default_administrator_account_changed: מנהל המערכת ברירת המחדל שונה
text_rmagick_available: RMagick available (optional)
-button_configure: Configure
-label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
+button_configure: ×פשרויות
+label_plugins: פל××’×™× ×™×
+label_ldap_authentication: ×ימות LDAP
label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+label_this_month: החודש
+label_last_n_days: ב-%d ×™×ž×™× ×חרוני×
+label_all_time: תמיד
+label_this_year: השנה
+label_date_range: טווח ת×ריכי×
+label_last_week: שבוע שעבר
+label_yesterday: ×תמול
+label_last_month: חודש שעבר
+label_add_another_file: הוסף עוד קובץ
+label_optional_description: תי×ור רשות
+text_destroy_time_entries_question: %.02f שעות דווחו על ×”× ×•×©×™× ×©×ת\×” עומד\ת למחוק. מה ברצונך לעשות ?
+error_issue_not_found_in_project: 'הנוש××™× ×œ× × ×ž×¦×ו ×ו ××™× × ×©×™×›×™× ×œ×¤×¨×•×™×§×˜'
+text_assign_time_entries_to_project: הצב שעות שדווחו לפרויקט הזה
+text_destroy_time_entries: מחק שעות שדווחו
+text_reassign_time_entries: 'הצב מחדש שעות שדווחו לפרויקט הזה:'
+setting_activity_days_default: ×™×ž×™× ×”×ž×•×¦×’×™× ×¢×œ פעילות הפרויקט
+label_chronological_order: בסדר כרונולוגי
+field_comments_sorting: הצג הערות
+label_reverse_chronological_order: בסדר כרונולוגי הפוך
+label_preferences: העדפות
+setting_display_subprojects_issues: הצג נוש××™× ×©×œ תת ×¤×¨×•×™×§×˜×™× ×›×‘×¨×™×¨×ª מחדל
+label_overall_activity: פעילות כוללת
+setting_default_projects_public: ×¤×¨×•×™×§×˜×™× ×—×“×©×™× ×”×™× × ×¤×•×ž×‘×™×™× ×›×‘×¨×™×¨×ª מחדל
+error_scm_annotate: "הכניסה ×œ× ×§×™×™×ž×ª ×ו ×©×œ× × ×™×ª×Ÿ לת×ר ×ותה."
+label_planning: תכנון
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/it.yml b/lang/it.yml
index 7e9345b30..3d1dea09e 100644
--- a/lang/it.yml
+++ b/lang/it.yml
@@ -617,3 +617,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/ja.yml b/lang/ja.yml
index 44d0c3ebd..680d29836 100644
--- a/lang/ja.yml
+++ b/lang/ja.yml
@@ -618,3 +618,4 @@ label_overall_activity: å…¨ã¦ã®æ´»å‹•
setting_default_projects_public: デフォルトã§æ–°ã—ã„プロジェクトã¯å…¬é–‹ã«ã™ã‚‹
error_scm_annotate: "エントリãŒå­˜åœ¨ã—ãªã„ã€ã‚‚ã—ãã¯ã‚¢ãƒŽãƒ†ãƒ¼ãƒˆã§ãã¾ã›ã‚“。"
label_planning: 計画
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/ko.yml b/lang/ko.yml
index a8b105855..4281f3881 100644
--- a/lang/ko.yml
+++ b/lang/ko.yml
@@ -617,3 +617,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/lt.yml b/lang/lt.yml
index 5db73d300..df7cd960b 100644
--- a/lang/lt.yml
+++ b/lang/lt.yml
@@ -618,3 +618,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/nl.yml b/lang/nl.yml
index 6494eae07..e487a7a6d 100644
--- a/lang/nl.yml
+++ b/lang/nl.yml
@@ -618,3 +618,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/no.yml b/lang/no.yml
index f2d7ac144..22b8c10af 100644
--- a/lang/no.yml
+++ b/lang/no.yml
@@ -80,7 +80,7 @@ notice_default_data_loaded: Standardkonfigurasjonen lastet inn.
error_can_t_load_default_data: "Standardkonfigurasjonen kunne ikke lastes inn: %s"
error_scm_not_found: "Elementet og/eller revisjonen eksisterer ikke i depoet."
error_scm_command_failed: "En feil oppstod under tilkobling til depoet: %s"
-error_scm_annotate: "Elementet eksisterer ikke, eller kan ikke annoteres."
+error_scm_annotate: "Elementet eksisterer ikke, eller kan ikke noteres."
error_issue_not_found_in_project: 'Saken eksisterer ikke, eller hører ikke til dette prosjektet'
mail_subject_lost_password: Ditt %s passord
@@ -137,7 +137,7 @@ field_parent: Underprosjekt til
field_is_in_chlog: Vises i endringslogg
field_is_in_roadmap: Vises i veikart
field_login: Brukernavn
-field_mail_notification: E-post varsling
+field_mail_notification: E-post-varsling
field_admin: Administrator
field_last_login_on: Sist innlogget
field_language: Språk
@@ -165,7 +165,7 @@ field_url: URL
field_start_page: Startside
field_subproject: Underprosjekt
field_hours: Timer
-field_activity: Activitet
+field_activity: Aktivitet
field_spent_on: Dato
field_identifier: Identifikasjon
field_is_filter: Brukes som filter
@@ -282,7 +282,7 @@ label_last_updates: Sist oppdatert
label_last_updates_plural: %d siste oppdaterte
label_registered_on: Registrert
label_activity: Aktivitet
-label_overall_activity: Total aktivitet
+label_overall_activity: All aktivitet
label_new: Ny
label_logged_as: Innlogget som
label_environment: Miljø
@@ -352,7 +352,7 @@ label_gantt: Gantt
label_internal: Intern
label_last_changes: siste %d endringer
label_change_view_all: Vis alle endringer
-label_personalize_page: Tilrettelegg denne siden
+label_personalize_page: Tilpass denne siden
label_comment: Kommentar
label_comment_plural: Kommentarer
label_comment_add: Legg til kommentar
@@ -545,7 +545,7 @@ button_reset: Nullstill
button_rename: Endre navn
button_change_password: Endre passord
button_copy: Kopier
-button_annotate: Annotér
+button_annotate: Notér
button_update: Oppdater
button_configure: Konfigurer
@@ -557,6 +557,7 @@ text_select_mail_notifications: Velg hendelser som skal varsles med e-post.
text_regexp_info: eg. ^[A-Z0-9]+$
text_min_max_length_info: 0 betyr ingen begrensning
text_project_destroy_confirmation: Er du sikker på at du vil slette dette prosjekter og alle relatert data ?
+text_subprojects_destroy_warning: 'Underprojekt(ene): %s vil også bli slettet.'
text_workflow_edit: Velg en rolle og en sakstype for å endre arbeidsflyten
text_are_you_sure: Er du sikker ?
text_journal_changed: endret fra %s til %s
@@ -565,7 +566,7 @@ text_journal_deleted: slettet
text_tip_task_begin_day: oppgaven starter denne dagen
text_tip_task_end_day: oppgaven avsluttes denne dagen
text_tip_task_begin_end_day: oppgaven starter og avsluttes denne dagen
-text_project_identifier_info: 'Små bokstaver (a-z), nummer og binde-/understrek tillat.<br />Identifikatoren kan ikke endres etter den er lagret.'
+text_project_identifier_info: 'Små bokstaver (a-z), nummer og bindestrek tillatt.<br />Identifikatoren kan ikke endres etter den er lagret.'
text_caracters_maximum: %d tegn maksimum.
text_caracters_minimum: Må være minst %d tegn langt.
text_length_between: Lengde mellom %d og %d tegn.
@@ -586,8 +587,8 @@ text_status_changed_by_changeset: Brukt i endringssett %s.
text_issues_destroy_confirmation: 'Er du sikker på at du vil slette valgte sak(er) ?'
text_select_project_modules: 'Velg moduler du vil aktivere for dette prosjektet:'
text_default_administrator_account_changed: Standard administrator-konto er endret
-text_file_repository_writable: Fil-depotet er skrivbart
-text_rmagick_available: RMagick tilgjengelig (valgfritt)
+text_file_repository_writable: Fil-arkivet er skrivbart
+text_rmagick_available: RMagick er tilgjengelig (valgfritt)
text_destroy_time_entries_question: %.02f timer er ført på sakene du er i ferd med å slette. Hva vil du gjøre ?
text_destroy_time_entries: Slett førte timer
text_assign_time_entries_to_project: Overfør førte timer til prosjektet
diff --git a/lang/pl.yml b/lang/pl.yml
index fdb8afaa2..81f03a62f 100644
--- a/lang/pl.yml
+++ b/lang/pl.yml
@@ -617,3 +617,4 @@ label_overall_activity: Ogólna aktywność
setting_default_projects_public: Nowe projekty są domyślnie publiczne
error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/pt-br.yml b/lang/pt-br.yml
index 218cbab51..9facd8d19 100644
--- a/lang/pt-br.yml
+++ b/lang/pt-br.yml
@@ -617,3 +617,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/pt.yml b/lang/pt.yml
index e82176c56..6f51c8ed2 100644
--- a/lang/pt.yml
+++ b/lang/pt.yml
@@ -617,3 +617,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/ro.yml b/lang/ro.yml
index 4d26ab103..59edfeb70 100644
--- a/lang/ro.yml
+++ b/lang/ro.yml
@@ -617,3 +617,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/ru.yml b/lang/ru.yml
index e488c35e1..f69009847 100644
--- a/lang/ru.yml
+++ b/lang/ru.yml
@@ -619,5 +619,6 @@ label_preferences: ПредпочтениÑ
setting_display_subprojects_issues: Отображение подпроектов по умолчанию
label_overall_activity: Ð¡Ð²Ð¾Ð´Ð½Ð°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð½Ð¾ÑÑ‚ÑŒ
setting_default_projects_public: Ðовые проекты ÑвлÑÑŽÑ‚ÑÑ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼Ð¸
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
+error_scm_annotate: "Данные отÑутÑтвуют или не могут быть подпиÑаны."
+label_planning: Планирование
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/sr.yml b/lang/sr.yml
index fa4ecd8de..d9869c362 100644
--- a/lang/sr.yml
+++ b/lang/sr.yml
@@ -618,3 +618,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/sv.yml b/lang/sv.yml
index e1ac6b4bc..c0f691230 100644
--- a/lang/sv.yml
+++ b/lang/sv.yml
@@ -618,3 +618,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/uk.yml b/lang/uk.yml
index 8fc418e67..a52a05603 100644
--- a/lang/uk.yml
+++ b/lang/uk.yml
@@ -619,3 +619,4 @@ label_overall_activity: Overall activity
setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml
index e16fbe591..a0c7fafb3 100644
--- a/lang/zh-tw.yml
+++ b/lang/zh-tw.yml
@@ -618,3 +618,4 @@ default_activity_development: 開發
enumeration_issue_priorities: 項目優先權
enumeration_doc_categories: 文件分類
enumeration_activities: 活動 (時間追蹤)
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lang/zh.yml b/lang/zh.yml
index bff45a2ec..12fb8cb3e 100644
--- a/lang/zh.yml
+++ b/lang/zh.yml
@@ -618,3 +618,4 @@ default_activity_development: å¼€å‘
enumeration_issue_priorities: 问题优先级
enumeration_doc_categories: 文档类别
enumeration_activities: 活动(时间跟踪)
+text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
diff --git a/lib/redcloth.rb b/lib/redcloth.rb
index 5ed23b8f7..7e0c71839 100644
--- a/lib/redcloth.rb
+++ b/lib/redcloth.rb
@@ -1134,7 +1134,7 @@ class RedCloth < String
ALLOWED_TAGS = %w(redpre pre code)
def escape_html_tags(text)
- text.gsub!(%r{<((\/?)(\w+))}) {|m| ALLOWED_TAGS.include?($3) ? "<#{$1}" : "&lt;#{$1}" }
+ text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' if $3}" }
end
end
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 5443eef4a..2697e8f5f 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -1,6 +1,7 @@
require 'redmine/access_control'
require 'redmine/menu_manager'
require 'redmine/mime_type'
+require 'redmine/core_ext'
require 'redmine/themes'
require 'redmine/plugin'
diff --git a/lib/redmine/core_ext.rb b/lib/redmine/core_ext.rb
new file mode 100644
index 000000000..573313e74
--- /dev/null
+++ b/lib/redmine/core_ext.rb
@@ -0,0 +1 @@
+Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
diff --git a/lib/redmine/core_ext/string.rb b/lib/redmine/core_ext/string.rb
new file mode 100644
index 000000000..ce2646fb9
--- /dev/null
+++ b/lib/redmine/core_ext/string.rb
@@ -0,0 +1,5 @@
+require File.dirname(__FILE__) + '/string/conversions'
+
+class String #:nodoc:
+ include Redmine::CoreExtensions::String::Conversions
+end
diff --git a/lib/redmine/core_ext/string/conversions.rb b/lib/redmine/core_ext/string/conversions.rb
new file mode 100644
index 000000000..7444445b0
--- /dev/null
+++ b/lib/redmine/core_ext/string/conversions.rb
@@ -0,0 +1,40 @@
+# redMine - project management software
+# Copyright (C) 2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine #:nodoc:
+ module CoreExtensions #:nodoc:
+ module String #:nodoc:
+ # Custom string conversions
+ module Conversions
+ # Parses hours format and returns a float
+ def to_hours
+ s = self.dup
+ s.strip!
+ unless s =~ %r{^[\d\.,]+$}
+ # 2:30 => 2.5
+ s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
+ # 2h30, 2h, 30m => 2.5, 2, 0.5
+ s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
+ end
+ # 2,5 => 2.5
+ s.gsub!(',', '.')
+ s.to_f
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb
index 41edf00ad..2c254d48d 100644
--- a/lib/redmine/scm/adapters/abstract_adapter.rb
+++ b/lib/redmine/scm/adapters/abstract_adapter.rb
@@ -59,8 +59,17 @@ module Redmine
# Returns the entry identified by path and revision identifier
# or nil if entry doesn't exist in the repository
def entry(path=nil, identifier=nil)
- e = entries(path, identifier)
- e ? e.first : nil
+ parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
+ search_path = parts[0..-2].join('/')
+ search_name = parts[-1]
+ if search_path.blank? && search_name.blank?
+ # Root entry
+ Entry.new(:path => '', :kind => 'dir')
+ else
+ # Search for the entry in the parent directory
+ es = entries(search_path, identifier)
+ es ? es.detect {|e| e.name == search_name} : nil
+ end
end
# Returns an Entries collection
diff --git a/lib/redmine/scm/adapters/bazaar_adapter.rb b/lib/redmine/scm/adapters/bazaar_adapter.rb
index 11a44b7cf..2225a627c 100644
--- a/lib/redmine/scm/adapters/bazaar_adapter.rb
+++ b/lib/redmine/scm/adapters/bazaar_adapter.rb
@@ -44,18 +44,6 @@ module Redmine
return nil
end
- # Returns the entry identified by path and revision identifier
- # or nil if entry doesn't exist in the repository
- def entry(path=nil, identifier=nil)
- path ||= ''
- parts = path.split(%r{[\/\\]}).select {|p| !p.blank?}
- if parts.size > 0
- parent = parts[0..-2].join('/')
- entries = entries(parent, identifier)
- entries ? entries.detect {|e| e.name == parts.last} : nil
- end
- end
-
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(path=nil, identifier=nil)
diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb
index c0f60c02a..37920b599 100644
--- a/lib/redmine/scm/adapters/cvs_adapter.rb
+++ b/lib/redmine/scm/adapters/cvs_adapter.rb
@@ -55,15 +55,6 @@ module Redmine
def get_previous_revision(revision)
CvsRevisionHelper.new(revision).prevRev
end
-
- # Returns the entry identified by path and revision identifier
- # or nil if entry doesn't exist in the repository
- # this method returns all revisions from one single SCM-Entry
- def entry(path=nil, identifier="HEAD")
- e = entries(path, identifier)
- logger.debug("<cvs-result> #{e.first.inspect}") if e
- e ? e.first : nil
- end
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
@@ -72,7 +63,9 @@ module Redmine
logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
path_with_project="#{url}#{with_leading_slash(path)}"
entries = Entries.new
- cmd = "#{CVS_BIN} -d #{root_url} rls -ed #{path_with_project}"
+ cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
+ cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
+ cmd << " #{path_with_project}"
shellout(cmd) do |io|
io.each_line(){|line|
fields=line.chop.split('/',-1)
diff --git a/lib/redmine/scm/adapters/darcs_adapter.rb b/lib/redmine/scm/adapters/darcs_adapter.rb
index cd8610121..a1d1867b1 100644
--- a/lib/redmine/scm/adapters/darcs_adapter.rb
+++ b/lib/redmine/scm/adapters/darcs_adapter.rb
@@ -40,20 +40,15 @@ module Redmine
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
end
- # Returns the entry identified by path and revision identifier
- # or nil if entry doesn't exist in the repository
- def entry(path=nil, identifier=nil)
- e = entries(path, identifier)
- e ? e.first : nil
- end
-
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(path=nil, identifier=nil)
path_prefix = (path.blank? ? '' : "#{path}/")
path = '.' if path.blank?
entries = Entries.new
- cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output #{path}"
+ cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output"
+ cmd << " --match \"hash #{identifier}\"" if identifier
+ cmd << " #{path}"
shellout(cmd) do |io|
begin
doc = REXML::Document.new(io)
diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb
index f1d076360..77604f283 100644
--- a/lib/redmine/scm/adapters/git_adapter.rb
+++ b/lib/redmine/scm/adapters/git_adapter.rb
@@ -79,7 +79,7 @@ module Redmine
rev = Revision.new({:identifier => changeset[:commit],
:scmid => changeset[:commit],
:author => changeset[:author],
- :time => Time.parse(changeset[:date]),
+ :time => (changeset[:date] ? Time.parse(changeset[:date]) : nil),
:message => changeset[:description],
:paths => files
})
@@ -132,14 +132,6 @@ module Redmine
entries.sort_by_name
end
- def entry(path=nil, identifier=nil)
- path ||= ''
- search_path = path.split('/')[0..-2].join('/')
- entry_name = path.split('/').last
- e = entries(search_path, identifier)
- e ? e.detect{|entry| entry.name == entry_name} : nil
- end
-
def revisions(path, identifier_from, identifier_to, options={})
revisions = Revisions.new
cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
index ff52ab893..6f42dda06 100644
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -59,14 +59,6 @@ module Redmine
return nil if $? && $?.exitstatus != 0
entries.sort_by_name
end
-
- def entry(path=nil, identifier=nil)
- path ||= ''
- search_path = path.split('/')[0..-2].join('/')
- entry_name = path.split('/').last
- e = entries(search_path, identifier)
- e ? e.detect{|entry| entry.name == entry_name} : nil
- end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
revisions = Revisions.new
@@ -88,13 +80,7 @@ module Redmine
value = $2
if parsing_descr && line_feeds > 1
parsing_descr = false
- revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
- :scmid => changeset[:changeset].split(':').last,
- :author => changeset[:user],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => changeset[:files].to_s.split.collect{|path| {:action => 'X', :path => "/#{path}"}}
- })
+ revisions << build_revision_from_changeset(changeset)
changeset = {}
end
if !parsing_descr
@@ -111,13 +97,8 @@ module Redmine
line_feeds += 1 if line.chomp.empty?
end
end
- revisions << Revision.new({:identifier => changeset[:changeset].split(':').first.to_i,
- :scmid => changeset[:changeset].split(':').last,
- :author => changeset[:user],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => changeset[:files].to_s.split.collect{|path| {:action => 'X', :path => "/#{path}"}}
- })
+ # Add the last changeset if there is one left
+ revisions << build_revision_from_changeset(changeset) if changeset[:date]
end
return nil if $? && $?.exitstatus != 0
revisions
@@ -171,6 +152,47 @@ module Redmine
return nil if $? && $?.exitstatus != 0
blame
end
+
+ private
+
+ # Builds a revision objet from the changeset returned by hg command
+ def build_revision_from_changeset(changeset)
+ rev_id = changeset[:changeset].to_s.split(':').first.to_i
+
+ # Changes
+ paths = (rev_id == 0) ?
+ # Can't get changes for revision 0 with hg status
+ changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} :
+ status(rev_id)
+
+ Revision.new({:identifier => rev_id,
+ :scmid => changeset[:changeset].to_s.split(':').last,
+ :author => changeset[:user],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => paths
+ })
+ end
+
+ # Returns the file changes for a given revision
+ def status(rev_id)
+ cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}"
+ result = []
+ shellout(cmd) do |io|
+ io.each_line do |line|
+ action, file = line.chomp.split
+ next unless action && file
+ file.gsub!("\\", "/")
+ case action
+ when 'R'
+ result << { :action => 'D' , :path => "/#{file}" }
+ else
+ result << { :action => action, :path => "/#{file}" }
+ end
+ end
+ end
+ result
+ end
end
end
end
diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb
index 1e0320e2c..40c7eb3f1 100644
--- a/lib/redmine/scm/adapters/subversion_adapter.rb
+++ b/lib/redmine/scm/adapters/subversion_adapter.rb
@@ -51,18 +51,11 @@ module Redmine
return nil
end
- # Returns the entry identified by path and revision identifier
- # or nil if entry doesn't exist in the repository
- def entry(path=nil, identifier=nil)
- e = entries(path, identifier)
- e ? e.first : nil
- end
-
# Returns an Entries collection
# or nil if the given path doesn't exist in the repository
def entries(path=nil, identifier=nil)
path ||= ''
- identifier = 'HEAD' unless identifier and identifier > 0
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
entries = Entries.new
cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
cmd << credentials_string
@@ -94,8 +87,8 @@ module Redmine
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path ||= ''
- identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
- identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
+ identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
+ identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
revisions = Revisions.new
cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
cmd << credentials_string
@@ -131,11 +124,9 @@ module Redmine
def diff(path, identifier_from, identifier_to=nil, type="inline")
path ||= ''
- if identifier_to and identifier_to.to_i > 0
- identifier_to = identifier_to.to_i
- else
- identifier_to = identifier_from.to_i - 1
- end
+ identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
+ identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
+
cmd = "#{SVN_BIN} diff -r "
cmd << "#{identifier_to}:"
cmd << "#{identifier_from}"
diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb
index c0f2b222a..0848aee4e 100644
--- a/lib/redmine/wiki_formatting/macros.rb
+++ b/lib/redmine/wiki_formatting/macros.rb
@@ -77,21 +77,24 @@ module Redmine
content_tag('dl', out)
end
- desc "Include a wiki page. Example:\n\n !{{include(Foo)}}"
+ desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
macro :include do |obj, args|
- if @project && !@project.wiki.nil?
- page = @project.wiki.find_page(args.first)
- if page && page.content
- @included_wiki_pages ||= []
- raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
- @included_wiki_pages << page.title
- out = textilizable(page.content, :text)
- @included_wiki_pages.pop
- out
- else
- raise "Page #{args.first} doesn't exist"
- end
+ project = @project
+ title = args.first.to_s
+ if title =~ %r{^([^\:]+)\:(.*)$}
+ project_identifier, title = $1, $2
+ project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
end
+ raise 'Unknow project' unless project && User.current.allowed_to?(:view_wiki_pages, project)
+ raise 'No wiki for this project' unless !project.wiki.nil?
+ page = project.wiki.find_page(title)
+ raise "Page #{args.first} doesn't exist" unless page && page.content
+ @included_wiki_pages ||= []
+ raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
+ @included_wiki_pages << page.title
+ out = textilizable(page.content, :text, :attachments => page.attachments)
+ @included_wiki_pages.pop
+ out
end
end
end
diff --git a/public/images/attachment.png b/public/images/attachment.png
index eea26921b..b7ce3c445 100644
--- a/public/images/attachment.png
+++ b/public/images/attachment.png
Binary files differ
diff --git a/public/images/changeset.png b/public/images/changeset.png
new file mode 100644
index 000000000..67de2c6cc
--- /dev/null
+++ b/public/images/changeset.png
Binary files differ
diff --git a/public/images/comments.png b/public/images/comments.png
new file mode 100644
index 000000000..39433cf78
--- /dev/null
+++ b/public/images/comments.png
Binary files differ
diff --git a/public/images/document.png b/public/images/document.png
new file mode 100644
index 000000000..d00b9b2f4
--- /dev/null
+++ b/public/images/document.png
Binary files differ
diff --git a/public/images/message.png b/public/images/message.png
new file mode 100644
index 000000000..252ea14d5
--- /dev/null
+++ b/public/images/message.png
Binary files differ
diff --git a/public/images/news.png b/public/images/news.png
new file mode 100644
index 000000000..6a2ecce1b
--- /dev/null
+++ b/public/images/news.png
Binary files differ
diff --git a/public/images/ticket.png b/public/images/ticket.png
new file mode 100644
index 000000000..244e6ca04
--- /dev/null
+++ b/public/images/ticket.png
Binary files differ
diff --git a/public/images/ticket_checked.png b/public/images/ticket_checked.png
new file mode 100644
index 000000000..4b1dfbc3e
--- /dev/null
+++ b/public/images/ticket_checked.png
Binary files differ
diff --git a/public/images/ticket_edit.png b/public/images/ticket_edit.png
new file mode 100644
index 000000000..291bfc764
--- /dev/null
+++ b/public/images/ticket_edit.png
Binary files differ
diff --git a/public/images/wiki_edit.png b/public/images/wiki_edit.png
new file mode 100644
index 000000000..bdc333a65
--- /dev/null
+++ b/public/images/wiki_edit.png
Binary files differ
diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js
index e3f128d89..3e2d571fa 100644
--- a/public/javascripts/context_menu.js
+++ b/public/javascripts/context_menu.js
@@ -93,14 +93,55 @@ ContextMenu.prototype = {
},
showMenu: function(e) {
- $('context-menu').style['left'] = (Event.pointerX(e) + 'px');
- $('context-menu').style['top'] = (Event.pointerY(e) + 'px');
- Element.update('context-menu', '');
- new Ajax.Updater({success:'context-menu'}, this.url,
+ var mouse_x = Event.pointerX(e);
+ var mouse_y = Event.pointerY(e);
+ var render_x = mouse_x;
+ var render_y = mouse_y;
+ var dims;
+ var menu_width;
+ var menu_height;
+ var window_width;
+ var window_height;
+ var max_width;
+ var max_height;
+
+ $('context-menu').style['left'] = (render_x + 'px');
+ $('context-menu').style['top'] = (render_y + 'px');
+ Element.update('context-menu', '');
+
+ new Ajax.Updater({success:'context-menu'}, this.url,
{asynchronous:true,
evalScripts:true,
parameters:Form.serialize(Event.findElement(e, 'form')),
onComplete:function(request){
+ dims = $('context-menu').getDimensions();
+ menu_width = dims.width;
+ menu_height = dims.height;
+ max_width = mouse_x + 2*menu_width;
+ max_height = mouse_y + menu_height;
+
+ var ws = window_size();
+ window_width = ws.width;
+ window_height = ws.height;
+
+ /* display the menu above and/or to the left of the click if needed */
+ if (max_width > window_width) {
+ render_x -= menu_width;
+ $('context-menu').addClassName('reverse-x');
+ } else {
+ $('context-menu').removeClassName('reverse-x');
+ }
+ if (max_height > window_height) {
+ render_y -= menu_height;
+ $('context-menu').addClassName('reverse-y');
+ } else {
+ $('context-menu').removeClassName('reverse-y');
+ }
+ if (render_x <= 0) render_x = 1;
+ if (render_y <= 0) render_y = 1;
+ $('context-menu').style['left'] = (render_x + 'px');
+ $('context-menu').style['top'] = (render_y + 'px');
+
Effect.Appear('context-menu', {duration: 0.20});
if (window.parseStylesheets) { window.parseStylesheets(); } // IE
}})
@@ -159,3 +200,19 @@ function toggleIssuesSelection(el) {
}
}
}
+
+function window_size() {
+ var w;
+ var h;
+ if (window.innerWidth) {
+ w = window.innerWidth;
+ h = window.innerHeight;
+ } else if (document.documentElement) {
+ w = document.documentElement.clientWidth;
+ h = document.documentElement.clientHeight;
+ } else {
+ w = document.body.clientWidth;
+ h = document.body.clientHeight;
+ }
+ return {width: w, height: h};
+}
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index a1981c64a..26f66f0b8 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -57,10 +57,7 @@ h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; bord
#content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
* html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
-html>body #content {
-height: auto;
-min-height: 600px;
-}
+html>body #content { height: auto; min-height: 600px; overflow: auto; }
#main.nosidebar #sidebar{ display: none; }
#main.nosidebar #content{ width: auto; border-right: 0; }
@@ -162,7 +159,13 @@ div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid
p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
-fieldset#filters .buttons { text-align: right; font-size: 0.9em; margin: 0 4px 0px 0; }
+fieldset#filters { padding: 0.7em; }
+fieldset#filters p { margin: 1.2em 0 0.8em 2px; }
+fieldset#filters .buttons { font-size: 0.9em; }
+fieldset#filters table { border-collapse: collapse; }
+fieldset#filters table td { padding: 0; vertical-align: middle; }
+fieldset#filters tr.filter { height: 2em; }
+fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
div#issue-changesets .changeset { padding: 4px;}
@@ -170,11 +173,21 @@ div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
div#activity dl { margin-left: 2em; }
-div#activity dd { margin-bottom: 1em; }
-div#activity dt { margin-bottom: 1px; }
+div#activity dd { margin-bottom: 1em; padding-left: 18px; }
+div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
div#activity dt .time { color: #777; font-size: 80%; }
div#activity dd .description { font-style: italic; }
div#activity span.project:after { content: " -"; }
+div#activity dt.issue { background-image: url(../images/ticket.png); }
+div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
+div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
+div#activity dt.changeset { background-image: url(../images/changeset.png); }
+div#activity dt.news { background-image: url(../images/news.png); }
+div#activity dt.message { background-image: url(../images/message.png); }
+div#activity dt.reply { background-image: url(../images/comments.png); }
+div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); }
+div#activity dt.attachment { background-image: url(../images/attachment.png); }
+div#activity dt.document { background-image: url(../images/document.png); }
div#roadmap fieldset.related-issues { margin-bottom: 1em; }
div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
@@ -186,7 +199,7 @@ div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom
div#version-summary fieldset { margin-bottom: 1em; }
div#version-summary .total-hours { text-align: right; }
-table#time-report td.hours { text-align: right; padding-right: 0.5em; }
+table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
table#time-report tbody tr { font-style: italic; color: #777; }
table#time-report tbody tr.last-level { font-style: normal; color: #555; }
table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
@@ -195,7 +208,7 @@ table#time-report .hours-dec { font-size: 0.9em; }
.total-hours { font-size: 110%; font-weight: bold; }
.total-hours span.hours-int { font-size: 120%; }
-.autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;}
+.autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
#user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
.pagination {font-size: 90%}
@@ -246,7 +259,7 @@ p.other-formats { text-align: right; font-size:0.9em; color: #666; }
a.feed { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
/***** Flash & error messages ****/
-#errorExplanation, div.flash, .nodata {
+#errorExplanation, div.flash, .nodata, .warning {
padding: 4px 4px 4px 30px;
margin-bottom: 12px;
font-size: 1.1em;
@@ -269,7 +282,7 @@ div.flash.notice {
color: #005f00;
}
-.nodata {
+.nodata, .warning {
text-align: center;
background-color: #FFEBC1;
border-color: #FDBF3B;
@@ -575,7 +588,7 @@ vertical-align: middle;
/***** Media print specific styles *****/
@media print {
- #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
+ #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
#main { background: #fff; }
#content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
}
diff --git a/public/stylesheets/context_menu.css b/public/stylesheets/context_menu.css
index f68b33fe1..e5a83be0d 100644
--- a/public/stylesheets/context_menu.css
+++ b/public/stylesheets/context_menu.css
@@ -22,13 +22,13 @@
padding:1px;
z-index:9;
}
-#context-menu li.folder ul {
- position:absolute;
- left:168px; /* IE6 */
- top:-2px;
-}
+#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; }
#context-menu li.folder>ul { left:148px; }
+#context-menu.reverse-y li.folder>ul { top:auto; bottom:0; }
+#context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ }
+#context-menu.reverse-x li.folder>ul { right:148px; }
+
#context-menu a {
border:1px solid white;
text-decoration:none;
diff --git a/test/fixtures/attachments.yml b/test/fixtures/attachments.yml
index 764948755..162d44720 100644
--- a/test/fixtures/attachments.yml
+++ b/test/fixtures/attachments.yml
@@ -23,4 +23,17 @@ attachments_002:
filesize: 28
filename: document.txt
author_id: 2
+attachments_003:
+ created_on: 2006-07-19 21:07:27 +02:00
+ downloads: 0
+ content_type: image/gif
+ disk_filename: 060719210727_logo.gif
+ container_id: 4
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 3
+ container_type: WikiPage
+ filesize: 280
+ filename: logo.gif
+ description: This is a logo
+ author_id: 2
\ No newline at end of file
diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml
index e58d8e3dc..6be840fcc 100644
--- a/test/fixtures/custom_fields.yml
+++ b/test/fixtures/custom_fields.yml
@@ -3,7 +3,7 @@ custom_fields_001:
name: Database
min_length: 0
regexp: ""
- is_for_all: false
+ is_for_all: true
type: IssueCustomField
max_length: 0
possible_values: MySQL|PostgreSQL|Oracle
diff --git a/test/fixtures/queries.yml b/test/fixtures/queries.yml
index a4c045b15..f12022729 100644
--- a/test/fixtures/queries.yml
+++ b/test/fixtures/queries.yml
@@ -1,7 +1,9 @@
---
queries_001:
- name: Multiple custom fields query
+ id: 1
project_id: 1
+ is_public: true
+ name: Multiple custom fields query
filters: |
---
cf_1:
@@ -17,6 +19,51 @@ queries_001:
- "125"
:operator: "="
- id: 1
- is_public: true
user_id: 1
+ column_names:
+queries_002:
+ id: 2
+ project_id: 1
+ is_public: false
+ name: Private query for cookbook
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+ status_id:
+ :values:
+ - "1"
+ :operator: o
+
+ user_id: 3
+ column_names:
+queries_003:
+ id: 3
+ project_id:
+ is_public: false
+ name: Private query for all projects
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+
+ user_id: 3
+ column_names:
+queries_004:
+ id: 4
+ project_id:
+ is_public: true
+ name: Public query for all projects
+ filters: |
+ ---
+ tracker_id:
+ :values:
+ - "3"
+ :operator: "="
+
+ user_id: 2
+ column_names:
diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml
index 0aabbb619..1ede6fca9 100644
--- a/test/fixtures/roles.yml
+++ b/test/fixtures/roles.yml
@@ -57,7 +57,6 @@ roles_002:
- :add_issue_notes
- :move_issues
- :delete_issues
- - :manage_public_queries
- :save_queries
- :view_gantt
- :view_calendar
@@ -94,7 +93,6 @@ roles_003:
- :manage_issue_relations
- :add_issue_notes
- :move_issues
- - :manage_public_queries
- :save_queries
- :view_gantt
- :view_calendar
diff --git a/test/fixtures/time_entries.yml b/test/fixtures/time_entries.yml
index 151077d2b..4a8a4a2a4 100644
--- a/test/fixtures/time_entries.yml
+++ b/test/fixtures/time_entries.yml
@@ -15,7 +15,7 @@ time_entries_001:
tyear: 2007
time_entries_002:
created_on: 2007-03-23 14:11:04 +01:00
- tweek: 12
+ tweek: 11
tmonth: 3
project_id: 1
comments: ""
@@ -36,7 +36,7 @@ time_entries_003:
updated_on: 2007-04-21 12:20:48 +02:00
activity_id: 9
spent_on: 2007-04-21
- issue_id: 2
+ issue_id: 3
id: 3
hours: 1.0
user_id: 1
diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml
index 6937dbd14..5d6d3f1de 100644
--- a/test/fixtures/wiki_contents.yml
+++ b/test/fixtures/wiki_contents.yml
@@ -15,6 +15,8 @@ wiki_contents_002:
h1. Another page
This is a link to a ticket: #2
+ And this is an included page:
+ {{include(Page with an inline image)}}
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 2
id: 2
@@ -32,3 +34,17 @@ wiki_contents_003:
version: 1
author_id: 1
comments:
+wiki_contents_004:
+ text: |-
+ h1. Page with an inline image
+
+ This is an inline image:
+
+ !logo.gif!
+ updated_on: 2007-03-08 00:18:07 +01:00
+ page_id: 4
+ id: 4
+ version: 1
+ author_id: 1
+ comments:
+ \ No newline at end of file
diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml
index ee260291d..f89832e44 100644
--- a/test/fixtures/wiki_pages.yml
+++ b/test/fixtures/wiki_pages.yml
@@ -14,4 +14,9 @@ wiki_pages_003:
title: Start_page
id: 3
wiki_id: 2
+wiki_pages_004:
+ created_on: 2007-03-08 00:18:07 +01:00
+ title: Page_with_an_inline_image
+ id: 4
+ wiki_id: 1
\ No newline at end of file
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index 7d49f088d..042a8f3f2 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -181,6 +181,16 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_equal 'Value for field 2', v.value
end
+ def test_post_new_without_custom_fields_param
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_redirected_to 'issues/show'
+ end
+
def test_copy_issue
@request.session[:user_id] = 2
get :new, :project_id => 1, :copy_from => 1
@@ -440,10 +450,11 @@ class IssuesControllerTest < Test::Unit::TestCase
end
def test_destroy_issue_with_no_time_entries
+ assert_nil TimeEntry.find_by_issue_id(2)
@request.session[:user_id] = 2
- post :destroy, :id => 3
+ post :destroy, :id => 2
assert_redirected_to 'projects/ecookbook/issues'
- assert_nil Issue.find_by_id(3)
+ assert_nil Issue.find_by_id(2)
end
def test_destroy_issues_with_time_entries
diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb
index dcfe0caa7..1fe8d086a 100644
--- a/test/functional/messages_controller_test.rb
+++ b/test/functional/messages_controller_test.rb
@@ -54,6 +54,9 @@ class MessagesControllerTest < Test::Unit::TestCase
def test_post_new
@request.session[:user_id] = 2
+ ActionMailer::Base.deliveries.clear
+ Setting.notified_events << 'message_posted'
+
post :new, :board_id => 1,
:message => { :subject => 'Test created message',
:content => 'Message body'}
@@ -63,6 +66,15 @@ class MessagesControllerTest < Test::Unit::TestCase
assert_equal 'Message body', message.content
assert_equal 2, message.author_id
assert_equal 1, message.board_id
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert_equal "[#{message.board.project.name} - #{message.board.name}] Test created message", mail.subject
+ assert mail.body.include?('Message body')
+ # author
+ assert mail.bcc.include?('jsmith@somenet.foo')
+ # project member
+ assert mail.bcc.include?('dlopper@somenet.foo')
end
def test_get_edit
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index 75b4673a1..eb5795152 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -144,7 +144,7 @@ class ProjectsControllerTest < Test::Unit::TestCase
:content => /#{2.days.ago.to_date.day}/,
:sibling => { :tag => "dl",
:child => { :tag => "dt",
- :attributes => { :class => 'journal' },
+ :attributes => { :class => 'issue-edit' },
:child => { :tag => "a",
:content => /(#{IssueStatus.find(2).name})/,
}
diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb
new file mode 100644
index 000000000..de08b4245
--- /dev/null
+++ b/test/functional/queries_controller_test.rb
@@ -0,0 +1,211 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'queries_controller'
+
+# Re-raise errors caught by the controller.
+class QueriesController; def rescue_action(e) raise e end; end
+
+class QueriesControllerTest < Test::Unit::TestCase
+ fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
+
+ def setup
+ @controller = QueriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_get_new_project_query
+ @request.session[:user_id] = 2
+ get :new, :project_id => 1
+ assert_response :success
+ assert_template 'new'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => nil }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => nil }
+ end
+
+ def test_get_new_global_query
+ @request.session[:user_id] = 2
+ get :new
+ assert_response :success
+ assert_template 'new'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => nil }
+ end
+
+ def test_new_project_public_query
+ @request.session[:user_id] = 2
+ post :new,
+ :project_id => 'ecookbook',
+ :confirm => '1',
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
+ :query => {"name" => "test_new_project_public_query", "is_public" => "1"}
+
+ q = Query.find_by_name('test_new_project_public_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
+ assert q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_new_project_private_query
+ @request.session[:user_id] = 3
+ post :new,
+ :project_id => 'ecookbook',
+ :confirm => '1',
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
+ :query => {"name" => "test_new_project_private_query", "is_public" => "1"}
+
+ q = Query.find_by_name('test_new_project_private_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
+ assert !q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_new_global_private_query_with_custom_columns
+ @request.session[:user_id] = 3
+ post :new,
+ :confirm => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
+ :query => {"name" => "test_new_global_private_query", "is_public" => "1", "column_names" => ["", "tracker", "subject", "priority", "category"]}
+
+ q = Query.find_by_name('test_new_global_private_query')
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => q
+ assert !q.is_public?
+ assert !q.has_default_columns?
+ assert_equal [:tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
+ assert q.valid?
+ end
+
+ def test_get_edit_global_public_query
+ @request.session[:user_id] = 1
+ get :edit, :id => 4
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => 'checked' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_global_public_query
+ @request.session[:user_id] = 1
+ post :edit,
+ :id => 4,
+ :confirm => '1',
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
+ :query => {"name" => "test_edit_global_public_query", "is_public" => "1"}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
+ q = Query.find_by_name('test_edit_global_public_query')
+ assert q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_get_edit_global_private_query
+ @request.session[:user_id] = 3
+ get :edit, :id => 3
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => 'checked',
+ :disabled => 'disabled' }
+ end
+
+ def test_edit_global_private_query
+ @request.session[:user_id] = 3
+ post :edit,
+ :id => 3,
+ :confirm => '1',
+ :default_columns => '1',
+ :fields => ["status_id", "assigned_to_id"],
+ :operators => {"assigned_to_id" => "=", "status_id" => "o"},
+ :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
+ :query => {"name" => "test_edit_global_private_query", "is_public" => "1"}
+
+ assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
+ q = Query.find_by_name('test_edit_global_private_query')
+ assert !q.is_public?
+ assert q.has_default_columns?
+ assert q.valid?
+ end
+
+ def test_get_edit_project_private_query
+ @request.session[:user_id] = 3
+ get :edit, :id => 2
+ assert_response :success
+ assert_template 'edit'
+ assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]' }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => nil }
+ end
+
+ def test_get_edit_project_public_query
+ @request.session[:user_id] = 2
+ get :edit, :id => 1
+ assert_response :success
+ assert_template 'edit'
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query[is_public]',
+ :checked => 'checked'
+ }
+ assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
+ :name => 'query_is_for_all',
+ :checked => nil,
+ :disabled => 'disabled' }
+ end
+
+ def test_destroy
+ @request.session[:user_id] = 2
+ post :destroy, :id => 1
+ assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
+ assert_nil Query.find_by_id(1)
+ end
+end
diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb
new file mode 100644
index 000000000..acb6c1d21
--- /dev/null
+++ b/test/functional/repositories_bazaar_controller_test.rb
@@ -0,0 +1,137 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesBazaarControllerTest < Test::Unit::TestCase
+ fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository'
+
+ def setup
+ @controller = RepositoriesController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ Repository::Bazaar.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_show
+ get :show, :id => 3
+ assert_response :success
+ assert_template 'show'
+ assert_not_nil assigns(:entries)
+ assert_not_nil assigns(:changesets)
+ end
+
+ def test_browse_root
+ get :browse, :id => 3
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal 2, assigns(:entries).size
+ assert assigns(:entries).detect {|e| e.name == 'directory' && e.kind == 'dir'}
+ assert assigns(:entries).detect {|e| e.name == 'doc-mkdir.txt' && e.kind == 'file'}
+ end
+
+ def test_browse_directory
+ get :browse, :id => 3, :path => ['directory']
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['doc-ls.txt', 'document.txt', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
+ assert_equal 'file', entry.kind
+ assert_equal 'directory/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ get :browse, :id => 3, :path => [], :rev => 3
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['directory', 'doc-deleted.txt', 'doc-ls.txt', 'doc-mkdir.txt'], assigns(:entries).collect(&:name)
+ end
+
+ def test_changes
+ get :changes, :id => 3, :path => ['doc-mkdir.txt']
+ assert_response :success
+ assert_template 'changes'
+ assert_tag :tag => 'h2', :content => 'doc-mkdir.txt'
+ end
+
+ def test_entry_show
+ get :entry, :id => 3, :path => ['directory', 'doc-ls.txt']
+ assert_response :success
+ assert_template 'entry'
+ # Line 19
+ assert_tag :tag => 'th',
+ :content => /29/,
+ :attributes => { :class => /line-num/ },
+ :sibling => { :tag => 'td', :content => /Show help message/ }
+ end
+
+ def test_entry_download
+ get :entry, :id => 3, :path => ['directory', 'doc-ls.txt'], :format => 'raw'
+ assert_response :success
+ # File content
+ assert @response.body.include?('Show help message')
+ end
+
+ def test_directory_entry
+ get :entry, :id => 3, :path => ['directory']
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entry)
+ assert_equal 'directory', assigns(:entry).name
+ end
+
+ def test_diff
+ # Full diff of changeset 3
+ get :diff, :id => 3, :rev => 3
+ assert_response :success
+ assert_template 'diff'
+ # Line 22 removed
+ assert_tag :tag => 'th',
+ :content => /2/,
+ :sibling => { :tag => 'td',
+ :attributes => { :class => /diff_in/ },
+ :content => /Main purpose/ }
+ end
+
+ def test_annotate
+ get :annotate, :id => 3, :path => ['doc-mkdir.txt']
+ assert_response :success
+ assert_template 'annotate'
+ # Line 2, revision 3
+ assert_tag :tag => 'th', :content => /2/,
+ :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /3/ } },
+ :sibling => { :tag => 'td', :content => /jsmith/ },
+ :sibling => { :tag => 'td', :content => /Main purpose/ }
+ end
+ else
+ puts "Bazaar test repository NOT FOUND. Skipping functional tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb
index 1e101f59a..e12bb53ac 100644
--- a/test/functional/repositories_cvs_controller_test.rb
+++ b/test/functional/repositories_cvs_controller_test.rb
@@ -65,13 +65,24 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase
end
def test_browse_directory
- get :browse, :id => 1, :path => ['sources']
+ get :browse, :id => 1, :path => ['images']
assert_response :success
assert_template 'browse'
assert_not_nil assigns(:entries)
- entry = assigns(:entries).detect {|e| e.name == 'watchers_controller.rb'}
+ assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name)
+ entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+ assert_not_nil entry
assert_equal 'file', entry.kind
- assert_equal 'sources/watchers_controller.rb', entry.path
+ assert_equal 'images/edit.png', entry.path
+ end
+
+ def test_browse_at_given_revision
+ Project.find(1).repository.fetch_changesets
+ get :browse, :id => 1, :path => ['images'], :rev => 1
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
end
def test_entry
@@ -90,6 +101,14 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase
get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
assert_response :success
end
+
+ def test_directory_entry
+ get :entry, :id => 1, :path => ['sources']
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
def test_diff
Project.find(1).repository.fetch_changesets
diff --git a/test/functional/repositories_darcs_controller_test.rb b/test/functional/repositories_darcs_controller_test.rb
index fc77b8747..43c715924 100644
--- a/test/functional/repositories_darcs_controller_test.rb
+++ b/test/functional/repositories_darcs_controller_test.rb
@@ -60,13 +60,22 @@ class RepositoriesDarcsControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'browse'
assert_not_nil assigns(:entries)
- assert_equal 2, assigns(:entries).size
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
assert_not_nil entry
assert_equal 'file', entry.kind
assert_equal 'images/edit.png', entry.path
end
+ def test_browse_at_given_revision
+ Project.find(3).repository.fetch_changesets
+ get :browse, :id => 3, :path => ['images'], :rev => 1
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
def test_changes
get :changes, :id => 3, :path => ['images', 'edit.png']
assert_response :success
diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb
index f8b3cb2bb..339e22897 100644
--- a/test/functional/repositories_git_controller_test.rb
+++ b/test/functional/repositories_git_controller_test.rb
@@ -1,5 +1,5 @@
# redMine - project management software
-# Copyright (C) 2006-2007 Jean-Philippe Lang
+# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -61,13 +61,21 @@ class RepositoriesGitControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'browse'
assert_not_nil assigns(:entries)
- assert_equal 2, assigns(:entries).size
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
assert_not_nil entry
assert_equal 'file', entry.kind
assert_equal 'images/edit.png', entry.path
end
+ def test_browse_at_given_revision
+ get :browse, :id => 3, :path => ['images'], :rev => '7234cb2750b63f47bff735edc50a1c0a433c2518'
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
def test_changes
get :changes, :id => 3, :path => ['images', 'edit.png']
assert_response :success
@@ -93,6 +101,14 @@ class RepositoriesGitControllerTest < Test::Unit::TestCase
assert @response.body.include?('WITHOUT ANY WARRANTY')
end
+ def test_directory_entry
+ get :entry, :id => 3, :path => ['sources']
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
def test_diff
# Full diff of changeset 2f9c0091
get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb
index 736e38c83..cb870aa32 100644
--- a/test/functional/repositories_mercurial_controller_test.rb
+++ b/test/functional/repositories_mercurial_controller_test.rb
@@ -1,5 +1,5 @@
# redMine - project management software
-# Copyright (C) 2006-2007 Jean-Philippe Lang
+# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -60,13 +60,21 @@ class RepositoriesMercurialControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'browse'
assert_not_nil assigns(:entries)
- assert_equal 2, assigns(:entries).size
+ assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
assert_not_nil entry
assert_equal 'file', entry.kind
assert_equal 'images/edit.png', entry.path
end
+ def test_browse_at_given_revision
+ get :browse, :id => 3, :path => ['images'], :rev => 0
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['delete.png'], assigns(:entries).collect(&:name)
+ end
+
def test_changes
get :changes, :id => 3, :path => ['images', 'edit.png']
assert_response :success
@@ -91,7 +99,15 @@ class RepositoriesMercurialControllerTest < Test::Unit::TestCase
# File content
assert @response.body.include?('WITHOUT ANY WARRANTY')
end
-
+
+ def test_directory_entry
+ get :entry, :id => 3, :path => ['sources']
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entry)
+ assert_equal 'sources', assigns(:entry).name
+ end
+
def test_diff
# Full diff of changeset 4
get :diff, :id => 3, :rev => 4
diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb
index 9b21a13e8..dd56947fc 100644
--- a/test/functional/repositories_subversion_controller_test.rb
+++ b/test/functional/repositories_subversion_controller_test.rb
@@ -1,5 +1,5 @@
# redMine - project management software
-# Copyright (C) 2006-2007 Jean-Philippe Lang
+# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -58,11 +58,20 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'browse'
assert_not_nil assigns(:entries)
+ assert_equal ['folder', '.project', 'helloworld.c', 'textfile.txt'], assigns(:entries).collect(&:name)
entry = assigns(:entries).detect {|e| e.name == 'helloworld.c'}
assert_equal 'file', entry.kind
assert_equal 'subversion_test/helloworld.c', entry.path
end
-
+
+ def test_browse_at_given_revision
+ get :browse, :id => 1, :path => ['subversion_test'], :rev => 4
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entries)
+ assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
+ end
+
def test_entry
get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
assert_response :success
@@ -80,6 +89,14 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase
assert_response :success
end
+ def test_directory_entry
+ get :entry, :id => 1, :path => ['subversion_test', 'folder']
+ assert_response :success
+ assert_template 'browse'
+ assert_not_nil assigns(:entry)
+ assert_equal 'folder', assigns(:entry).name
+ end
+
def test_diff
get :diff, :id => 1, :rev => 3
assert_response :success
diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb
index fa4432295..e80a67728 100644
--- a/test/functional/timelog_controller_test.rb
+++ b/test/functional/timelog_controller_test.rb
@@ -22,7 +22,7 @@ require 'timelog_controller'
class TimelogController; def rescue_action(e) raise e end; end
class TimelogControllerTest < Test::Unit::TestCase
- fixtures :projects, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses
+ fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
def setup
@controller = TimelogController.new
@@ -78,30 +78,75 @@ class TimelogControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'report'
end
+
+ def test_report_all_time
+ get :report, :project_id => 1, :criterias => ['project', 'issue']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ end
+
+ def test_report_all_time_by_day
+ get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day'
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ assert_tag :tag => 'th', :content => '2007-03-12'
+ end
def test_report_one_criteria
- get :report, :project_id => 1, :period => 'week', :date_from => "2007-04-01", :date_to => "2007-04-30", :criterias => ['project']
+ get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project']
assert_response :success
assert_template 'report'
assert_not_nil assigns(:total_hours)
assert_equal "8.65", "%.2f" % assigns(:total_hours)
- end
+ end
def test_report_two_criterias
- get :report, :project_id => 1, :period => 'month', :date_from => "2007-01-01", :date_to => "2007-12-31", :criterias => ["member", "activity"]
+ get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"]
assert_response :success
assert_template 'report'
assert_not_nil assigns(:total_hours)
assert_equal "162.90", "%.2f" % assigns(:total_hours)
end
+ def test_report_custom_field_criteria
+ get :report, :project_id => 1, :criterias => ['project', 'cf_1']
+ assert_response :success
+ assert_template 'report'
+ assert_not_nil assigns(:total_hours)
+ assert_not_nil assigns(:criterias)
+ assert_equal 2, assigns(:criterias).size
+ assert_equal "162.90", "%.2f" % assigns(:total_hours)
+ # Custom field column
+ assert_tag :tag => 'th', :content => 'Database'
+ # Custom field row
+ assert_tag :tag => 'td', :content => 'MySQL',
+ :sibling => { :tag => 'td', :attributes => { :class => 'hours' },
+ :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' },
+ :content => '1' }}
+ end
+
def test_report_one_criteria_no_result
- get :report, :project_id => 1, :period => 'week', :date_from => "1998-04-01", :date_to => "1998-04-30", :criterias => ['project']
+ get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project']
assert_response :success
assert_template 'report'
assert_not_nil assigns(:total_hours)
assert_equal "0.00", "%.2f" % assigns(:total_hours)
- end
+ end
+
+ def test_report_csv_export
+ get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv"
+ assert_response :success
+ assert_equal 'text/csv', @response.content_type
+ lines = @response.body.chomp.split("\n")
+ # Headers
+ assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first
+ # Total row
+ assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last
+ end
def test_details_at_project_level
get :details, :project_id => 1
@@ -114,8 +159,8 @@ class TimelogControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:total_hours)
assert_equal "162.90", "%.2f" % assigns(:total_hours)
# display all time by default
- assert_nil assigns(:from)
- assert_nil assigns(:to)
+ assert_equal '2007-03-11'.to_date, assigns(:from)
+ assert_equal '2007-04-22'.to_date, assigns(:to)
end
def test_details_at_project_level_with_date_range
@@ -149,8 +194,8 @@ class TimelogControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:total_hours)
assert_equal 154.25, assigns(:total_hours)
# display all time by default
- assert_nil assigns(:from)
- assert_nil assigns(:to)
+ assert_equal '2007-03-11'.to_date, assigns(:from)
+ assert_equal '2007-04-22'.to_date, assigns(:to)
end
def test_details_csv_export
@@ -158,6 +203,6 @@ class TimelogControllerTest < Test::Unit::TestCase
assert_response :success
assert_equal 'text/csv', @response.content_type
assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n")
- assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,2,Feature request,Add ingredients categories,1.0,\"\"\n")
+ assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n")
end
end
diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb
index 0418a02b6..bf31e6614 100644
--- a/test/functional/wiki_controller_test.rb
+++ b/test/functional/wiki_controller_test.rb
@@ -22,7 +22,7 @@ require 'wiki_controller'
class WikiController; def rescue_action(e) raise e end; end
class WikiControllerTest < Test::Unit::TestCase
- fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
+ fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
def setup
@controller = WikiController.new
@@ -43,6 +43,10 @@ class WikiControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'show'
assert_tag :tag => 'h1', :content => /Another page/
+ # Included page with an inline image
+ assert_tag :tag => 'p', :content => /This is an inline image/
+ assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
+ :alt => 'This is a logo' }
end
def test_show_unexistent_page_without_edit_right
@@ -147,7 +151,7 @@ class WikiControllerTest < Test::Unit::TestCase
assert_template 'special_page_index'
pages = assigns(:pages)
assert_not_nil pages
- assert_equal 2, pages.size
+ assert_equal Project.find(1).wiki.pages.size, pages.size
assert_tag :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' },
:content => /CookBook documentation/
end
diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb
index 2442a8b8c..bbfe6952d 100644
--- a/test/unit/changeset_test.rb
+++ b/test/unit/changeset_test.rb
@@ -41,22 +41,22 @@ class ChangesetTest < Test::Unit::TestCase
end
def test_previous
- changeset = Changeset.find_by_revision(3)
- assert_equal Changeset.find_by_revision(2), changeset.previous
+ changeset = Changeset.find_by_revision('3')
+ assert_equal Changeset.find_by_revision('2'), changeset.previous
end
def test_previous_nil
- changeset = Changeset.find_by_revision(1)
+ changeset = Changeset.find_by_revision('1')
assert_nil changeset.previous
end
def test_next
- changeset = Changeset.find_by_revision(2)
- assert_equal Changeset.find_by_revision(3), changeset.next
+ changeset = Changeset.find_by_revision('2')
+ assert_equal Changeset.find_by_revision('3'), changeset.next
end
def test_next_nil
- changeset = Changeset.find_by_revision(4)
+ changeset = Changeset.find_by_revision('4')
assert_nil changeset.next
end
end
diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb
index 66499c003..fa2109131 100644
--- a/test/unit/helpers/application_helper_test.rb
+++ b/test/unit/helpers/application_helper_test.rb
@@ -20,7 +20,7 @@ require File.dirname(__FILE__) + '/../../test_helper'
class ApplicationHelperTest < HelperTestCase
include ApplicationHelper
include ActionView::Helpers::TextHelper
- fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents
+ fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules
def setup
super
@@ -134,8 +134,9 @@ class ApplicationHelperTest < HelperTestCase
def test_html_tags
to_test = {
- "<div>content</div>" => "<p>&lt;div>content&lt;/div></p>",
- "<script>some script;</script>" => "<p>&lt;script>some script;&lt;/script></p>",
+ "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
+ "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
+ "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
# do not escape pre/code tags
"<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
"<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
@@ -167,6 +168,24 @@ class ApplicationHelperTest < HelperTestCase
assert_equal '<p>{{hello_world}}</p>', textilizable(text)
end
+ def test_macro_include
+ @project = Project.find(1)
+ # include a page of the current project wiki
+ text = "{{include(Another page)}}"
+ assert textilizable(text).match(/This is a link to a ticket/)
+
+ @project = nil
+ # include a page of a specific project wiki
+ text = "{{include(ecookbook:Another page)}}"
+ assert textilizable(text).match(/This is a link to a ticket/)
+
+ text = "{{include(ecookbook:)}}"
+ assert textilizable(text).match(/CookBook documentation/)
+
+ text = "{{include(unknowidentifier:somepage)}}"
+ assert textilizable(text).match(/Unknow project/)
+ end
+
def test_date_format_default
today = Date.today
Setting.date_format = ''
diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb
index 7712b764e..36ba1fb45 100644
--- a/test/unit/issue_test.rb
+++ b/test/unit/issue_test.rb
@@ -20,6 +20,13 @@ require File.dirname(__FILE__) + '/../test_helper'
class IssueTest < Test::Unit::TestCase
fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
+ def test_create
+ issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
+ assert issue.save
+ issue.reload
+ assert_equal 1.5, issue.estimated_hours
+ end
+
def test_category_based_assignment
issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
@@ -48,6 +55,8 @@ class IssueTest < Test::Unit::TestCase
IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
# And 3 is a dupe of 2
IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
+ # And 3 is a dupe of 1 (circular duplicates)
+ IssueRelation.create(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
assert issue1.reload.duplicates.include?(issue2)
diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb
index f7da6ecb5..9af68c231 100644
--- a/test/unit/project_test.rb
+++ b/test/unit/project_test.rb
@@ -18,7 +18,7 @@
require File.dirname(__FILE__) + '/../test_helper'
class ProjectTest < Test::Unit::TestCase
- fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers
+ fixtures :projects, :issues, :issue_statuses, :journals, :journal_details, :users, :members, :roles, :projects_trackers, :trackers, :boards
def setup
@ecookbook = Project.find(1)
@@ -84,12 +84,15 @@ class ProjectTest < Test::Unit::TestCase
assert_equal 2, @ecookbook.members.size
# and 1 is locked
assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
+ # some boards
+ assert @ecookbook.boards.any?
@ecookbook.destroy
# make sure that the project non longer exists
assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
- # make sure all members have been removed
- assert_equal 0, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
+ # make sure related data was removed
+ assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
+ assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
end
def test_subproject_ok
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
index c00f47e5d..d291018fb 100644
--- a/test/unit/query_test.rb
+++ b/test/unit/query_test.rb
@@ -1,5 +1,5 @@
# redMine - project management software
-# Copyright (C) 2006-2007 Jean-Philippe Lang
+# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -18,12 +18,12 @@
require File.dirname(__FILE__) + '/../test_helper'
class QueryTest < Test::Unit::TestCase
- fixtures :projects, :users, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
+ fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
def test_query_with_multiple_custom_fields
query = Query.find(1)
assert query.valid?
- assert query.statement.include?("custom_values.value IN ('MySQL')")
+ assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement
assert_equal 1, issues.length
assert_equal Issue.find(3), issues.first
@@ -41,4 +41,34 @@ class QueryTest < Test::Unit::TestCase
c = q.columns.first
assert q.has_column?(c)
end
+
+ def test_editable_by
+ admin = User.find(1)
+ manager = User.find(2)
+ developer = User.find(3)
+
+ # Public query on project 1
+ q = Query.find(1)
+ assert q.editable_by?(admin)
+ assert q.editable_by?(manager)
+ assert !q.editable_by?(developer)
+
+ # Private query on project 1
+ q = Query.find(2)
+ assert q.editable_by?(admin)
+ assert !q.editable_by?(manager)
+ assert q.editable_by?(developer)
+
+ # Private query for all projects
+ q = Query.find(3)
+ assert q.editable_by?(admin)
+ assert !q.editable_by?(manager)
+ assert q.editable_by?(developer)
+
+ # Public query for all projects
+ q = Query.find(4)
+ assert q.editable_by?(admin)
+ assert !q.editable_by?(manager)
+ assert !q.editable_by?(developer)
+ end
end
diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb
index 15fcc8672..b7a3cf98e 100644
--- a/test/unit/repository_bazaar_test.rb
+++ b/test/unit/repository_bazaar_test.rb
@@ -36,13 +36,13 @@ class RepositoryBazaarTest < Test::Unit::TestCase
assert_equal 4, @repository.changesets.count
assert_equal 9, @repository.changes.count
- assert_equal 'Initial import', @repository.changesets.find_by_revision(1).comments
+ assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments
end
def test_fetch_changesets_incremental
@repository.fetch_changesets
# Remove changesets with revision > 5
- @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy)
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
@repository.reload
assert_equal 2, @repository.changesets.count
diff --git a/test/unit/repository_darcs_test.rb b/test/unit/repository_darcs_test.rb
index 1228976f1..1c8c1b8dd 100644
--- a/test/unit/repository_darcs_test.rb
+++ b/test/unit/repository_darcs_test.rb
@@ -35,13 +35,13 @@ class RepositoryDarcsTest < Test::Unit::TestCase
assert_equal 6, @repository.changesets.count
assert_equal 13, @repository.changes.count
- assert_equal "Initial commit.", @repository.changesets.find_by_revision(1).comments
+ assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments
end
def test_fetch_changesets_incremental
@repository.fetch_changesets
# Remove changesets with revision > 3
- @repository.changesets.find(:all, :conditions => 'revision > 3').each(&:destroy)
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3}
@repository.reload
assert_equal 3, @repository.changesets.count
diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb
index e6cfdf9b2..21ddf1e3a 100644
--- a/test/unit/repository_mercurial_test.rb
+++ b/test/unit/repository_mercurial_test.rb
@@ -35,13 +35,13 @@ class RepositoryMercurialTest < Test::Unit::TestCase
assert_equal 6, @repository.changesets.count
assert_equal 11, @repository.changes.count
- assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision(0).comments
+ assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments
end
def test_fetch_changesets_incremental
@repository.fetch_changesets
# Remove changesets with revision > 2
- @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy)
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
@repository.reload
assert_equal 3, @repository.changesets.count
diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb
index 879feece8..7a1c9df4a 100644
--- a/test/unit/repository_subversion_test.rb
+++ b/test/unit/repository_subversion_test.rb
@@ -35,13 +35,13 @@ class RepositorySubversionTest < Test::Unit::TestCase
assert_equal 8, @repository.changesets.count
assert_equal 16, @repository.changes.count
- assert_equal 'Initial import.', @repository.changesets.find_by_revision(1).comments
+ assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
end
def test_fetch_changesets_incremental
@repository.fetch_changesets
# Remove changesets with revision > 5
- @repository.changesets.find(:all, :conditions => 'revision > 5').each(&:destroy)
+ @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5}
@repository.reload
assert_equal 5, @repository.changesets.count
diff --git a/test/unit/time_entry_test.rb b/test/unit/time_entry_test.rb
new file mode 100644
index 000000000..f86e42eab
--- /dev/null
+++ b/test/unit/time_entry_test.rb
@@ -0,0 +1,46 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class TimeEntryTest < Test::Unit::TestCase
+ fixtures :issues, :projects, :users, :time_entries
+
+ def test_hours_format
+ assertions = { "2" => 2.0,
+ "21.1" => 21.1,
+ "2,1" => 2.1,
+ "7:12" => 7.2,
+ "10h" => 10.0,
+ "10 h" => 10.0,
+ "45m" => 0.75,
+ "45 m" => 0.75,
+ "3h15" => 3.25,
+ "3h 15" => 3.25,
+ "3 h 15" => 3.25,
+ "3 h 15m" => 3.25,
+ "3 h 15 m" => 3.25,
+ "3 hours" => 3.0,
+ "12min" => 0.2,
+ }
+
+ assertions.each do |k, v|
+ t = TimeEntry.new(:hours => k)
+ assert_equal v, t.hours
+ end
+ end
+end
diff --git a/vendor/plugins/acts_as_event/lib/acts_as_event.rb b/vendor/plugins/acts_as_event/lib/acts_as_event.rb
index a0d1822ad..d7f437a5e 100644
--- a/vendor/plugins/acts_as_event/lib/acts_as_event.rb
+++ b/vendor/plugins/acts_as_event/lib/acts_as_event.rb
@@ -25,11 +25,12 @@ module Redmine
module ClassMethods
def acts_as_event(options = {})
return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
- options[:datetime] ||= 'created_on'
- options[:title] ||= 'title'
- options[:description] ||= 'description'
- options[:author] ||= 'author'
+ options[:datetime] ||= :created_on
+ options[:title] ||= :title
+ options[:description] ||= :description
+ options[:author] ||= :author
options[:url] ||= {:controller => 'welcome'}
+ options[:type] ||= self.name.underscore.dasherize
cattr_accessor :event_options
self.event_options = options
send :include, Redmine::Acts::Event::InstanceMethods
@@ -41,11 +42,17 @@ module Redmine
base.extend ClassMethods
end
- %w(datetime title description author).each do |attr|
+ %w(datetime title description author type).each do |attr|
src = <<-END_SRC
def event_#{attr}
option = event_options[:#{attr}]
- option.is_a?(Proc) ? option.call(self) : send(option)
+ if option.is_a?(Proc)
+ option.call(self)
+ elsif option.is_a?(Symbol)
+ send(option)
+ else
+ option
+ end
end
END_SRC
class_eval src, __FILE__, __LINE__