Compare commits

..

17 Commits

Author SHA1 Message Date
Eric Davis
99ea01ec93 Added test coverage for Redmine::Plugin.add_hook 2008-07-28 17:30:32 -07:00
Eric Davis
e87140a719 More unit tests to cover hook_registered? 2008-07-28 17:12:51 -07:00
Eric Davis
ca8fb4026e More unit tests for the plugins. Fixed bug where a plugin's hook method could return nil. 2008-07-28 17:10:22 -07:00
Eric Davis
e14b86453e Implementing more unit tests for the plugin hooks 2008-07-28 17:02:23 -07:00
Eric Davis
cb485c92ef Added Redmine::Plugin::Hook::Manager.clear_listeners to remove all hook listeners. 2008-07-28 17:01:59 -07:00
Eric Davis
e7309d8c57 Added tests for Redmine::Plugin::Hook::Base 2008-07-28 16:40:18 -07:00
Eric Davis
d6808130dc Added test stubs for testing the Plugin API 2008-07-28 16:26:46 -07:00
Eric Davis
c5242b4386 Merge branch 'master' into plugin-hooks
Conflicts:

	lib/redmine/plugin.rb
2008-07-28 16:14:10 -07:00
Eric Davis
6615002df2 Added Number helper 2008-07-23 21:04:56 -07:00
Eric Davis
08058e6a02 Added documentation 2008-07-23 21:04:51 -07:00
Eric Davis
8995245a0c Added Base class for Plugin Hooks. #1296 2008-07-23 21:03:10 -07:00
Eric Davis
04434cd6ef Changed Hook API to use a Manager class. #1296 2008-07-23 21:01:43 -07:00
Eric Davis
fe22ef95a8 Added new hook for the issues_helper.show_details 2008-07-23 17:30:27 -07:00
Eric Davis
355143ca18 Added hooks for the member page. #1147 2008-07-23 17:27:06 -07:00
Eric Davis
5e1bcc6b24 Added support for saving a bulk edit. #1147 2008-07-23 17:27:01 -07:00
Eric Davis
00659ab8c5 Added hooks to issue_edit, issue_bulk_edit, and issue_show. #1147 2008-07-23 17:26:53 -07:00
Eric Davis
404e6164cb Adding Redmine::Plugin::Hook class to register and use hooks
#1147
2008-07-23 17:26:43 -07:00
423 changed files with 8103 additions and 18323 deletions

View File

@@ -15,19 +15,11 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AWSProjectWithRepository < ActionWebService::Struct
member :id, :int
member :identifier, :string
member :name, :string
member :is_public, :bool
member :repository, Repository
end
class SysApi < ActionWebService::API::Base
api_method :projects_with_repository_enabled,
api_method :projects,
:expects => [],
:returns => [[AWSProjectWithRepository]]
:returns => [[Project]]
api_method :repository_created,
:expects => [:string, :string, :string],
:expects => [:string, :string],
:returns => [:int]
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AccountController < ApplicationController
layout 'base'
helper :custom_fields
include CustomFieldsHelper
@@ -24,21 +25,13 @@ class AccountController < ApplicationController
# Show user's account
def show
@user = User.active.find(params[:id])
@custom_values = @user.custom_values
@user = User.find_active(params[:id])
@custom_values = @user.custom_values.find(:all, :include => :custom_field)
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
if @user != User.current && !User.current.admin? && @memberships.empty? && events.empty?
render_404 and return
end
rescue ActiveRecord::RecordNotFound
render_404
end
@@ -67,7 +60,6 @@ class AccountController < ApplicationController
token = Token.create(:user => user, :action => 'autologin')
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
call_hook(:controller_account_success_authentication_after, {:user => user })
redirect_back_or_default :controller => 'my', :action => 'page'
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AdminController < ApplicationController
layout 'base'
before_filter :require_admin
helper :sort
@@ -27,32 +28,24 @@ class AdminController < ApplicationController
def projects
sort_init 'name', 'asc'
sort_update %w(name is_public created_on)
sort_update
@status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
@status = params[:status] ? params[:status].to_i : 0
conditions = nil
conditions = ["status=?", @status] unless @status == 0
unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
end
@project_count = Project.count(:conditions => c.conditions)
@project_count = Project.count(:conditions => conditions)
@project_pages = Paginator.new self, @project_count,
per_page_option,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => c.conditions,
:conditions => conditions,
:limit => @project_pages.items_per_page,
:offset => @project_pages.current.offset
render :action => "projects", :layout => false if request.xhr?
end
def plugins
@plugins = Redmine::Plugin.all
end
# Loads the default configuration
# (roles, trackers, statuses, workflow, enumerations)
def default_configuration
@@ -86,8 +79,8 @@ class AdminController < ApplicationController
@flags = {
:default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
:file_repository_writable => File.writable?(Attachment.storage_path),
:plugin_assets_writable => File.writable?(Engines.public_directory),
:rmagick_available => Object.const_defined?(:Magick)
}
@plugins = Redmine::Plugin.registered_plugins
end
end

View File

