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

This commit is contained in:
Eric Davis 2011-04-16 16:38:00 -07:00
commit d14417070d
419 changed files with 32299 additions and 3766 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,20 @@
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class PreviewsController < ApplicationController class PreviewsController < ApplicationController
before_filter :find_project before_filter :find_project

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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