@@ -16,20 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'uri'
require 'cgi'
class ApplicationController < ActionController::Base
class MissingSessionSecret < Exception ; end
layout 'base'
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
if session.first[:secret].blank?
raise MissingSessionSecret, "Missing session secret. Please run 'rake config/initializers/session_store.rb' to generate one"
else
protect_from_forgery :secret => session.first[:secret]
end
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
@@ -53,7 +43,7 @@ class ApplicationController < ActionController::Base
def find_current_user
if session[:user_id]
# existing session
(User.active.find(session[:user_id]) rescue nil)
(User.find_active(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.find_by_autologin_key(cookies[:autologin])
@@ -89,13 +79,7 @@ class ApplicationController < ActionController::Base
def require_login
if !User.current.logged?
# Extract only the basic url parameters on non-GET requests
if request.get?
url = url_for(params)
else
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
end
redirect_to :controller => "account", :action => "login", :back_url => url
redirect_to :controller => "account", :action => "login", :back_url => request.request_uri
return false
end
true
@@ -109,15 +93,11 @@ class ApplicationController < ActionController::Base
end
true
end
def deny_access
User.current.logged? ? render_403 : require_login
end
# Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action])
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : deny_access
allowed ? true : (User.current.logged? ? render_403 : require_login)
end
# make sure that the user is a member of the project (or admin) if project is private
@@ -137,16 +117,12 @@ class ApplicationController < ActionController::Base
end
def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s)
back_url = params[:back_url]
if !back_url.blank?
begin
uri = URI.parse(back_url)
# do not redirect user to another host or to the login or register page
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
redirect_to(back_url) and return
end
rescue URI::InvalidURIError
# redirect to default
uri = URI.parse(back_url)
# do not redirect user to another host
if uri.relative? || (uri.host == request.host)
redirect_to(back_url) and return
end
end
redirect_to default
@@ -188,7 +164,6 @@ class ApplicationController < ActionController::Base
# TODO: move to model
def attach_files(obj, attachments)
attached = []
unsaved = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
@@ -197,10 +172,7 @@ class ApplicationController < ActionController::Base
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
a.new_record? ? (unsaved << a) : (attached << a)
end
if unsaved.any?
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
attached << a unless a.new_record?
end
end
attached

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -16,12 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AttachmentsController < ApplicationController
layout 'base'
before_filter :find_project
before_filter :read_authorize, :except => :destroy
before_filter :delete_authorize, :only => :destroy
verify :method => :post, :only => :destroy
def show
if @attachment.is_diff?
@diff = File.new(@attachment.diskfile, "rb").read
@@ -35,40 +32,25 @@ class AttachmentsController < ApplicationController
end
def download
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
@attachment.increment_download
end
@attachment.increment_download if @attachment.container.is_a?(Version)
# images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
end
def destroy
# Make sure association callbacks are called
@attachment.container.attachments.delete(@attachment)
redirect_to :back
rescue ::ActionController::RedirectBackError
redirect_to :controller => 'projects', :action => 'show', :id => @project
end
private
def find_project
@attachment = Attachment.find(params[:id])
# Show 404 if the filename in the url is wrong
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
@project = @attachment.project
permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
allowed = User.current.allowed_to?(permission, @project)
allowed ? true : (User.current.logged? ? render_403 : require_login)
rescue ActiveRecord::RecordNotFound
render_404
end
def read_authorize
@attachment.visible? ? true : deny_access
end
def delete_authorize
@attachment.deletable? ? true : deny_access
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSourcesController < ApplicationController
layout 'base'
before_filter :require_admin
def index

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class BoardsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
helper :messages
@@ -35,14 +36,12 @@ class BoardsController < ApplicationController
end
def show
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
sort_init "#{Message.table_name}.updated_on", "desc"
sort_update
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
@topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}",
:include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CustomFieldsController < ApplicationController
layout 'base'
before_filter :require_admin
def index
@@ -46,7 +47,6 @@ class CustomFieldsController < ApplicationController
end
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'list', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')
@@ -59,7 +59,6 @@ class CustomFieldsController < ApplicationController
@custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : []
end
flash[:notice] = l(:notice_successful_update)
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
redirect_to :action => 'list', :tab => @custom_field.class.name
end
@trackers = Tracker.find(:all, :order => 'position')

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentsController < ApplicationController
layout 'base'
before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new]
before_filter :authorize
@@ -35,7 +36,6 @@ class DocumentsController < ApplicationController
else
@grouped = documents.group_by(&:category)
end
@document = @project.documents.build
render :layout => false if request.xhr?
end
@@ -71,6 +71,11 @@ class DocumentsController < ApplicationController
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
redirect_to :action => 'show', :id => @document
end
def destroy_attachment
@document.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'show', :id => @document
end
private
def find_project

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class EnumerationsController < ApplicationController
layout 'base'
before_filter :require_admin
def index

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueCategoriesController < ApplicationController
layout 'base'
menu_item :settings
before_filter :find_project, :authorize

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueRelationsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
def new

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueStatusesController < ApplicationController
layout 'base'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move ],

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -16,20 +16,23 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController
layout 'base'
menu_item :new_issue, :only => :new
before_filter :find_issue, :only => [:show, :edit, :reply]
before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
accept_key_auth :index, :show, :changes
before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes
helper :journals
helper :projects
include ProjectsHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issue_relations
include IssueRelationsHelper
helper :watchers
@@ -41,17 +44,11 @@ class IssuesController < ApplicationController
include SortHelper
include IssuesHelper
helper :timelog
include Redmine::Export::PDF
verify :method => :post,
:only => :destroy,
:render => { :nothing => true, :status => :method_not_allowed }
def index
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid?
limit = per_page_option
respond_to do |format|
@@ -71,7 +68,7 @@ class IssuesController < ApplicationController
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(issues_to_pdf(@issues, @project), :type => 'application/pdf', :filename => 'export.pdf') }
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
end
else
# Send html if the query is not valid
@@ -82,10 +79,9 @@ class IssuesController < ApplicationController
end
def changes
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
sort_init 'id', 'desc'
sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
if @query.valid?
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:conditions => @query.statement,
@@ -109,7 +105,7 @@ class IssuesController < ApplicationController
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
end
@@ -126,10 +122,7 @@ class IssuesController < ApplicationController
render :nothing => true, :layout => true
return
end
if params[:issue].is_a?(Hash)
@issue.attributes = params[:issue]
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
end
@issue.attributes = params[:issue]
@issue.author = User.current
default_status = IssueStatus.default
@@ -151,9 +144,7 @@ class IssuesController < ApplicationController
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
{ :action => 'show', :id => @issue })
redirect_to :controller => 'issues', :action => 'show', :id => @issue
return
end
end
@@ -186,12 +177,9 @@ class IssuesController < ApplicationController
@time_entry.attributes = params[:time_entry]
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
# Log spend time
if User.current.allowed_to?(:log_time, @project)
if current_role.allowed_to?(:log_time)
@time_entry.save
end
if !journal.new_record?
@@ -199,7 +187,6 @@ class IssuesController < ApplicationController
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
end
end
@@ -236,7 +223,6 @@ class IssuesController < ApplicationController
assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
unsaved_issue_ids = []
@issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
@@ -247,7 +233,9 @@ class IssuesController < ApplicationController
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
Redmine::Plugin::Hook::Manager.call_hook(:issue_bulk_edit_save, {:params => params, :issue => issue })
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
@@ -262,12 +250,12 @@ class IssuesController < ApplicationController
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
end
def move
@@ -324,65 +312,16 @@ class IssuesController < ApplicationController
@issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
end
def gantt
@gantt = Redmine::Helpers::Gantt.new(params)
retrieve_query
if @query.valid?
events = []
# Issues that have start and due dates
events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += Issue.find(:all,
:order => "start_date, effective_date",
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
:conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Versions
events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events
end
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
end
end
def calendar
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
retrieve_query
if @query.valid?
events = []
events += Issue.find(:all,
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
)
events += Version.find(:all, :include => :project,
:conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
@calendar.events = events
end
render :layout => false if request.xhr?
def destroy_attachment
a = @issue.attachments.find(params[:attachment_id])
a.destroy
journal = @issue.init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:old_value => a.filename)
journal.save
redirect_to :action => 'show', :id => @issue
end
def context_menu
@@ -390,22 +329,20 @@ class IssuesController < ApplicationController
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@assignables = @issue.assignable_users
@assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
:update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
:update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
if @project
@assignables = @project.assignable_users
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = request.env['HTTP_REFERER']
@@ -455,9 +392,9 @@ private
end
def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
allowed ? true : deny_access
return true unless params[:project_id]
@project = Project.find(params[:project_id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -16,13 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
layout 'base'
before_filter :find_journal
def edit
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController
layout 'base'
before_filter :find_member, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize

View File

@@ -16,21 +16,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MessagesController < ApplicationController
layout 'base'
menu_item :boards
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => [:preview, :edit, :destroy]
before_filter :authorize, :except => :preview
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
verify :xhr => true, :only => :quote
helper :watchers
helper :attachments
include AttachmentsHelper
# Show a topic and its replies
def show
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
@replies = @topic.children
@replies.reverse! if User.current.wants_comments_in_reverse_order?
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr?
@@ -46,7 +45,6 @@ class MessagesController < ApplicationController
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.save
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
attach_files(@message, params[:attachments])
redirect_to :action => 'show', :id => @message
end
@@ -59,7 +57,6 @@ class MessagesController < ApplicationController
@reply.board = @board
@topic.children << @reply
if !@reply.new_record?
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
attach_files(@reply, params[:attachments])
end
redirect_to :action => 'show', :id => @topic
@@ -67,8 +64,7 @@ class MessagesController < ApplicationController
# Edit a message
def edit
render_403 and return false unless @message.editable_by?(User.current)
if params[:message]
if params[:message] && User.current.allowed_to?(:edit_messages, @project)
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
end
@@ -81,27 +77,12 @@ class MessagesController < ApplicationController
# Delete a messages
def destroy
render_403 and return false unless @message.destroyable_by?(User.current)
@message.destroy
redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
{ :action => 'show', :id => @message.parent }
end
def quote
user = @message.author
text = @message.content
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
render(:update) { |page|
page.<< "$('message_content').value = \"#{content}\";"
page.show 'reply'
page << "Form.Element.focus('message_content');"
page << "Element.scrollTo('reply');"
page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
}
end
def preview
message = @board.messages.find_by_id(params[:id])
@attachements = message.attachments if message

View File

@@ -16,9 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MyController < ApplicationController
before_filter :require_login
helper :issues
layout 'base'
before_filter :require_login
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
'issuesreportedbyme' => :label_reported_issues,

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsController < ApplicationController
layout 'base'
before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview]
@@ -65,7 +66,6 @@ class NewsController < ApplicationController
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
else
show
render :action => 'show'
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ProjectsController < ApplicationController
layout 'base'
menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap
@@ -27,12 +28,14 @@ class ProjectsController < ApplicationController
before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity
accept_key_auth :activity, :calendar
helper :sort
include SortHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issues
helper IssuesHelper
helper :queries
@@ -67,7 +70,6 @@ class ProjectsController < ApplicationController
:order => 'name')
@project = Project.new(params[:project])
if request.get?
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
@@ -82,11 +84,6 @@ class ProjectsController < ApplicationController
# Show @project
def show
if params[:jump]
# try to redirect to the requested menu item
redirect_to_project_menu_item(@project, params[:jump]) && return
end
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
@subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@@ -191,26 +188,18 @@ class ProjectsController < ApplicationController
def add_file
if request.post?
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
attachments = attach_files(container, params[:attachments])
if !attachments.empty? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments)
end
@version = @project.versions.find_by_id(params[:version_id])
attachments = attach_files(@version, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
return
end
@versions = @project.versions.sort
end
def list_files
sort_init 'filename', 'asc'
sort_update 'filename' => "#{Attachment.table_name}.filename",
'created_on' => "#{Attachment.table_name}.created_on",
'size' => "#{Attachment.table_name}.filesize",
'downloads' => "#{Attachment.table_name}.downloads"
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
sort_init "#{Attachment.table_name}.filename", "asc"
sort_update
@versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
end
@@ -232,19 +221,16 @@ class ProjectsController < ApplicationController
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end
begin; @date_to = params[:from].to_date; rescue; end
end
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
:with_subprojects => @with_subprojects,
:author => @author)
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects)
@activity.scope_select {|t| !params["show_#{t}"].nil?}
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
@activity.default_scope! if @activity.scope.empty?
events = @activity.events(@date_from, @date_to)
@@ -254,18 +240,101 @@ class ProjectsController < ApplicationController
render :layout => false if request.xhr?
}
format.atom {
title = l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
title = (@scope.size == 1) ? l("label_#{@scope.first.singularize}_plural") : l(:label_activity)
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
}
end
end
def calendar
@trackers = @project.rolled_up_trackers
retrieve_selected_tracker_ids(@trackers)
rescue ActiveRecord::RecordNotFound
render_404
if params[:year] and params[:year].to_i > 1900
@year = params[:year].to_i
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
@month = params[:month].to_i
end
end
@year ||= Date.today.year
@month ||= Date.today.month
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
events = []
@project.issues_with_subprojects(@with_subprojects) do
events += Issue.find(:all,
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
) unless @selected_tracker_ids.empty?
events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
end
@calendar.events = events
render :layout => false if request.xhr?
end
def gantt
@trackers = @project.rolled_up_trackers
retrieve_selected_tracker_ids(@trackers)
if params[:year] and params[:year].to_i >0
@year_from = params[:year].to_i
if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
@month_from = params[:month].to_i
else
@month_from = 1
end
else
@month_from ||= Date.today.month
@year_from ||= Date.today.year
end
zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2
months = (params[:months] || User.current.pref[:gantt_months]).to_i
@months = (months > 0 && months < 25) ? months : 6
# Save gantt paramters as user preference (zoom and months count)
if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
User.current.preference.save
end
@date_from = Date.civil(@year_from, @month_from, 1)
@date_to = (@date_from >> @months) - 1
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
@events = []
@project.issues_with_subprojects(@with_subprojects) do
# Issues that have start and due dates
@events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
) unless @selected_tracker_ids.empty?
# Issues that don't have a due date but that are assigned to a version with a date
@events += Issue.find(:all,
:order => "start_date, effective_date",
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
:conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
) unless @selected_tracker_ids.empty?
@events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
end
@events.sort! {|x,y| x.start_date <=> y.start_date }
if params[:format]=='pdf'
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
render :template => "projects/gantt.rfpdf", :layout => false
elsif params[:format]=='png' && respond_to?('gantt_image')
image = gantt_image(@events, @date_from, @months, @zoom)
image.format = 'PNG'
send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
else
render :template => "projects/gantt.rhtml"
end
end
private

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueriesController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_query, :except => :new
before_filter :find_optional_project, :only => :new

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ReportsController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_project, :authorize

View File

@@ -23,6 +23,7 @@ class ChangesetNotFound < Exception; end
class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
layout 'base'
menu_item :repository
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
@@ -44,21 +45,6 @@ class RepositoriesController < ApplicationController
render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
end
def committers
@committers = @repository.committers
@users = @project.users
additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
@users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
@users.compact!
@users.sort!
if request.post? && params[:committers].is_a?(Hash)
# Build a hash with repository usernames as keys and corresponding user ids as values
@repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'committers', :id => @project
end
end
def destroy
@repository.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
@@ -99,8 +85,7 @@ class RepositoriesController < ApplicationController
params['page']
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset,
:include => :user)
:offset => @changeset_pages.current.offset)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
@@ -127,9 +112,6 @@ class RepositoriesController < ApplicationController
end
def annotate
@entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end
@@ -137,6 +119,11 @@ class RepositoriesController < ApplicationController
def revision
@changeset = @repository.changesets.find_by_revision(@rev)
raise ChangesetNotFound unless @changeset
@changes_count = @changeset.changes.size
@changes_pages = Paginator.new self, @changes_count, 150, params['page']
@changes = @changeset.changes.find(:all,
:limit => @changes_pages.items_per_page,
:offset => @changes_pages.current.offset)
respond_to do |format|
format.html

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class RolesController < ApplicationController
layout 'base'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :move ],
@@ -79,6 +80,27 @@ class RolesController < ApplicationController
redirect_to :action => 'list'
end
def workflow
@role = Role.find_by_id(params[:role_id])
@tracker = Tracker.find_by_id(params[:tracker_id])
if request.post?
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
(params[:issue_status] || []).each { |old, news|
news.each { |new|
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
}
}
if @role.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'workflow', :role_id => @role, :tracker_id => @tracker
end
end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
end
def report
@roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }

View File

@@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SearchController < ApplicationController
layout 'base'
before_filter :find_optional_project
helper :messages

View File

@@ -5,19 +5,20 @@
# 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 SettingsController < ApplicationController
layout 'base'
before_filter :require_admin
def index
edit
render :action => 'edit'
@@ -39,21 +40,17 @@ class SettingsController < ApplicationController
@options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
@deliveries = ActionMailer::Base.perform_deliveries
@guessed_host_and_path = request.host_with_port.dup
@guessed_host_and_path << ('/'+ request.relative_url_root.gsub(%r{^\/}, '')) unless request.relative_url_root.blank?
end
def plugin
@plugin = Redmine::Plugin.find(params[:id])
plugin_id = params[:id].to_sym
@plugin = Redmine::Plugin.registered_plugins[plugin_id]
if request.post?
Setting["plugin_#{@plugin.id}"] = params[:settings]
Setting["plugin_#{plugin_id}"] = params[:settings]
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => @plugin.id
redirect_to :action => 'plugin', :id => params[:id]
end
@partial = @plugin.settings[:partial]
@settings = Setting["plugin_#{@plugin.id}"]
rescue Redmine::PluginNotFound
render_404
@settings = Setting["plugin_#{plugin_id}"]
end
end

View File

@@ -23,17 +23,18 @@ class SysController < ActionController::Base
before_invocation :check_enabled
# Returns the projects list, with their repositories
def projects_with_repository_enabled
Project.has_module(:repository).find(:all, :include => :repository, :order => 'identifier')
def projects
Project.find(:all, :include => :repository)
end
# Registers a repository for the given project identifier
def repository_created(identifier, vendor, url)
# (Subversion specific)
def repository_created(identifier, url)
project = Project.find_by_identifier(identifier)
# Do not create the repository if the project has already one
return 0 unless project && project.repository.nil?
logger.debug "Repository for #{project.name} was created"
repository = Repository.factory(vendor, :project => project, :url => url)
repository = Repository.factory('Subversion', :project => project, :url => url)
repository.save
repository.id || 0
end

View File

@@ -16,9 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TimelogController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_project, :authorize, :only => [:edit, :destroy]
before_filter :find_optional_project, :only => [:report, :details]
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
@@ -54,12 +54,11 @@ class TimelogController < ApplicationController
}
# Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end if @project
end
# Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@@ -85,10 +84,9 @@ class TimelogController < ApplicationController
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE"
sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)]
sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql)
@@ -138,21 +136,11 @@ class TimelogController < ApplicationController
def details
sort_init 'spent_on', 'desc'
sort_update 'spent_on' => 'spent_on',
'user' => 'user_id',
'activity' => 'activity_id',
'project' => "#{Project.table_name}.name",
'issue' => 'issue_id',
'hours' => 'hours'
sort_update
cond = ARCondition.new
if @project.nil?
cond << Project.allowed_to_condition(User.current, :view_time_entries)
elsif @issue.nil?
cond << @project.project_condition(Setting.display_subprojects_issues?)
else
cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
end
cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
@@ -197,12 +185,9 @@ class TimelogController < ApplicationController
render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_back_or_default :action => 'details', :project_id => @time_entry.project
redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
return
end
end
@@ -213,7 +198,7 @@ class TimelogController < ApplicationController
@time_entry.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :back
rescue ::ActionController::RedirectBackError
rescue RedirectBackError
redirect_to :action => 'details', :project_id => @time_entry.project
end
@@ -235,16 +220,6 @@ private
render_404
end
def find_optional_project
if !params[:issue_id].blank?
@issue = Issue.find(params[:issue_id])
@project = @issue.project
elsif !params[:project_id].blank?
@project = Project.find(params[:project_id])
end
deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
end
# Retrieves the date range based on predefined ranges or specific from/to param dates
def retrieve_date_range
@free_period = false
@@ -287,7 +262,7 @@ private
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
@from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TrackersController < ApplicationController
layout 'base'
before_filter :require_admin
def index

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class UsersController < ApplicationController
layout 'base'
before_filter :require_admin
helper :sort
@@ -30,22 +31,18 @@ class UsersController < ApplicationController
def list
sort_init 'login', 'asc'
sort_update %w(login firstname lastname mail admin created_on last_login_on)
sort_update
@status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
unless params[:name].blank?
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
end
conditions = "status <> 0"
conditions = ["status=?", @status] unless @status == 0
@user_count = User.count(:conditions => c.conditions)
@user_count = User.count(:conditions => conditions)
@user_pages = Paginator.new self, @user_count,
per_page_option,
params['page']
@users = User.find :all,:order => sort_clause,
:conditions => c.conditions,
:conditions => conditions,
:limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset
@@ -75,11 +72,7 @@ class UsersController < ApplicationController
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
@user.attributes = params[:user]
# Was the account actived ? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
if @user.save
Mailer.deliver_account_activated(@user) if was_activated
if @user.update_attributes(params[:user])
flash[:notice] = l(:notice_successful_update)
# Give a string to redirect_to otherwise it would use status param as the response code
redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class VersionsController < ApplicationController
layout 'base'
menu_item :roadmap
before_filter :find_project, :authorize
@@ -33,10 +34,16 @@ class VersionsController < ApplicationController
@version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
rescue
flash[:error] = l(:notice_unable_delete_version)
flash[:error] = "Unable to delete version"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def destroy_file
@version.attachments.find(params[:attachment_id]).destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
def status_by
respond_to do |format|
format.html { render :action => 'show' }

View File

@@ -16,38 +16,31 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WatchersController < ApplicationController
before_filter :find_project
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
before_filter :authorize, :only => :new
layout 'base'
before_filter :require_login, :find_project, :check_project_privacy
verify :method => :post,
:only => [ :watch, :unwatch ],
:render => { :nothing => true, :status => :method_not_allowed }
def watch
set_watcher(User.current, true)
end
def unwatch
set_watcher(User.current, false)
end
def new
@watcher = Watcher.new(params[:watcher])
@watcher.watchable = @watched
@watcher.save if request.post?
def add
user = User.current
@watched.add_watcher(user)
respond_to do |format|
format.html { redirect_to :back }
format.js do
render :update do |page|
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
end
end
format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
end
rescue ::ActionController::RedirectBackError
rescue RedirectBackError
render :text => 'Watcher added.', :layout => true
end
def remove
user = User.current
@watched.remove_watcher(user)
respond_to do |format|
format.html { redirect_to :back }
format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
end
rescue RedirectBackError
render :text => 'Watcher removed.', :layout => true
end
private
def find_project
klass = Object.const_get(params[:object_type].camelcase)
@@ -57,14 +50,4 @@ private
rescue
render_404
end
def set_watcher(user, watching)
@watched.set_watcher(user, watching)
respond_to do |format|
format.html { redirect_to :back }
format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
end
rescue ::ActionController::RedirectBackError
render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WelcomeController < ApplicationController
layout 'base'
def index
@news = News.latest User.current

View File

@@ -18,10 +18,10 @@
require 'diff'
class WikiController < ApplicationController
layout 'base'
before_filter :find_wiki, :authorize
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
helper :attachments
include AttachmentsHelper
@@ -39,11 +39,6 @@ class WikiController < ApplicationController
end
return
end
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
# Redirects user to the current version if he's not allowed to view previous versions
redirect_to :version => nil
return
end
@content = @page.content_for_version(params[:version])
if params[:export] == 'html'
export = render_to_string :action => 'export', :layout => false
@@ -64,13 +59,10 @@ class WikiController < ApplicationController
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@content.text = initial_page_content(@page) if @content.text.blank?
@content.text = "h1. #{@page.pretty_title}" if @content.text.blank?
# don't keep previous comment
@content.comments = nil
if request.get?
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
else
if request.post?
if !@page.new_record? && @content.text == params[:content][:text]
# don't save if text wasn't changed
redirect_to :action => 'index', :id => @project, :page => @page.title
@@ -82,7 +74,6 @@ class WikiController < ApplicationController
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
@@ -93,7 +84,8 @@ class WikiController < ApplicationController
# rename a page
def rename
return render_403 unless editable?
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.redirect_existing_links = true
# used to display the *original* title if some AR validation errors occur
@original_title = @page.pretty_title
@@ -104,12 +96,15 @@ class WikiController < ApplicationController
end
def protect
@page.update_attribute :protected, params[:protected]
redirect_to :action => 'index', :id => @project, :page => @page.title
page = @wiki.find_page(params[:page])
page.update_attribute :protected, params[:protected]
redirect_to :action => 'index', :id => @project, :page => page.title
end
# show page history
def history
@page = @wiki.find_page(params[:page])
@version_count = @page.content.versions.count
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text
@@ -123,19 +118,21 @@ class WikiController < ApplicationController
end
def diff
@page = @wiki.find_page(params[:page])
@diff = @page.diff(params[:version], params[:version_from])
render_404 unless @diff
end
def annotate
@page = @wiki.find_page(params[:page])
@annotate = @page.annotate(params[:version])
render_404 unless @annotate
end
# remove a wiki page and its history
def destroy
return render_403 unless editable?
@page.destroy
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.destroy if @page
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end
@@ -177,11 +174,19 @@ class WikiController < ApplicationController
end
def add_attachment
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title
end
def destroy_attachment
@page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'index', :page => @page.title
end
private
def find_wiki
@@ -192,21 +197,8 @@ private
render_404
end
# Finds the requested page and returns a 404 error if it doesn't exist
def find_existing_page
@page = @wiki.find_page(params[:page])
render_404 if @page.nil?
end
# Returns true if the current user is allowed to edit the page, otherwise false
def editable?(page = @page)
page.editable_by?(User.current)
end
# Returns the default content of a new wiki page
def initial_page_content(page)
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper unless self.instance_of?(helper)
helper.instance_method(:initial_page_content).bind(self).call(page)
end
end

View File

@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WikisController < ApplicationController
layout 'base'
menu_item :settings
before_filter :find_project, :authorize

View File

@@ -1,45 +0,0 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WorkflowsController < ApplicationController
before_filter :require_admin
def index
@workflow_counts = Workflow.count_by_tracker_and_role
end
def edit
@role = Role.find_by_id(params[:role_id])
@tracker = Tracker.find_by_id(params[:tracker_id])
if request.post?
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
(params[:issue_status] || []).each { |old, news|
news.each { |new|
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
}
}
if @role.save
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
end
end
@roles = Role.find(:all, :order => 'builtin, position')
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
end
end

View File

@@ -17,7 +17,7 @@
module AdminHelper
def project_status_options_for_select(selected)
options_for_select([[l(:label_all), ''],
options_for_select([[l(:label_all), "*"],
[l(:status_active), 1]], selected)
end
end

View File

@@ -5,32 +5,26 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'coderay'
require 'coderay/helpers/file_type'
require 'forwardable'
require 'cgi'
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
include GravatarHelper::PublicMethods
extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
def current_role
@current_role ||= User.current.role_for_project(@project)
end
# Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action)
User.current.allowed_to?({:controller => controller, :action => action}, @project)
@@ -41,24 +35,18 @@ module ApplicationHelper
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
end
# Display a link to remote if user is authorized
def link_to_remote_if_authorized(name, options = {}, html_options = nil)
url = options[:url] || {}
link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
end
# Display a link to user's account page
def link_to_user(user, options={})
(user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
def link_to_user(user)
user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end
def link_to_issue(issue, options={})
options[:class] ||= ''
options[:class] << ' issue'
options[:class] << ' closed' if issue.closed?
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
end
# Generates a link to an attachment.
# Options:
# * :text - Link text (default to attachment filename)
@@ -66,124 +54,70 @@ module ApplicationHelper
def link_to_attachment(attachment, options={})
text = options.delete(:text) || attachment.filename
action = options.delete(:download) ? 'download' : 'show'
link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
end
def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
onclick << "return false;"
link_to(name, "#", :onclick => onclick)
end
def image_to_function(name, function, html_options = {})
html_options.symbolize_keys!
tag(:input, html_options.merge({
:type => "image", :src => image_path(name),
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
tag(:input, html_options.merge({
:type => "image", :src => image_path(name),
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
}))
end
def prompt_to_remote(name, text, param, url, html_options = {})
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
link_to name, {}, html_options
end
def format_date(date)
return nil unless date
# "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
date.strftime(@date_format)
end
def format_time(time, include_date = true)
return nil unless time
time = time.to_time if time.is_a?(String)
zone = User.current.time_zone
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end
def format_activity_title(text)
h(truncate_single_line(text, 100))
end
def format_activity_day(date)
date == Date.today ? l(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
end
def distance_of_date_in_words(from_date, to_date = 0)
from_date = from_date.to_date if from_date.respond_to?(:to_date)
to_date = to_date.to_date if to_date.respond_to?(:to_date)
distance_in_days = (to_date - from_date).abs
lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
end
def due_date_distance_in_words(date)
if date
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
end
end
def render_page_hierarchy(pages, node=nil)
content = ''
if pages[node]
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
content << "</li>\n"
end
content << "</ul>\n"
end
content
end
# Renders flash messages
def render_flash_messages
s = ''
flash.each do |k,v|
s << content_tag('div', v, :class => "flash #{k}")
end
s
end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end
def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end
def authoring(created, author, options={})
time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
link_to(distance_of_time_in_words(Time.now, created),
{:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
:title => format_time(created))
def authoring(created, author)
time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
l(options[:label] || :label_added_time_by, author_tag, time_tag)
l(:label_added_time_by, author_tag, time_tag)
end
def l_or_humanize(s, options={})
k = "#{options[:prefix]}#{s}".to_sym
l_has_string?(k) ? l(k) : s.to_s.humanize
def l_or_humanize(s)
l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
end
def day_name(day)
l(:general_day_names).split(',')[day-1]
end
def month_name(month)
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end
@@ -192,7 +126,7 @@ module ApplicationHelper
type = CodeRay::FileType[name]
type ? CodeRay.scan(content, type).html : h(content)
end
def to_path_param(path)
path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
end
@@ -202,51 +136,51 @@ module ApplicationHelper
url_param = params.dup
# don't reuse params if filters are present
url_param.clear if url_param.has_key?(:set_filter)
html = ''
html << link_to_remote(('&#171; ' + l(:label_previous)),
html = ''
html << link_to_remote(('&#171; ' + l(:label_previous)),
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.previous),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
html << (pagination_links_each(paginator, options) do |n|
link_to_remote(n.to_s,
link_to_remote(n.to_s,
{:url => {:params => url_param.merge(page_param => n)},
:update => 'content',
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => n))})
end || '')
html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.next),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
unless count.nil?
html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
end
html
html
end
def per_page_links(selected=nil)
url_param = params.dup
url_param.clear if url_param.has_key?(:set_filter)
links = Setting.per_page_options_array.collect do |n|
n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
{:href => url_for(url_param.merge(:per_page => n))})
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
def breadcrumb(*args)
elements = args.flatten
elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
end
def html_title(*args)
if args.empty?
title = []
@@ -281,30 +215,32 @@ module ApplicationHelper
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
only_path = options.delete(:only_path) == false ? false : true
# when using an image link, try to use an attachment, if possible
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments
attachments = attachments.sort_by(&:created_on).reverse
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
style = $1
filename = $6.downcase
filename = $6
rf = Regexp.new(filename, Regexp::IGNORECASE)
# search for the picture in attachments
if found = attachments.detect { |att| att.filename.downcase == filename }
if found = attachments.detect { |att| att.filename =~ rf }
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!"
else
m
"!#{style}#{filename}!"
end
end
end
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
text = (Setting.text_formatting == 'textile') ?
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
simple_format(auto_link(h(text)))
# different methods for formatting wiki links
case options[:wiki_links]
@@ -317,11 +253,11 @@ module ApplicationHelper
else
format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
end
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
# Wiki links
#
#
# Examples:
# [[mypage]]
# [[mypage|mytext]]
@@ -339,7 +275,7 @@ module ApplicationHelper
page = $2
title ||= $1 if page.blank?
end
if link_project && link_project.wiki
# extract anchor
anchor = nil
@@ -360,7 +296,7 @@ module ApplicationHelper
end
# Redmine links
#
#
# Examples:
# Issues:
# #52 -> Link to issue #52
@@ -383,9 +319,7 @@ module ApplicationHelper
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
# Forum messages:
# message#1218 -> Link to message with id 1218
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil
if esc.nil?
@@ -399,7 +333,7 @@ module ApplicationHelper
oid = oid.to_i
case prefix
when nil
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
@@ -415,16 +349,6 @@ module ApplicationHelper
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version'
end
when 'message'
if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
:controller => 'messages',
:action => 'show',
:board_id => message.board,
:id => message.root,
:anchor => (message.parent ? "message-#{message.id}" : nil)},
:class => 'message'
end
end
elsif sep == ':'
# removes the double quotes if any
@@ -467,10 +391,10 @@ module ApplicationHelper
end
leading + (link || "#{prefix}#{sep}#{oid}")
end
text
end
# Same as Rails' simple_format helper without using paragraphs
def simple_format_without_paragraph(text)
text.to_s.
@@ -478,7 +402,7 @@ module ApplicationHelper
gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
end
def error_messages_for(object_name, options = {})
options = options.symbolize_keys
object = instance_variable_get("@#{object_name}")
@@ -487,23 +411,23 @@ module ApplicationHelper
full_messages = []
object.errors.each do |attr, msg|
next if msg.nil?
msg = [msg] unless msg.is_a?(Array)
msg = msg.first if msg.is_a? Array
if attr == "base"
full_messages << l(*msg)
full_messages << l(msg)
else
full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(*msg) unless attr == "custom_values"
full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
end
end
# retrieve custom values error messages
if object.errors[:custom_values]
object.custom_values.each do |v|
object.custom_values.each do |v|
v.errors.each do |attr, msg|
next if msg.nil?
msg = [msg] unless msg.is_a?(Array)
full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(*msg)
msg = msg.first if msg.is_a? Array
full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
end
end
end
end
content_tag("div",
content_tag(
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
@@ -515,35 +439,34 @@ module ApplicationHelper
""
end
end
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
(blank ? [["(auto)", ""]] : []) +
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
end
def label_tag_for(name, option_tags = nil, options = {})
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
content_tag("label", label_text)
end
def labelled_tabular_form_for(name, object, options, &proc)
options[:html] ||= {}
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
end
def back_url_hidden_field_tag
back_url = params[:back_url] || request.env['HTTP_REFERER']
back_url = CGI.unescape(back_url.to_s)
hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
hidden_field_tag('back_url', back_url) unless back_url.blank?
end
def check_all_links(form_name)
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
" | " +
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
end
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts[1] = pcts[1] - pcts[0]
@@ -552,13 +475,13 @@ module ApplicationHelper
legend = options[:legend] || ''
content_tag('table',
content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
(pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent')
end
def context_menu_link(name, url, options={})
options[:class] ||= ''
if options.delete(:selected)
@@ -574,7 +497,7 @@ module ApplicationHelper
end
link_to name, url, options
end
def calendar_for(field_id)
include_calendar_headers_tags
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
@@ -592,36 +515,26 @@ module ApplicationHelper
end
end
end
def wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
help_link = l(:setting_text_formatting) + ': ' +
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
end
def content_for(name, content = nil, &block)
@has_content ||= {}
@has_content[name] = true
super(name, content, &block)
end
def has_content?(name)
(@has_content && @has_content[name]) || false
end
# Returns the avatar image tag for the given +user+ if avatars are enabled
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = { })
if Setting.gravatar_enabled?
email = nil
if user.respond_to?(:mail)
email = user.mail
elsif user.to_s =~ %r{<(.+?)>}
email = $1
end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
end
end
private
def wiki_helper
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper
return self
end
end

View File

@@ -16,15 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module AttachmentsHelper
# Displays view/delete links to the attachments of the given object
# Options:
# :author -- author names are not displayed if set to false
def link_to_attachments(container, options = {})
options.assert_valid_keys(:author)
if container.attachments.any?
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
# displays the links to a collection of attachments
def link_to_attachments(attachments, options = {})
if attachments.any?
render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
end
end

View File

@@ -0,0 +1,85 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'iconv'
require 'rfpdf/chinese'
module IfpdfHelper
class IFPDF < FPDF
include GLoc
attr_accessor :footer_date
def initialize(lang)
super()
set_language_if_valid lang
case current_language.to_s
when 'ja'
extend(PDF_Japanese)
AddSJISFont()
@font_for_content = 'SJIS'
@font_for_footer = 'SJIS'
when 'zh'
extend(PDF_Chinese)
AddGBFont()
@font_for_content = 'GB'
@font_for_footer = 'GB'
when 'zh-tw'
extend(PDF_Chinese)
AddBig5Font()
@font_for_content = 'Big5'
@font_for_footer = 'Big5'
else
@font_for_content = 'Arial'
@font_for_footer = 'Helvetica'
end
SetCreator("redMine #{Redmine::VERSION}")
SetFont(@font_for_content)
end
def SetFontStyle(style, size)
SetFont(@font_for_content, style, size)
end
def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
@ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
# these quotation marks are not correctly rendered in the pdf
txt = txt.gsub(/[“”]/, '"') if txt
txt = begin
# 0x5c char handling
txtar = txt.split('\\')
txtar << '' if txt[-1] == ?\\
txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
rescue
txt
end || ''
super w,h,txt,border,ln,align,fill,link
end
def Footer
SetFont(@font_for_footer, 'I', 8)
SetY(-15)
SetX(15)
Cell(0, 5, @footer_date, 0, 0, 'L')
SetY(-15)
SetX(-30)
Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
end
end
end

View File

@@ -33,16 +33,6 @@ module IssuesHelper
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
# Returns a string of css classes that apply to the given issue
def css_issue_classes(issue)
s = "issue status-#{issue.status.position} priority-#{issue.priority.position}"
s << ' closed' if issue.closed?
s << ' overdue' if issue.overdue?
s << ' created-by-me' if User.current.logged? && issue.author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && issue.assigned_to_id == User.current.id
s
end
def sidebar_queries
unless @sidebar_queries
# User can see public queries and his own queries
@@ -85,9 +75,6 @@ module IssuesHelper
when 'fixed_version_id'
v = Version.find_by_id(detail.value) and value = v.name if detail.value
v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
when 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
end
when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key)
@@ -99,8 +86,9 @@ module IssuesHelper
when 'attachment'
label = l(:label_attachment)
end
call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
Redmine::Plugin::Hook::Manager.call_hook(:issues_helper_show_details, {:detail => detail, :label => label, :value => value, :old_value => old_value })
label ||= detail.prop_key
value ||= detail.value
old_value ||= detail.old_value

View File

@@ -21,12 +21,12 @@ module JournalsHelper
editable = journal.editable_by?(User.current)
links = []
if !journal.notes.blank?
links << link_to_remote(image_tag('comment.png'),
{ :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} },
:title => l(:button_quote)) if options[:reply_links]
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
:title => l(:button_edit)) if editable
links << link_to_remote(image_tag('comment.png'),
{ :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} },
:title => l(:button_reply)) if options[:reply_links]
end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)

View File

@@ -21,6 +21,18 @@ module ProjectsHelper
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end
def format_activity_title(text)
h(truncate_single_line(text, 100))
end
def format_activity_day(date)
date == Date.today ? l(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
end
def project_settings_tabs
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
@@ -33,4 +45,154 @@ module ProjectsHelper
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
# Generates a gantt image
# Only defined if RMagick is avalaible
def gantt_image(events, date_from, months, zoom)
date_to = (date_from >> months)-1
show_weeks = zoom > 1
show_days = zoom > 2
subject_width = 320
header_heigth = 18
# width of one day in pixels
zoom = zoom*2
g_width = (date_to - date_from + 1)*zoom
g_height = 20 * events.length + 20
headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
height = g_height + headers_heigth
imgl = Magick::ImageList.new
imgl.new_image(subject_width+g_width+1, height)
gc = Magick::Draw.new
# Subjects
top = headers_heigth + 20
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
events.each do |i|
gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name))
top = top + 20
end
# Months headers
month_f = date_from
left = subject_width
months.times do
width = ((month_f >> 1) - month_f) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, 0, left + width, height)
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
left = left + width
month_f = month_f >> 1
end
# Weeks headers
if show_weeks
left = subject_width
height = header_heigth
if date_from.cwday == 1
# date_from is monday
week_f = date_from
else
# find next monday after date_from
week_f = date_from + (7 - date_from.cwday + 1)
width = (7 - date_from.cwday + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
left = left + width
end
while week_f <= date_to
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
gc.fill('white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
left = left + width
week_f = week_f+7
end
end
# Days details (week-end in grey)
if show_days
left = subject_width
height = g_height + header_heigth - 1
wday = date_from.cwday
(date_to - date_from + 1).to_i.times do
width = zoom
gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
# border
gc.fill('transparent')
gc.stroke('grey')
gc.stroke_width(1)
gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
gc.stroke('black')
gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
# content
top = headers_heigth + 20
gc.stroke('transparent')
events.each do |i|
if i.is_a?(Issue)
i_start_date = (i.start_date >= date_from ? i.start_date : date_from )
i_end_date = (i.due_date <= date_to ? i.due_date : date_to )
i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= date_from ? date_from : i_done_date )
i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = subject_width + ((i_start_date - date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
d_width = ((i_done_date - i_start_date)*zoom).floor # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
gc.fill('grey')
gc.rectangle(i_left, top, i_left + i_width, top - 6)
gc.fill('red')
gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
gc.fill('blue')
gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
gc.fill('black')
gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
else
i_left = subject_width + ((i.start_date - date_from)*zoom).floor
gc.fill('green')
gc.rectangle(i_left, top, i_left + 6, top - 6)
gc.fill('black')
gc.text(i_left + 11, top + 1, i.name)
end
top = top + 20
end
# today red line
if Date.today >= date_from and Date.today <= date_to
gc.stroke('red')
x = (Date.today-date_from+1)*zoom + subject_width
gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
end
gc.draw(imgl)
imgl
end if Object.const_defined?(:Magick)
end

View File

@@ -22,8 +22,8 @@ module QueriesHelper
end
def column_header(column)
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
:default_order => column.default_order) :
column.sortable ? sort_header_tag(column.sortable, :caption => column.caption,
:default_order => column.default_order) :
content_tag('th', column.caption)
end
@@ -40,18 +40,10 @@ module QueriesHelper
else
case column.name
when :subject
h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
when :project
link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
when :assigned_to
link_to(h(value), :controller => 'account', :action => 'show', :id => value)
when :author
link_to(h(value), :controller => 'account', :action => 'show', :id => value)
when :done_ratio
progress_bar(value, :width => '80px')
when :fixed_version
link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
else
h(value)
end

View File

@@ -22,12 +22,6 @@ module RepositoriesHelper
txt.to_s[0,8]
end
def truncate_at_line_break(text, length = 255)
if text
text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
end
end
def render_properties(properties)
unless properties.nil? || properties.empty?
content = ''
@@ -38,74 +32,6 @@ module RepositoriesHelper
end
end
def render_changeset_changes
changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
case change.action
when 'A'
# Detects moved/copied files
if !change.from_path.blank?
change.action = @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
end
change
when 'D'
@changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
else
change
end
end.compact
tree = { }
changes.each do |change|
p = tree
dirs = change.path.to_s.split('/').select {|d| !d.blank?}
dirs.each do |dir|
p[:s] ||= {}
p = p[:s]
p[dir] ||= {}
p = p[dir]
end
p[:c] = change
end
render_changes_tree(tree[:s])
end
def render_changes_tree(tree)
return '' if tree.nil?
output = ''
output << '<ul>'
tree.keys.sort.each do |file|
s = !tree[file][:s].nil?
c = tree[file][:c]
style = 'change'
style << ' folder' if s
style << " change-#{c.action}" if c
text = h(file)
unless c.nil?
path_param = to_path_param(@repository.relative_path(c.path))
text = link_to(text, :controller => 'repositories',
:action => 'entry',
:id => @project,
:path => path_param,
:rev => @changeset.revision) unless s || c.action == 'D'
text << " - #{c.revision}" unless c.revision.blank?
text << ' (' + link_to('diff', :controller => 'repositories',
:action => 'diff',
:id => @project,
:path => path_param,
:rev => @changeset.revision) + ') ' if c.action == 'M'
text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
end
output << "<li class='#{style}'>#{text}</li>"
output << render_changes_tree(tree[file][:s]) if s
end
output << '</ul>'
output
end
def to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@@ -147,7 +73,7 @@ module RepositoriesHelper
def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
'<br />(http://, https://, svn://, file:///)') +
content_tag('p', form.text_field(:login, :size => 30)) +
content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),

View File

@@ -19,7 +19,6 @@ module SettingsHelper
def administration_settings_tabs
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},

View File

@@ -67,31 +67,23 @@ module SortHelper
# Updates the sort state. Call this in the controller prior to calling
# sort_clause.
# sort_keys can be either an array or a hash of allowed keys
def sort_update(sort_keys)
sort_key = params[:sort_key]
sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key])
sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC')
if sort_key
sort = {:key => sort_key, :order => sort_order}
#
def sort_update()
if params[:sort_key]
sort = {:key => params[:sort_key], :order => params[:sort_order]}
elsif session[@sort_name]
sort = session[@sort_name] # Previous sort.
else
sort = @sort_default
end
session[@sort_name] = sort
sort_column = (sort_keys.is_a?(Hash) ? sort_keys[sort[:key]] : sort[:key])
@sort_clause = (sort_column.blank? ? nil : "#{sort_column} #{sort[:order]}")
end
# Returns an SQL sort clause corresponding to the current sort state.
# Use this to sort the controller's table items collection.
#
def sort_clause()
@sort_clause
session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
end
# Returns a link which sorts by the named column.

View File

@@ -16,16 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module TimelogHelper
include ApplicationHelper
def render_timelog_breadcrumb
links = []
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
links << link_to_issue(@issue) if @issue
breadcrumb links
end
def activity_collection_for_select_options
activities = Enumeration::get_values('ACTI')
collection = []
@@ -35,11 +25,7 @@ module TimelogHelper
end
def select_hours(data, criteria, value)
if value.to_s.empty?
data.select {|row| row[criteria].blank? }
else
data.select {|row| row[criteria] == value}
end
data.select {|row| row[criteria] == value}
end
def sum_hours(data)
@@ -87,7 +73,7 @@ module TimelogHelper
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
entries.each do |entry|
fields = [format_date(entry.spent_on),
fields = [l_date(entry.spent_on),
entry.user,
entry.activity,
entry.project,

View File

@@ -16,12 +16,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module UsersHelper
def users_status_options_for_select(selected)
user_count_by_status = User.count(:group => 'status').to_hash
def status_options_for_select(selected)
options_for_select([[l(:label_all), ''],
["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1],
["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2],
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected)
[l(:status_active), 1],
[l(:status_registered), 2],
[l(:status_locked), 3]], selected)
end
# Options for the new membership projects combo-box

View File

@@ -24,7 +24,7 @@ module WatchersHelper
return '' unless user && user.logged? && object.respond_to?('watched_by?')
watched = object.watched_by?(user)
url = {:controller => 'watchers',
:action => (watched ? 'unwatch' : 'watch'),
:action => (watched ? 'remove' : 'add'),
:object_type => object.class.to_s.underscore,
:object_id => object.id}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
@@ -33,9 +33,4 @@ module WatchersHelper
:class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
end
# Returns a comma separated list of users watching the given object
def watchers_list(object)
object.watcher_users.collect {|u| content_tag('span', link_to_user(u), :class => 'user') }.join(",\n")
end
end

View File

@@ -16,6 +16,22 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module WikiHelper
def render_page_hierarchy(pages, node=nil)
content = ''
if pages[node]
content << "<ul class=\"pages-hierarchy\">\n"
pages[node].each do |page|
content << "<li>"
content << link_to(h(page.pretty_title), {:action => 'index', :page => page.title},
:title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
content << "</li>\n"
end
content << "</ul>\n"
end
content
end
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}
@@ -36,7 +52,7 @@ module WikiHelper
words_add += 1
else
del_at = pos unless del_at
deleted << ' ' + h(change[2])
deleted << ' ' + change[2]
words_del += 1
end
end

View File

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

View File

@@ -30,14 +30,12 @@ class Attachment < ActiveRecord::Base
acts_as_activity_provider :type => 'files',
:permission => :view_files,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity_provider :type => 'documents',
:permission => :view_documents,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
@@ -65,20 +63,14 @@ class Attachment < ActiveRecord::Base
nil
end
# Copies the temporary file to its final location
# and computes its MD5 hash
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
md5 = Digest::MD5.new
File.open(diskfile, "wb") do |f|
buffer = ""
while (buffer = @temp_file.read(8192))
f.write(buffer)
md5.update(buffer)
end
f.write(@temp_file.read)
end
self.digest = md5.hexdigest
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
end
# Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255
@@ -104,14 +96,6 @@ class Attachment < ActiveRecord::Base
container.project
end
def visible?(user=User.current)
container.attachments_visible?(user)
end
def deletable?(user=User.current)
container.attachments_deletable?(user)
end
def image?
self.filename =~ /\.(jpe?g|gif|png)$/i
end

View File

@@ -38,8 +38,7 @@ class AuthSource < ActiveRecord::Base
begin
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
attrs = source.authenticate(login, password)
rescue => e
logger.error "Error during authentication: #{e.message}"
rescue
attrs = nil
end
return attrs if attrs

View File

@@ -25,8 +25,6 @@ class AuthSourceLdap < AuthSource
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
validates_numericality_of :port, :only_integer => true
before_validation :strip_ldap_attributes
def after_initialize
self.port = 389 if self.port == 0
end
@@ -73,14 +71,7 @@ class AuthSourceLdap < AuthSource
"LDAP"
end
private
def strip_ldap_attributes
[:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
end
end
private
def initialize_ldap_con(ldap_user, ldap_password)
options = { :host => self.host,
:port => self.port,
@@ -91,8 +82,6 @@ class AuthSourceLdap < AuthSource
end
def self.get_attr(entry, attr_name)
if !attr_name.blank?
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
end
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
end
end

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -15,17 +15,15 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'iconv'
class Changeset < ActiveRecord::Base
belongs_to :repository
belongs_to :user
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
:description => :comments,
:datetime => :committed_on,
:author => :committer,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
@@ -34,7 +32,6 @@ class Changeset < ActiveRecord::Base
:date_column => 'committed_on'
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
@@ -46,7 +43,7 @@ class Changeset < ActiveRecord::Base
end
def comments=(comment)
write_attribute(:comments, Changeset.normalize_comments(comment))
write_attribute(:comments, comment.strip)
end
def committed_on=(date)
@@ -58,14 +55,6 @@ class Changeset < ActiveRecord::Base
repository.project
end
def author
user || committer.to_s.split('<').first
end
def before_create
self.user = repository.find_committer_user(committer)
end
def after_create
scan_comment_for_issue_ids
end
@@ -105,15 +94,14 @@ class Changeset < ActiveRecord::Base
issue.reload
# don't change the status is the issue is closed
next if issue.status.is_closed?
user = committer_user || User.anonymous
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
issue.status = fix_status
issue.done_ratio = done_ratio if done_ratio
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
{ :changeset => self, :issue => issue })
issue.save
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
@@ -123,6 +111,16 @@ class Changeset < ActiveRecord::Base
self.issues = referenced_issues.uniq
end
# Returns the Redmine User corresponding to the committer
def committer_user
if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
end
end
# Returns the previous changeset
def previous
@@ -133,25 +131,4 @@ class Changeset < ActiveRecord::Base
def next
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
end
# Strips and reencodes a commit log before insertion into the database
def self.normalize_comments(str)
to_utf8(str.to_s.strip)
end
private
def self.to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
encoding = Setting.commit_logs_encoding.to_s.strip
unless encoding.blank? || encoding == 'UTF-8'
begin
return Iconv.conv('UTF-8', encoding, str)
rescue Iconv::Failure
# do nothing here
end
end
str
end
end

View File

@@ -30,9 +30,9 @@ class CustomField < ActiveRecord::Base
}.freeze
validates_presence_of :name, :field_format
validates_uniqueness_of :name, :scope => :type
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
def initialize(attributes = nil)
@@ -66,7 +66,7 @@ class CustomField < ActiveRecord::Base
# to move in project_custom_field
def self.for_all
find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
find(:all, :conditions => ["is_for_all=?", true])
end
def type_name

View File

@@ -18,7 +18,7 @@
class Document < ActiveRecord::Base
belongs_to :project
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
acts_as_attachable :delete_permission => :manage_documents
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
@@ -28,10 +28,4 @@ class Document < ActiveRecord::Base
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
def after_initialize
if new_record?
self.category ||= Enumeration.default('DCAT')
end
end
end

View File

@@ -44,9 +44,7 @@ class Enumeration < ActiveRecord::Base
end
def before_save
if is_default? && is_default_changed?
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
end
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
end
def objects_count

View File

@@ -26,13 +26,13 @@ class Issue < ActiveRecord::Base
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable
acts_as_watchable
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
@@ -42,10 +42,9 @@ class Issue < ActiveRecord::Base
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true
@@ -185,8 +184,6 @@ class Issue < ActiveRecord::Base
@issue_before_change.status = self.status
@custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
# Make sure updated_on is updated when adding a note.
updated_on_will_change!
@current_journal
end
@@ -195,11 +192,6 @@ class Issue < ActiveRecord::Base
self.status.is_closed?
end
# Returns true if the issue is overdue
def overdue?
!due_date.nil? && (due_date < Date.today)
end
# Users the issue can be assigned to
def assignable_users
project.assignable_users
@@ -266,15 +258,4 @@ class Issue < ActiveRecord::Base
def to_s
"#{tracker} ##{id}: #{subject}"
end
private
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => obj.id,
:old_value => obj.filename)
journal.save
end
end

View File

@@ -25,8 +25,8 @@ class IssueStatus < ActiveRecord::Base
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
def after_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
def before_save
IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default?
end
# Returns the default status for new issues

View File

@@ -33,12 +33,11 @@ class Journal < ActiveRecord::Base
acts_as_activity_provider :type => 'issues',
:permission => :view_issues,
:author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
def save(*args)
def save
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end

View File

@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper
class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end
@@ -32,15 +31,13 @@ class MailHandler < ActionMailer::Base
@@handler_options[:allow_override] ||= []
# Project needs to be overridable if not specified
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
# Status overridable by default
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
super email
end
# Processes incoming emails
def receive(email)
@email = email
@user = User.active.find(:first, :conditions => ["LOWER(mail) = ?", email.from.to_a.first.to_s.strip.downcase])
@user = User.find_active(:first, :conditions => {:mail => email.from.first})
unless @user
# Unknown user => the email is ignored
# TODO: ability to create the user's account
@@ -79,30 +76,15 @@ class MailHandler < ActionMailer::Base
tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
# check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.subject = email.subject.chomp.toutf8
issue.description = plain_text_body
# custom fields
issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
if value = get_keyword(c.name, :override => true)
h[c.id] = value
end
h
end
issue.subject = email.subject.chomp
issue.description = email.plain_text_body.chomp
issue.save!
add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
# add To and Cc as watchers
add_watchers(issue)
# send notification after adding watchers so that they can reply to Redmine
Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
issue
end
@@ -118,21 +100,13 @@ class MailHandler < ActionMailer::Base
# Adds a note to an existing issue
def receive_issue_update(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
issue = Issue.find_by_id(issue_id)
return unless issue
# check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
# add the note
journal = issue.init_journal(user, plain_text_body)
journal = issue.init_journal(user, email.plain_text_body.chomp)
add_attachments(issue)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
@@ -150,51 +124,22 @@ class MailHandler < ActionMailer::Base
end
end
# Adds To and Cc as watchers of the given object if the sender has the
# appropriate permission
def add_watchers(obj)
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
unless addresses.empty?
watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
watchers.each {|w| obj.add_watcher(w)}
end
def get_keyword(attr)
if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
end
end
def get_keyword(attr, options={})
@keywords ||= {}
if @keywords.has_key?(attr)
@keywords[attr]
else
@keywords[attr] = begin
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
end
end
end
end
# Returns the text/plain part of the email
# If not found (eg. HTML-only email), returns the body with tags removed
def plain_text_body
return @plain_text_body unless @plain_text_body.nil?
parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
if parts.empty?
parts << @email
end
plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
if plain_text_part.nil?
# no text/plain part found, assuming html-only email
# strip html tags and remove doctype directive
@plain_text_body = strip_tags(@email.body.to_s)
@plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
else
@plain_text_body = plain_text_part.body.to_s
end
@plain_text_body.strip!
@plain_text_body
end
end
class TMail::Mail
# Returns body of the first plain text part found if any
def plain_text_body
return @plain_text_body unless @plain_text_body.nil?
p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
plain = p.detect {|c| c.content_type == 'text/plain'}
@plain_text_body = plain.nil? ? self.body : plain.body
end
end

View File

@@ -5,12 +5,12 @@
# 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.
@@ -19,22 +19,15 @@ class Mailer < ActionMailer::Base
helper :application
helper :issues
helper :custom_fields
include ActionController::UrlWriter
def self.default_url_options
h = Setting.host_name
h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
{ :host => h, :protocol => Setting.protocol }
end
def issue_add(issue)
include ActionController::UrlWriter
def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
cc(issue.watcher_recipients - @recipients)
recipients issue.recipients
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
@@ -46,7 +39,6 @@ class Mailer < ActionMailer::Base
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
@author = journal.user
recipients issue.recipients
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
@@ -58,16 +50,16 @@ class Mailer < ActionMailer::Base
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def reminder(user, issues, days)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_reminder, issues.size)
body :issues => issues,
:days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc')
end
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.project.recipients
@@ -75,15 +67,12 @@ class Mailer < ActionMailer::Base
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
end
def attachments_added(attachments)
container = attachments.first.container
added_to = ''
added_to_url = ''
case container.class.name
when 'Project'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
added_to = "#{l(:label_project)}: #{container}"
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
@@ -115,7 +104,7 @@ class Mailer < ActionMailer::Base
body :message => message,
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
end
def account_information(user, password)
set_language_if_valid user.language
recipients user.mail
@@ -124,31 +113,22 @@ class Mailer < ActionMailer::Base
:password => password,
:login_url => url_for(:controller => 'account', :action => 'login')
end
def account_activation_request(user)
# Send the email to all active administrators
recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
end
# A registered user's account was activated by an administrator
def account_activated(user)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register, Setting.app_title)
body :user => user,
:login_url => url_for(:controller => 'account', :action => 'login')
end
def lost_password(token)
set_language_if_valid(token.user.language)
recipients token.user.mail
subject l(:mail_subject_lost_password, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
end
end
def register(token)
set_language_if_valid(token.user.language)
@@ -157,7 +137,7 @@ class Mailer < ActionMailer::Base
body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
end
def test(user)
set_language_if_valid(user.language)
recipients user.mail
@@ -168,12 +148,12 @@ class Mailer < ActionMailer::Base
# Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc
def deliver!(mail = @mail)
return false if (recipients.nil? || recipients.empty?) &&
return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?)
super
end
# Sends reminders to issue assignees
# Available options:
# * :days => how many days in the future to remind about (defaults to 7)
@@ -183,13 +163,13 @@ class Mailer < ActionMailer::Base
days = options[:days] || 7
project = options[:project] ? Project.find(options[:project]) : nil
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
s << "#{Issue.table_name}.project_id = #{project.id}" if project
s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
:conditions => s.conditions
).group_by(&:assigned_to)
@@ -203,60 +183,43 @@ class Mailer < ActionMailer::Base
super
set_language_if_valid Setting.default_language
from Setting.mail_from
default_url_options[:host] = Setting.host_name
default_url_options[:protocol] = Setting.protocol
# Common headers
headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name,
'X-Redmine-Site' => Setting.app_title,
'Precedence' => 'bulk',
'Auto-Submitted' => 'auto-generated'
'X-Redmine-Site' => Setting.app_title
end
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
def redmine_headers(h)
h.each { |k,v| headers["X-Redmine-#{k}"] = v }
end
# Overrides the create_mail method
def create_mail
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
@author ||= User.current
if @author.pref[:no_self_notified]
recipients.delete(@author.mail) if recipients
cc.delete(@author.mail) if cc
if User.current.pref[:no_self_notified]
recipients.delete(User.current.mail) if recipients
cc.delete(User.current.mail) if cc
end
# Blind carbon copy recipients
if Setting.bcc_recipients?
bcc([recipients, cc].flatten.compact.uniq)
recipients []
cc []
end
end
super
end
# Renders a message with the corresponding layout
def render_message(method_name, body)
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
body[:content_for_layout] = render(:file => method_name, :body => body)
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}")
end
# for the case of plain text only
def body(*params)
value = super(*params)
if Setting.plain_text_mail?
templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
unless String === @body or templates.empty?
template = File.basename(templates.first)
@body[:content_for_layout] = render(:file => template, :body => @body)
@body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
return @body
end
end
return value
end
# Makes partial rendering work with Rails 1.2 (retro-compatibility)
def self.controller_path
''

View File

@@ -19,11 +19,11 @@ class Message < ActiveRecord::Base
belongs_to :board
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
acts_as_attachable
has_many :attachments, :as => :container, :dependent => :destroy
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'],
:include => {:board => :project},
:include => {:board, :project},
:project_key => 'project_id',
:date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
@@ -32,16 +32,12 @@ class Message < ActiveRecord::Base
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
{:id => o.parent_id, :anchor => "message-#{o.id}"})}
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
:author_key => :author_id
acts_as_watchable
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}
attr_protected :locked, :sticky
validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255
after_create :add_author_as_watcher
def validate_on_create
# Can not reply to a locked topic
errors.add_to_base 'Topic is locked' if root.locked? && self != root
@@ -72,18 +68,4 @@ class Message < ActiveRecord::Base
def project
board.project
end
def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
end
def destroyable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
end
private
def add_author_as_watcher
Watcher.create(:watchable => self.root, :user => author)
end
end

View File

@@ -17,9 +17,8 @@
class MessageObserver < ActiveRecord::Observer
def after_create(message)
recipients = []
# send notification to the topic watchers
recipients += message.root.watcher_recipients
# send notification to the authors of the thread
recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?}
# send notification to the board watchers
recipients += message.board.watcher_recipients
# send notification to project members who want to be notified

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006 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
@@ -24,13 +24,12 @@ class News < ActiveRecord::Base
validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:author_key => :author_id
acts_as_activity_provider :find_options => {:include => [:project, :author]}
# returns latest news for projects visible by user
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")
def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
end
end

View File

@@ -44,8 +44,6 @@ class Project < ActiveRecord::Base
:association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
acts_as_customizable
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
@@ -60,12 +58,10 @@ class Project < ActiveRecord::Base
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 1..20
validates_length_of :identifier, :in => 3..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
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] } }
def identifier=(identifier)
super unless identifier_frozen?
@@ -110,12 +106,6 @@ class Project < ActiveRecord::Base
def self.allowed_to_condition(user, permission, options={})
statements = []
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if perm = Redmine::AccessControl.permission(permission)
unless perm.project_module.nil?
# If the permission belongs to a project module, make sure the module is enabled
base_statement << " AND EXISTS (SELECT em.id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}' AND em.project_id=#{Project.table_name}.id)"
end
end
if options[:project]
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
@@ -208,7 +198,7 @@ class Project < ActiveRecord::Base
# Returns an array of all custom fields enabled for project issues
# (explictly associated custom fields and custom fields enabled for all projects)
def all_issue_custom_fields
@all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
@all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq
end
def project
@@ -248,12 +238,6 @@ class Project < ActiveRecord::Base
enabled_modules << EnabledModule.new(:name => name.to_s)
end
end
# Returns an auto-generated project identifier based on the last identifier used
def self.next_identifier
p = Project.find(:first, :order => 'created_on DESC')
p.nil? ? nil : p.identifier.to_s.succ
end
protected
def validate

View File

@@ -1,5 +1,5 @@
# Redmine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -93,7 +93,6 @@ class Query < ActiveRecord::Base
cattr_reader :operators_by_filter_type
@@available_columns = [
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"),
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
@@ -235,10 +234,7 @@ class Query < ActiveRecord::Base
def columns
if has_default_columns?
available_columns.select do |c|
# Adds the project column by default for cross-project lists
Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
end
available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
else
# preserve the column_names order
column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
@@ -258,8 +254,9 @@ class Query < ActiveRecord::Base
def has_default_columns?
column_names.nil? || column_names.empty?
end
def project_statement
def statement
# project/subprojects clause
project_clauses = []
if project && !@project.active_children.empty?
ids = [project.id]
@@ -277,15 +274,12 @@ class Query < ActiveRecord::Base
elsif Setting.display_subprojects_issues?
ids += project.child_ids
end
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
elsif project
project_clauses << "#{Project.table_name}.id = %d" % project.id
project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
end
project_clauses << Project.allowed_to_condition(User.current, :view_issues)
project_clauses.join(' AND ')
end
def statement
project_clauses << Project.visible_by(User.current)
# filters clauses
filters_clauses = []
filters.each_key do |field|
@@ -313,69 +307,60 @@ class Query < ActiveRecord::Base
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
end
sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
case operator_for field
when "="
sql = sql + "#{db_table}.#{db_field} IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = sql + "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
when "<="
sql = sql + "#{db_table}.#{db_field} <= #{v.first.to_i}"
when "o"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = sql + "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today + 1).to_time)]
when "<t-"
sql = sql + "#{db_table}.#{db_field} <= '%s'" % connection.quoted_date((Date.today - v.first.to_i).to_time)
when "t-"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today - v.first.to_i).to_time), connection.quoted_date((Date.today - v.first.to_i + 1).to_time)]
when ">t+"
sql = sql + "#{db_table}.#{db_field} >= '%s'" % connection.quoted_date((Date.today + v.first.to_i).to_time)
when "<t+"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t+"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
when "!~"
sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
end
sql << ')'
filters_clauses << sql
end if filters and valid?
(filters_clauses << project_statement).join(' AND ')
(project_clauses + filters_clauses).join(' AND ')
end
private
# Helper method to generate the WHERE sql for a +field+ with a +value+
def sql_for_field(field, value, db_table, db_field, is_custom_filter)
sql = ''
case operator_for field
when "="
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
when "!"
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
sql = "#{db_table}.#{db_field} IS NULL"
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
sql = "#{db_table}.#{db_field} IS NOT NULL"
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
when "<="
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
when "o"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
when "c"
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
when ">t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
when "<t-"
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
when "t-"
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
when ">t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
when "<t+"
sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
when "t+"
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
when "t"
sql = date_range_clause(db_table, db_field, 0, 0)
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
when "!~"
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
end
return sql
end
def add_custom_fields_filters(custom_fields)
@available_filters ||= {}
@@ -395,16 +380,4 @@ class Query < ActiveRecord::Base
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
end
end
# Returns a SQL clause for a date or datetime field.
def date_range_clause(table, field, from, to)
s = []
if from
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
end
if to
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
end
s.join(' AND ')
end
end

View File

@@ -78,7 +78,7 @@ class Repository < ActiveRecord::Base
# Default behaviour: we search in cached changesets
def changesets_for_path(path)
path = "/#{path}" unless path.starts_with?('/')
Change.find(:all, :include => {:changeset => :user},
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
end
@@ -96,45 +96,6 @@ class Repository < ActiveRecord::Base
self.changesets.each(&:scan_comment_for_issue_ids)
end
# Returns an array of committers usernames and associated user_id
def committers
@committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
end
# Maps committers username to a user ids
def committer_ids=(h)
if h.is_a?(Hash)
committers.each do |committer, user_id|
new_user_id = h[committer]
if new_user_id && (new_user_id.to_i != user_id.to_i)
new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
end
end
@committers = nil
true
else
false
end
end
# Returns the Redmine User corresponding to the given +committer+
# It will return nil if the committer is not yet mapped and if no User
# with the same username or email was found
def find_committer_user(committer)
if committer
c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
if c && c.user
c.user
elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
end
end
end
# fetch new changesets for all repositories
# can be called periodically by an external script
# eg. ruby script/runner "Repository.fetch_changesets"
@@ -173,7 +134,6 @@ class Repository < ActiveRecord::Base
def clear_changesets
connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
connection.delete("DELETE FROM changesets_issues WHERE changesets_issues.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}")
end
end

View File

@@ -109,7 +109,7 @@ class Repository::Cvs < Repository
cs = changesets.find(:first, :conditions=>{
:committed_on=>revision.time-time_delta..revision.time+time_delta,
:committer=>revision.author,
:comments=>Changeset.normalize_comments(revision.message)
:comments=>revision.message
})
# create a new changeset....

View File

@@ -28,11 +28,6 @@ class Repository::Darcs < Repository
'Darcs'
end
def entry(path=nil, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
scm.entry(path, patch.nil? ? nil : patch.scmid)
end
def entries(path=nil, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
@@ -51,11 +46,6 @@ class Repository::Darcs < Repository
entries
end
def cat(path, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
scm.cat(path, patch.nil? ? nil : patch.scmid)
end
def diff(path, rev, rev_to)
patch_from = changesets.find_by_revision(rev)
return nil if patch_from.nil?

View File

@@ -30,7 +30,7 @@ class Repository::Git < Repository
end
def changesets_for_path(path)
Change.find(:all, :include => {:changeset => :user},
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
end
@@ -45,22 +45,20 @@ class Repository::Git < Repository
unless changesets.find_by_scmid(scm_revision)
scm.revisions('', db_revision, nil, :reverse => true) do |revision|
if changesets.find_by_scmid(revision.scmid.to_s).nil?
transaction do
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|
Change.create!(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
transaction do
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|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end
end

View File

@@ -20,7 +20,7 @@ require 'redmine/scm/adapters/subversion_adapter'
class Repository::Subversion < Repository
attr_protected :root_url
validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
def scm_adapter
Redmine::Scm::Adapters::SubversionAdapter
@@ -32,7 +32,7 @@ class Repository::Subversion < Repository
def changesets_for_path(path)
revisions = scm.revisions(path)
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") : []
end
# Returns a path relative to the url of the repository
@@ -54,8 +54,8 @@ class Repository::Subversion < Repository
# loads changesets by batches of 200
identifier_to = [identifier_from + 199, scm_revision].min
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
revisions.reverse_each do |revision|
transaction do
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:committer => revision.author,
@@ -68,7 +68,7 @@ class Repository::Subversion < Repository
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end unless changeset.new_record?
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1

View File

@@ -19,11 +19,6 @@ class Role < ActiveRecord::Base
# Built-in roles
BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2
named_scope :builtin, lambda { |*args|
compare = 'not' if args.first == true
{ :conditions => "#{compare} builtin = 0" }
}
before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all do
@@ -31,9 +26,9 @@ class Role < ActiveRecord::Base
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role)
raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
clear
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
" FROM #{Workflow.table_name}" +
" FROM workflows" +
" WHERE role_id = #{role.id}"
end
end
@@ -41,7 +36,7 @@ class Role < ActiveRecord::Base
has_many :members
acts_as_list
serialize :permissions, Array
serialize :permissions
attr_protected :builtin
validates_presence_of :name
@@ -54,32 +49,9 @@ class Role < ActiveRecord::Base
end
def permissions=(perms)
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact if perms
write_attribute(:permissions, perms)
end
def add_permission!(*perms)
self.permissions = [] unless permissions.is_a?(Array)
permissions_will_change!
perms.each do |p|
p = p.to_sym
permissions << p unless permissions.include?(p)
end
save!
end
def remove_permission!(*perms)
return unless permissions.is_a?(Array)
permissions_will_change!
perms.each { |p| permissions.delete(p.to_sym) }
save!
end
# Returns true if the role has the given permission
def has_permission?(perm)
!permissions.nil? && permissions.include?(perm.to_sym)
end
def <=>(role)
position <=> role.position

View File

@@ -33,51 +33,12 @@ class Setting < ActiveRecord::Base
'%H:%M',
'%I:%M %p'
]
ENCODINGS = %w(US-ASCII
windows-1250
windows-1251
windows-1252
windows-1253
windows-1254
windows-1255
windows-1256
windows-1257
windows-1258
windows-31j
ISO-2022-JP
ISO-2022-KR
ISO-8859-1
ISO-8859-2
ISO-8859-3
ISO-8859-4
ISO-8859-5
ISO-8859-6
ISO-8859-7
ISO-8859-8
ISO-8859-9
ISO-8859-13
ISO-8859-15
KOI8-R
UTF-8
UTF-16
UTF-16BE
UTF-16LE
EUC-JP
Shift_JIS
GB18030
GBK
ISCII91
EUC-KR
Big5
Big5-HKSCS
TIS-620)
cattr_accessor :available_settings
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
Redmine::Plugin.all.each do |plugin|
Redmine::Plugin.registered_plugins.each do |id, plugin|
next unless plugin.settings
@@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
@@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
end
validates_uniqueness_of :name

View File

@@ -32,7 +32,7 @@ class TimeEntry < ActiveRecord::Base
:description => :comments
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid
validates_numericality_of :hours, :allow_nil => true
validates_length_of :comments, :maximum => 255, :allow_nil => true
def after_initialize
@@ -54,7 +54,7 @@ class TimeEntry < ActiveRecord::Base
end
def hours=(h)
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
write_attribute :hours, (h.is_a?(String) ? h.to_hours : h)
end
# tyear, tmonth, tweek assigned where setting spent_on attributes

View File

@@ -15,11 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'rails_generator/secret_key_generator'
class Token < ActiveRecord::Base
belongs_to :user
validates_uniqueness_of :value
@@validity_time = 1.day
@@ -39,7 +36,9 @@ class Token < ActiveRecord::Base
private
def self.generate_token_value
s = Rails::SecretKeyGenerator.new(object_id).generate_secret
s[0, 40]
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
token_value = ''
40.times { |i| token_value << chars[rand(chars.size-1)] }
token_value
end
end

View File

@@ -23,9 +23,9 @@ class Tracker < ActiveRecord::Base
raise "Can not copy workflow from a #{tracker.class}" unless tracker.is_a?(Tracker)
raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
clear
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, old_status_id, new_status_id, role_id)" +
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
" FROM #{Workflow.table_name}" +
" FROM workflows" +
" WHERE tracker_id = #{tracker.id}"
end
end

View File

@@ -33,18 +33,13 @@ class User < ActiveRecord::Base
:username => '#{login}'
}
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
has_many :members, :dependent => :delete_all
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source
# Active non-anonymous users scope
named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
acts_as_customizable
attr_accessor :password, :password_confirmation
@@ -74,10 +69,17 @@ class User < ActiveRecord::Base
# update hashed_password if password was set
self.hashed_password = User.hash_password(self.password) if self.password
end
def self.active
with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
yield
end
end
def reload(*args)
@name = nil
super
def self.find_active(*args)
active do
find(*args)
end
end
# Returns the user that matches provided login and password, or nil
@@ -116,11 +118,8 @@ class User < ActiveRecord::Base
# Return user's full name for display
def name(formatter = nil)
if formatter
eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
else
@name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
end
f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
eval '"' + f + '"'
end
def active?
@@ -144,7 +143,7 @@ class User < ActiveRecord::Base
end
def time_zone
@time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
end
def wants_comments_in_reverse_order?
@@ -175,24 +174,18 @@ class User < ActiveRecord::Base
end
def self.find_by_autologin_key(key)
tokens = Token.find_all_by_action_and_value('autologin', key)
# Make sure there's only 1 token that matches the key
if tokens.size == 1
token = tokens.first
if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
token.user
end
end
end
# Makes find_by_mail case-insensitive
def self.find_by_mail(mail)
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
token = Token.find_by_action_and_value('autologin', key)
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
end
# Sort users by their display names
def <=>(user)
self.to_s.downcase <=> user.to_s.downcase
if user.nil?
-1
elsif lastname.to_s.downcase == user.lastname.to_s.downcase
firstname.to_s.downcase <=> user.firstname.to_s.downcase
else
lastname.to_s.downcase <=> user.lastname.to_s.downcase
end
end
def to_s
@@ -249,7 +242,7 @@ class User < ActiveRecord::Base
elsif options[:global]
# authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.role}.uniq
roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
roles.detect {|r| r.allowed_to?(action)}
else
false
end

View File

@@ -19,13 +19,12 @@ class Version < ActiveRecord::Base
before_destroy :check_integrity
belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
has_many :attachments, :as => :container, :dependent => :destroy
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true
def start_date
effective_date

View File

@@ -19,12 +19,5 @@ class Watcher < ActiveRecord::Base
belongs_to :watchable, :polymorphic => true
belongs_to :user
validates_presence_of :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
protected
def validate
errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active?
end
end

View File

@@ -43,25 +43,6 @@ class Wiki < ActiveRecord::Base
page
end
# Finds a page by title
# The given string can be of one of the forms: "title" or "project:title"
# Examples:
# Wiki.find_page("bar", project => foo)
# Wiki.find_page("foo:bar")
def self.find_page(title, options = {})
project = options[:project]
if title.to_s =~ %r{^([^\:]+)\:(.*)$}
project_identifier, title = $1, $2
project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
end
if project && project.wiki
page = project.wiki.find_page(title)
if page && page.content
page
end
end
end
# turn a string into a valid page title
def self.titleize(title)
# replace spaces with _ and remove unwanted caracters

View File

@@ -22,7 +22,6 @@ class WikiContent < ActiveRecord::Base
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
validates_presence_of :text
validates_length_of :comments, :maximum => 255, :allow_nil => true
acts_as_versioned
class Version
@@ -36,10 +35,9 @@ class WikiContent < ActiveRecord::Base
:type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
acts_as_activity_provider :type => 'wiki_edits',
acts_as_activity_provider :type => 'wiki_pages',
:timestamp => "#{WikiContent.versioned_table_name}.updated_on",
:author_key => "#{WikiContent.versioned_table_name}.author_id",
:permission => :view_wiki_edits,
:permission => :view_wiki_pages,
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +

View File

@@ -21,7 +21,7 @@ require 'enumerator'
class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_tree :order => 'title'
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
@@ -111,10 +111,6 @@ class WikiPage < ActiveRecord::Base
def editable_by?(usr)
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
end
def attachments_deletable?(usr=User.current)
editable_by?(usr) && super(usr)
end
def parent_title
@parent_title || (self.parent && self.parent.pretty_title)

View File

@@ -21,23 +21,4 @@ class Workflow < ActiveRecord::Base
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
validates_presence_of :role, :old_status, :new_status
# Returns workflow transitions count by tracker and role
def self.count_by_tracker_and_role
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id")
roles = Role.find(:all, :order => 'builtin, position')
trackers = Tracker.find(:all, :order => 'position')
result = []
trackers.each do |tracker|
t = []
roles.each do |role|
row = counts.detect {|c| c['role_id'] == role.id.to_s && c['tracker_id'] == tracker.id.to_s}
t << [role, (row.nil? ? 0 : row['c'].to_i)]
end
result << [tracker, t]
end
result
end
end

View File

@@ -1,24 +1,16 @@
<div class="contextual">
<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>
</div>
<h2><%=h @user.name %></h2>
<h2><%= avatar @user %> <%=h @user.name %></h2>
<div class="splitcontentleft">
<p>
<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>
<ul>
<% unless @user.pref.hide_mail %>
<li><%=l(:field_mail)%>: <%= mail_to(h(@user.mail), nil, :encode => 'javascript') %></li>
<% end %>
<% for custom_value in @custom_values %>
<% if !custom_value.value.blank? %>
<li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
<% end %>
<% end %>
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
<% unless @user.last_login_on.nil? %>
<li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li>
<% end %>
<% for custom_value in @custom_values %>
<% if !custom_value.value.empty? %>
<li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
<% end %>
<% end %>
</ul>
</p>
<% unless @memberships.empty? %>
<h3><%=l(:label_project_plural)%></h3>
@@ -29,43 +21,8 @@
<% end %>
</ul>
<% end %>
<%= call_hook :view_account_left_bottom, :user => @user %>
</div>
<div class="splitcontentright">
<% unless @events_by_day.empty? %>
<h3><%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :user_id => @user, :from => @events_by_day.keys.first %></h3>
<h3><%=l(:label_activity)%></h3>
<p>
<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
</p>
<div id="activity">
<% @events_by_day.keys.sort.reverse.each do |day| %>
<h4><%= format_activity_day(day) %></h4>
<dl>
<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
<dt class="<%= e.event_type %>">
<span class="time"><%= format_time(e.event_datetime, false) %></span>
<%= content_tag('span', h(e.project), :class => 'project') %>
<%= link_to format_activity_title(e.event_title), e.event_url %></dt>
<dd><span class="description"><%= format_activity_description(e.event_description) %></span></dd>
<% end -%>
</dl>
<% end -%>
</div>
<p class="other-formats">
<%= l(:label_export_to) %>
<%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key}, :class => 'feed' %>
</p>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %>
<% end %>
<% end %>
<%= call_hook :view_account_right_bottom, :user => @user %>
</div>
<% html_title @user.name %>
</p>

View File

@@ -19,7 +19,7 @@
<p class="icon22 icon22-tracker">
<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> |
<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> |
<%= link_to l(:label_workflow), :controller => 'workflows', :action => 'edit' %>
<%= link_to l(:label_workflow), :controller => 'roles', :action => 'workflow' %>
</p>
<p class="icon22 icon22-workflow">
@@ -34,16 +34,6 @@
<%= link_to l(:label_settings), :controller => 'settings' %>
</p>
<% menu_items_for(:admin_menu) do |item, caption, url, selected| -%>
<%= content_tag 'p',
link_to(h(caption), item.url, item.html_options),
:class => ["icon22", "icon22-#{item.name}"].join(' ') %>
<% end -%>
<p class="icon22 icon22-plugin">
<%= link_to l(:label_plugins), :controller => 'admin', :action => 'plugins' %>
</p>
<p class="icon22 icon22-info">
<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %>
</p>

View File

@@ -4,9 +4,24 @@
<table class="list">
<tr class="odd"><td><%= l(:text_default_administrator_account_changed) %></td><td><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %> (<%= Attachment.storage_path %>)</td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_plugin_assets_writable) %> (<%= Engines.public_directory %>)</td><td><%= image_tag (@flags[:plugin_assets_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="even"><td><%= l(:text_file_repository_writable) %></td><td><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
<tr class="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %></td></tr>
</table>
<% if @plugins.any? %>
&nbsp;
<h3 class="icon22 icon22-plugin"><%= l(:label_plugins) %></h3>
<table class="list">
<% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %>
<tr class="<%= cycle('odd', 'even') %>">
<td><%=h @plugins[plugin].name %></td>
<td><%=h @plugins[plugin].description %></td>
<td><%=h @plugins[plugin].author %></td>
<td><%=h @plugins[plugin].version %></td>
<td><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %></td>
</tr>
<% end %>
</table>
<% end %>
<% html_title(l(:label_information_plural)) -%>

View File

@@ -1,19 +0,0 @@
<h2><%= l(:label_plugins) %></h2>
<% if @plugins.any? %>
<table class="list plugins">
<% @plugins.each do |plugin| %>
<tr class="<%= cycle('odd', 'even') %>">
<td><span class="name"><%=h plugin.name %></span>
<%= content_tag('span', h(plugin.description), :class => 'description') unless plugin.description.blank? %>
<%= content_tag('span', link_to(h(plugin.url), plugin.url), :class => 'url') unless plugin.url.blank? %>
</td>
<td class="author"><%= plugin.author_url.blank? ? h(plugin.author) : link_to(h(plugin.author), plugin.author_url) %></td>
<td class="version"><%=h plugin.version %></td>
<td class="configure"><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.id) if plugin.configurable? %></td>
</tr>
<% end %>
</table>
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>

View File

@@ -4,13 +4,11 @@
<h2><%=l(:label_project_plural)%></h2>
<% form_tag({}, :method => :get) do %>
<% form_tag() do %>
<fieldset><legend><%= l(:label_filter_plural) %></legend>
<label><%= l(:field_status) %> :</label>
<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
<label><%= l(:label_project) %>:</label>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
<%= submit_tag l(:button_apply), :class => "small" %>
</fieldset>
<% end %>
&nbsp;

View File

@@ -3,14 +3,14 @@
<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
<% if options[:deletable] %>
<%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment},
<% if options[:delete_url] %>
<%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
:confirm => l(:text_are_you_sure),
:method => :post,
:class => 'delete',
:title => l(:button_delete) %>
<% end %>
<% if options[:author] %>
<% unless options[:no_author] %>
<span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
<% end %>
</p>

View File

@@ -9,7 +9,6 @@
<th><%=l(:field_name)%></th>
<th><%=l(:field_type)%></th>
<th><%=l(:field_host)%></th>
<th><%=l(:label_user_plural)%></th>
<th></th>
<th></th>
</tr></thead>
@@ -19,12 +18,8 @@
<td><%= link_to source.name, :action => 'edit', :id => source%></td>
<td align="center"><%= source.auth_method_name %></td>
<td align="center"><%= source.host %></td>
<td align="center"><%= source.users.count %></td>
<td align="center"><%= link_to l(:button_test), :action => 'test_connection', :id => source %></td>
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source },
:confirm => l(:text_are_you_sure),
:class => "button-small",
:disabled => source.users.any? %></td>
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, :confirm => l(:text_are_you_sure), :class => "button-small" %></td>
</tr>
<% end %>
</tbody>

View File

@@ -26,16 +26,15 @@
</div>
<h2><%=h @board.name %></h2>
<p class="subtitle"><%=h @board.description %></p>
<% if @topics.any? %>
<table class="list messages">
<thead><tr>
<th><%= l(:field_subject) %></th>
<th><%= l(:field_author) %></th>
<%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
<%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
<%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
<%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
<th><%= l(:label_reply_plural) %></th>
<%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
</tr></thead>
<tbody>
<% @topics.each do |topic| %>

View File

@@ -11,7 +11,7 @@ while day <= calendar.enddt %>
<p class="day-num"><%= day.day %></p>
<% calendar.events_on(day).each do |i| %>
<% if i.is_a? Issue %>
<div class="<%= css_issue_classes(i) %> tooltip">
<div class="tooltip">
<%= if day == i.start_date && day == i.due_date
image_tag('arrow_bw.png')
elsif day == i.start_date

View File

@@ -1,5 +1,4 @@
<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%>
<% diff.each do |table_file| -%>
<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%>
<div class="autoscroll">
<% if diff_type == 'sbs' -%>
<table class="filecontent CodeRay">
@@ -63,5 +62,3 @@
</div>
<% end -%>
<%= l(:text_diff_truncated) if diff.truncated? %>

View File

@@ -3,7 +3,7 @@
<tbody>
<% line_num = 1 %>
<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %>
<tr><th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th><td class="line-code"><pre><%= line %></pre></td></tr>
<tr><th class="line-num" id="L<%= line_num %>"><%= line_num %></th><td class="line-code"><pre><%= line %></pre></td></tr>
<% line_num += 1 %>
<% end %>
</tbody>

View File

@@ -1,12 +1,12 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.title truncate_single_line(@title, 100)
xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false))
xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false}))
xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false)
xml.id url_for(:controller => 'welcome', :only_path => false)
xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
xml.author { xml.name "#{Setting.app_title}" }
xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
xml.generator(:uri => Redmine::Info.url, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; }
@items.each do |item|
xml.entry do
url = url_for(item.event_url(:only_path => false))
@@ -21,10 +21,10 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
author = item.event_author if item.respond_to?(:event_author)
xml.author do
xml.name(author)
xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank?
end if author
xml.content "type" => "html" do
xml.text! textilizable(item, :event_description, :only_path => false)
xml.text! textilizable(item.event_description)
end
end
end

View File

@@ -82,7 +82,6 @@ function deleteValueField(e) {
<% end %>
</p>
<p><%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %></p>
<%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
</div>
<div class="box">
@@ -110,6 +109,5 @@ when "IssueCustomField" %>
<p><%= f.check_box :is_required %></p>
<% end %>
<%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
</div>
<%= javascript_tag "toggle_custom_field_format();" %>

View File

@@ -12,7 +12,7 @@
</div>
<h3><%= l(:label_attachment_plural) %></h3>
<%= link_to_attachments @document %>
<%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %>
<% if authorize_for('documents', 'add_attachment') %>
<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",

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