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
393 changed files with 8016 additions and 17680 deletions
+3 -11
View File
@@ -15,19 +15,11 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class 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 class SysApi < ActionWebService::API::Base
api_method :projects_with_repository_enabled, api_method :projects,
:expects => [], :expects => [],
:returns => [[AWSProjectWithRepository]] :returns => [[Project]]
api_method :repository_created, api_method :repository_created,
:expects => [:string, :string, :string], :expects => [:string, :string],
:returns => [:int] :returns => [:int]
end end
+5 -8
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AccountController < ApplicationController class AccountController < ApplicationController
layout 'base'
helper :custom_fields helper :custom_fields
include CustomFieldsHelper include CustomFieldsHelper
@@ -24,17 +25,13 @@ class AccountController < ApplicationController
# Show user's account # Show user's account
def show def show
@user = User.active.find(params[:id]) @user = User.find_active(params[:id])
@custom_values = @user.custom_values @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 # show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership| @memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project)) membership.project.is_public? || (User.current.member_of?(membership.project))
end end
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
@events_by_day = events.group_by(&:event_date)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
+8 -15
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AdminController < ApplicationController class AdminController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
helper :sort helper :sort
@@ -27,32 +28,24 @@ class AdminController < ApplicationController
def projects def projects
sort_init 'name', 'asc' sort_init 'name', 'asc'
sort_update %w(name is_public created_on) sort_update
@status = params[:status] ? params[:status].to_i : 1 @status = params[:status] ? params[:status].to_i : 0
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) conditions = nil
conditions = ["status=?", @status] unless @status == 0
unless params[:name].blank? @project_count = Project.count(:conditions => conditions)
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
end
@project_count = Project.count(:conditions => c.conditions)
@project_pages = Paginator.new self, @project_count, @project_pages = Paginator.new self, @project_count,
per_page_option, per_page_option,
params['page'] params['page']
@projects = Project.find :all, :order => sort_clause, @projects = Project.find :all, :order => sort_clause,
:conditions => c.conditions, :conditions => conditions,
:limit => @project_pages.items_per_page, :limit => @project_pages.items_per_page,
:offset => @project_pages.current.offset :offset => @project_pages.current.offset
render :action => "projects", :layout => false if request.xhr? render :action => "projects", :layout => false if request.xhr?
end end
def plugins
@plugins = Redmine::Plugin.all
end
# Loads the default configuration # Loads the default configuration
# (roles, trackers, statuses, workflow, enumerations) # (roles, trackers, statuses, workflow, enumerations)
def default_configuration def default_configuration
@@ -86,8 +79,8 @@ class AdminController < ApplicationController
@flags = { @flags = {
:default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?, :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), :file_repository_writable => File.writable?(Attachment.storage_path),
:plugin_assets_writable => File.writable?(Engines.public_directory),
:rmagick_available => Object.const_defined?(:Magick) :rmagick_available => Object.const_defined?(:Magick)
} }
@plugins = Redmine::Plugin.registered_plugins
end end
end end
+9 -24
View File
@@ -16,11 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'uri' require 'uri'
require 'cgi'
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
layout 'base'
before_filter :user_setup, :check_if_login_required, :set_localization before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password filter_parameter_logging :password
@@ -46,7 +43,7 @@ class ApplicationController < ActionController::Base
def find_current_user def find_current_user
if session[:user_id] if session[:user_id]
# existing session # existing session
(User.active.find(session[:user_id]) rescue nil) (User.find_active(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin? elsif cookies[:autologin] && Setting.autologin?
# auto-login feature # auto-login feature
User.find_by_autologin_key(cookies[:autologin]) User.find_by_autologin_key(cookies[:autologin])
@@ -82,7 +79,7 @@ class ApplicationController < ActionController::Base
def require_login def require_login
if !User.current.logged? if !User.current.logged?
redirect_to :controller => "account", :action => "login", :back_url => url_for(params) redirect_to :controller => "account", :action => "login", :back_url => request.request_uri
return false return false
end end
true true
@@ -97,14 +94,10 @@ class ApplicationController < ActionController::Base
true true
end end
def deny_access
User.current.logged? ? render_403 : require_login
end
# Authorize the user for the requested action # Authorize the user for the requested action
def authorize(ctrl = params[:controller], action = params[:action]) def authorize(ctrl = params[:controller], action = params[:action])
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project) allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : deny_access allowed ? true : (User.current.logged? ? render_403 : require_login)
end end
# make sure that the user is a member of the project (or admin) if project is private # make sure that the user is a member of the project (or admin) if project is private
@@ -124,16 +117,12 @@ class ApplicationController < ActionController::Base
end end
def redirect_back_or_default(default) def redirect_back_or_default(default)
back_url = CGI.unescape(params[:back_url].to_s) back_url = params[:back_url]
if !back_url.blank? if !back_url.blank?
begin uri = URI.parse(back_url)
uri = URI.parse(back_url) # do not redirect user to another host
# do not redirect user to another host or to the login or register page if uri.relative? || (uri.host == request.host)
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) redirect_to(back_url) and return
redirect_to(back_url) and return
end
rescue URI::InvalidURIError
# redirect to default
end end
end end
redirect_to default redirect_to default
@@ -175,7 +164,6 @@ class ApplicationController < ActionController::Base
# TODO: move to model # TODO: move to model
def attach_files(obj, attachments) def attach_files(obj, attachments)
attached = [] attached = []
unsaved = []
if attachments && attachments.is_a?(Hash) if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment| attachments.each_value do |attachment|
file = attachment['file'] file = attachment['file']
@@ -184,10 +172,7 @@ class ApplicationController < ActionController::Base
:file => file, :file => file,
:description => attachment['description'].to_s.strip, :description => attachment['description'].to_s.strip,
:author => User.current) :author => User.current)
a.new_record? ? (unsaved << a) : (attached << a) attached << a unless a.new_record?
end
if unsaved.any?
flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
end end
end end
attached attached
+8 -26
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,11 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AttachmentsController < ApplicationController class AttachmentsController < ApplicationController
layout 'base'
before_filter :find_project before_filter :find_project
before_filter :read_authorize, :except => :destroy
before_filter :delete_authorize, :only => :destroy
verify :method => :post, :only => :destroy
def show def show
if @attachment.is_diff? if @attachment.is_diff?
@@ -35,23 +32,12 @@ class AttachmentsController < ApplicationController
end end
def download def download
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project) @attachment.increment_download if @attachment.container.is_a?(Version)
@attachment.increment_download
end
# images are sent inline # images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type, :type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment') :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 end
private private
@@ -59,16 +45,12 @@ private
@attachment = Attachment.find(params[:id]) @attachment = Attachment.find(params[:id])
# Show 404 if the filename in the url is wrong # Show 404 if the filename in the url is wrong
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
@project = @attachment.project @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 rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
def read_authorize
@attachment.visible? ? true : deny_access
end
def delete_authorize
@attachment.deletable? ? true : deny_access
end
end end
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSourcesController < ApplicationController class AuthSourcesController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
def index def index
+4 -5
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class BoardsController < ApplicationController class BoardsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize before_filter :find_project, :authorize
helper :messages helper :messages
@@ -35,14 +36,12 @@ class BoardsController < ApplicationController
end end
def show def show
sort_init 'updated_on', 'desc' sort_init "#{Message.table_name}.updated_on", "desc"
sort_update 'created_on' => "#{Message.table_name}.created_on", sort_update
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topic_count = @board.topics.count @topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] @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}], :include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page, :limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset :offset => @topic_pages.current.offset
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CustomFieldsController < ApplicationController class CustomFieldsController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
def index def index
+6 -1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentsController < ApplicationController class DocumentsController < ApplicationController
layout 'base'
before_filter :find_project, :only => [:index, :new] before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new] before_filter :find_document, :except => [:index, :new]
before_filter :authorize before_filter :authorize
@@ -35,7 +36,6 @@ class DocumentsController < ApplicationController
else else
@grouped = documents.group_by(&:category) @grouped = documents.group_by(&:category)
end end
@document = @project.documents.build
render :layout => false if request.xhr? render :layout => false if request.xhr?
end end
@@ -72,6 +72,11 @@ class DocumentsController < ApplicationController
redirect_to :action => 'show', :id => @document redirect_to :action => 'show', :id => @document
end end
def destroy_attachment
@document.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'show', :id => @document
end
private private
def find_project def find_project
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class EnumerationsController < ApplicationController class EnumerationsController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
def index def index
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueCategoriesController < ApplicationController class IssueCategoriesController < ApplicationController
layout 'base'
menu_item :settings menu_item :settings
before_filter :find_project, :authorize before_filter :find_project, :authorize
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueRelationsController < ApplicationController class IssueRelationsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize before_filter :find_project, :authorize
def new def new
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueStatusesController < ApplicationController class IssueStatusesController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move ], verify :method => :post, :only => [ :destroy, :create, :update, :move ],
+37 -92
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -16,13 +16,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController class IssuesController < ApplicationController
layout 'base'
menu_item :new_issue, :only => :new 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_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview] before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes accept_key_auth :index, :changes
helper :journals helper :journals
@@ -30,6 +31,8 @@ class IssuesController < ApplicationController
include ProjectsHelper include ProjectsHelper
helper :custom_fields helper :custom_fields
include CustomFieldsHelper include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issue_relations helper :issue_relations
include IssueRelationsHelper include IssueRelationsHelper
helper :watchers helper :watchers
@@ -41,13 +44,11 @@ class IssuesController < ApplicationController
include SortHelper include SortHelper
include IssuesHelper include IssuesHelper
helper :timelog helper :timelog
include Redmine::Export::PDF
def index def index
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query 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? if @query.valid?
limit = per_page_option limit = per_page_option
respond_to do |format| respond_to do |format|
@@ -67,7 +68,7 @@ class IssuesController < ApplicationController
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } 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.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.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 end
else else
# Send html if the query is not valid # Send html if the query is not valid
@@ -78,10 +79,9 @@ class IssuesController < ApplicationController
end end
def changes def changes
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query 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? if @query.valid?
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:conditions => @query.statement, :conditions => @query.statement,
@@ -105,7 +105,7 @@ class IssuesController < ApplicationController
respond_to do |format| respond_to do |format|
format.html { render :template => 'issues/show.rhtml' } format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } 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
end end
@@ -122,10 +122,7 @@ class IssuesController < ApplicationController
render :nothing => true, :layout => true render :nothing => true, :layout => true
return return
end end
if params[:issue].is_a?(Hash) @issue.attributes = params[:issue]
@issue.attributes = params[:issue]
@issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
end
@issue.author = User.current @issue.author = User.current
default_status = IssueStatus.default default_status = IssueStatus.default
@@ -147,8 +144,7 @@ class IssuesController < ApplicationController
attach_files(@issue, params[:attachments]) attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create) flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } : redirect_to :controller => 'issues', :action => 'show', :id => @issue
{ :action => 'show', :id => @issue })
return return
end end
end end
@@ -181,12 +177,9 @@ class IssuesController < ApplicationController
@time_entry.attributes = params[:time_entry] @time_entry.attributes = params[:time_entry]
attachments = attach_files(@issue, params[:attachments]) attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} 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 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
# Log spend time # Log spend time
if User.current.allowed_to?(:log_time, @project) if current_role.allowed_to?(:log_time)
@time_entry.save @time_entry.save
end end
if !journal.new_record? if !journal.new_record?
@@ -230,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]) 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]) 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]) 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 = [] unsaved_issue_ids = []
@issues.each do |issue| @issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes]) journal = issue.init_journal(User.current, params[:notes])
@@ -241,7 +233,9 @@ class IssuesController < ApplicationController
issue.start_date = params[:start_date] unless params[:start_date].blank? issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank? issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].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 # 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 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) # Send notification for each issue (if changed)
@@ -256,12 +250,12 @@ class IssuesController < ApplicationController
else else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end end
redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project}) redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return return
end end
# Find potential statuses the user could be allowed to switch issues to # Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status, @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 end
def move def move
@@ -319,62 +313,15 @@ class IssuesController < ApplicationController
redirect_to :action => 'index', :project_id => @project redirect_to :action => 'index', :project_id => @project
end end
def gantt def destroy_attachment
@gantt = Redmine::Helpers::Gantt.new(params) a = @issue.attachments.find(params[:attachment_id])
retrieve_query a.destroy
if @query.valid? journal = @issue.init_journal(User.current)
events = [] journal.details << JournalDetail.new(:property => 'attachment',
# Issues that have start and due dates :prop_key => a.id,
events += Issue.find(:all, :old_value => a.filename)
:order => "start_date, due_date", journal.save
:include => [:tracker, :status, :assigned_to, :priority, :project], redirect_to :action => 'show', :id => @issue
: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
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 => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.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?
end end
def context_menu def context_menu
@@ -382,21 +329,19 @@ class IssuesController < ApplicationController
if (@issues.size == 1) if (@issues.size == 1)
@issue = @issues.first @issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) @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 end
projects = @issues.collect(&:project).compact.uniq projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1 @project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:log_time => (@project && User.current.allowed_to?(:log_time, @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)), :move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_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)) :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 @priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position') @statuses = IssueStatus.find(:all, :order => 'position')
@@ -447,9 +392,9 @@ private
end end
def find_optional_project def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank? return true unless params[:project_id]
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true) @project = Project.find(params[:project_id])
allowed ? true : deny_access authorize
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
+1 -1
View File
@@ -16,13 +16,13 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController class JournalsController < ApplicationController
layout 'base'
before_filter :find_journal before_filter :find_journal
def edit def edit
if request.post? if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes] @journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank? @journal.destroy if @journal.details.empty? && @journal.notes.blank?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format| respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id } format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' } format.js { render :action => 'update' }
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController class MembersController < ApplicationController
layout 'base'
before_filter :find_member, :except => :new before_filter :find_member, :except => :new
before_filter :find_project, :only => :new before_filter :find_project, :only => :new
before_filter :authorize before_filter :authorize
+4 -21
View File
@@ -16,21 +16,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MessagesController < ApplicationController class MessagesController < ApplicationController
layout 'base'
menu_item :boards menu_item :boards
before_filter :find_board, :only => [:new, :preview] before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [: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 :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
verify :xhr => true, :only => :quote
helper :watchers
helper :attachments helper :attachments
include AttachmentsHelper include AttachmentsHelper
# Show a topic and its replies # Show a topic and its replies
def show 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? @replies.reverse! if User.current.wants_comments_in_reverse_order?
@reply = Message.new(:subject => "RE: #{@message.subject}") @reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr? render :action => "show", :layout => false if request.xhr?
@@ -65,8 +64,7 @@ class MessagesController < ApplicationController
# Edit a message # Edit a message
def edit def edit
render_403 and return false unless @message.editable_by?(User.current) if params[:message] && User.current.allowed_to?(:edit_messages, @project)
if params[:message]
@message.locked = params[:message]['locked'] @message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky'] @message.sticky = params[:message]['sticky']
end end
@@ -79,27 +77,12 @@ class MessagesController < ApplicationController
# Delete a messages # Delete a messages
def destroy def destroy
render_403 and return false unless @message.destroyable_by?(User.current)
@message.destroy @message.destroy
redirect_to @message.parent.nil? ? redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
{ :action => 'show', :id => @message.parent } { :action => 'show', :id => @message.parent }
end 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 def preview
message = @board.messages.find_by_id(params[:id]) message = @board.messages.find_by_id(params[:id])
@attachements = message.attachments if message @attachements = message.attachments if message
+3 -2
View File
@@ -16,10 +16,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MyController < ApplicationController class MyController < ApplicationController
before_filter :require_login
helper :issues helper :issues
layout 'base'
before_filter :require_login
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
'issuesreportedbyme' => :label_reported_issues, 'issuesreportedbyme' => :label_reported_issues,
'issueswatched' => :label_watched_issues, 'issueswatched' => :label_watched_issues,
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsController < ApplicationController class NewsController < ApplicationController
layout 'base'
before_filter :find_news, :except => [:new, :index, :preview] before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => [:new, :preview] before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview] before_filter :authorize, :except => [:index, :preview]
+104 -35
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ProjectsController < ApplicationController class ProjectsController < ApplicationController
layout 'base'
menu_item :overview menu_item :overview
menu_item :activity, :only => :activity menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap menu_item :roadmap, :only => :roadmap
@@ -27,12 +28,14 @@ class ProjectsController < ApplicationController
before_filter :find_optional_project, :only => :activity before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ] before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ] before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity accept_key_auth :activity, :calendar
helper :sort helper :sort
include SortHelper include SortHelper
helper :custom_fields helper :custom_fields
include CustomFieldsHelper include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issues helper :issues
helper IssuesHelper helper IssuesHelper
helper :queries helper :queries
@@ -67,7 +70,6 @@ class ProjectsController < ApplicationController
:order => 'name') :order => 'name')
@project = Project.new(params[:project]) @project = Project.new(params[:project])
if request.get? if request.get?
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
@project.trackers = Tracker.all @project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public? @project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Redmine::AccessControl.available_project_modules @project.enabled_module_names = Redmine::AccessControl.available_project_modules
@@ -82,11 +84,6 @@ class ProjectsController < ApplicationController
# Show @project # Show @project
def show 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} @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)) @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") @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 def add_file
if request.post? if request.post?
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id])) @version = @project.versions.find_by_id(params[:version_id])
attachments = attach_files(container, params[:attachments]) attachments = attach_files(@version, params[:attachments])
if !attachments.empty? && Setting.notified_events.include?('file_added') Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
Mailer.deliver_attachments_added(attachments)
end
redirect_to :controller => 'projects', :action => 'list_files', :id => @project redirect_to :controller => 'projects', :action => 'list_files', :id => @project
return
end end
@versions = @project.versions.sort @versions = @project.versions.sort
end end
def list_files def list_files
sort_init 'filename', 'asc' sort_init "#{Attachment.table_name}.filename", "asc"
sort_update 'filename' => "#{Attachment.table_name}.filename", sort_update
'created_on' => "#{Attachment.table_name}.created_on", @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
'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
render :layout => !request.xhr? render :layout => !request.xhr?
end end
@@ -232,19 +221,16 @@ class ProjectsController < ApplicationController
@days = Setting.activity_days_default.to_i @days = Setting.activity_days_default.to_i
if params[:from] if params[:from]
begin; @date_to = params[:from].to_date + 1; rescue; end begin; @date_to = params[:from].to_date; rescue; end
end end
@date_to ||= Date.today + 1 @date_to ||= Date.today + 1
@date_from = @date_to - @days @date_from = @date_to - @days
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') @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, @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects)
:with_subprojects => @with_subprojects,
:author => @author)
@activity.scope_select {|t| !params["show_#{t}"].nil?} @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) events = @activity.events(@date_from, @date_to)
@@ -254,18 +240,101 @@ class ProjectsController < ApplicationController
render :layout => false if request.xhr? render :layout => false if request.xhr?
} }
format.atom { format.atom {
title = l(:label_activity) title = (@scope.size == 1) ? l("label_#{@scope.first.singularize}_plural") : l(:label_activity)
if @author
title = @author.name
elsif @activity.scope.size == 1
title = l("label_#{@activity.scope.first.singularize}_plural")
end
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
} }
end end
end
rescue ActiveRecord::RecordNotFound def calendar
render_404 @trackers = @project.rolled_up_trackers
retrieve_selected_tracker_ids(@trackers)
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 end
private private
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueriesController < ApplicationController class QueriesController < ApplicationController
layout 'base'
menu_item :issues menu_item :issues
before_filter :find_query, :except => :new before_filter :find_query, :except => :new
before_filter :find_optional_project, :only => :new before_filter :find_optional_project, :only => :new
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ReportsController < ApplicationController class ReportsController < ApplicationController
layout 'base'
menu_item :issues menu_item :issues
before_filter :find_project, :authorize before_filter :find_project, :authorize
+7 -20
View File
@@ -23,6 +23,7 @@ class ChangesetNotFound < Exception; end
class InvalidRevisionParam < Exception; end class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController class RepositoriesController < ApplicationController
layout 'base'
menu_item :repository menu_item :repository
before_filter :find_repository, :except => :edit before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :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'} render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
end 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 def destroy
@repository.destroy @repository.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
@@ -99,8 +85,7 @@ class RepositoriesController < ApplicationController
params['page'] params['page']
@changesets = @repository.changesets.find(:all, @changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page, :limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset, :offset => @changeset_pages.current.offset)
:include => :user)
respond_to do |format| respond_to do |format|
format.html { render :layout => false if request.xhr? } format.html { render :layout => false if request.xhr? }
@@ -127,9 +112,6 @@ class RepositoriesController < ApplicationController
end end
def annotate def annotate
@entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
@annotate = @repository.scm.annotate(@path, @rev) @annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
end end
@@ -137,6 +119,11 @@ class RepositoriesController < ApplicationController
def revision def revision
@changeset = @repository.changesets.find_by_revision(@rev) @changeset = @repository.changesets.find_by_revision(@rev)
raise ChangesetNotFound unless @changeset raise ChangesetNotFound unless @changeset
@changes_count = @changeset.changes.size
@changes_pages = Paginator.new self, @changes_count, 150, params['page']
@changes = @changeset.changes.find(:all,
:limit => @changes_pages.items_per_page,
:offset => @changes_pages.current.offset)
respond_to do |format| respond_to do |format|
format.html format.html
+22
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class RolesController < ApplicationController class RolesController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
verify :method => :post, :only => [ :destroy, :move ], verify :method => :post, :only => [ :destroy, :move ],
@@ -79,6 +80,27 @@ class RolesController < ApplicationController
redirect_to :action => 'list' redirect_to :action => 'list'
end 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 def report
@roles = Role.find(:all, :order => 'builtin, position') @roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? } @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
+2
View File
@@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SearchController < ApplicationController class SearchController < ApplicationController
layout 'base'
before_filter :find_optional_project before_filter :find_optional_project
helper :messages helper :messages
+6 -9
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SettingsController < ApplicationController class SettingsController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
def index def index
@@ -39,21 +40,17 @@ class SettingsController < ApplicationController
@options = {} @options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
@deliveries = ActionMailer::Base.perform_deliveries @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 end
def plugin def plugin
@plugin = Redmine::Plugin.find(params[:id]) plugin_id = params[:id].to_sym
@plugin = Redmine::Plugin.registered_plugins[plugin_id]
if request.post? if request.post?
Setting["plugin_#{@plugin.id}"] = params[:settings] Setting["plugin_#{plugin_id}"] = params[:settings]
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => @plugin.id redirect_to :action => 'plugin', :id => params[:id]
end end
@partial = @plugin.settings[:partial] @partial = @plugin.settings[:partial]
@settings = Setting["plugin_#{@plugin.id}"] @settings = Setting["plugin_#{plugin_id}"]
rescue Redmine::PluginNotFound
render_404
end end
end end
+5 -4
View File
@@ -23,17 +23,18 @@ class SysController < ActionController::Base
before_invocation :check_enabled before_invocation :check_enabled
# Returns the projects list, with their repositories # Returns the projects list, with their repositories
def projects_with_repository_enabled def projects
Project.has_module(:repository).find(:all, :include => :repository, :order => 'identifier') Project.find(:all, :include => :repository)
end end
# Registers a repository for the given project identifier # 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) project = Project.find_by_identifier(identifier)
# Do not create the repository if the project has already one # Do not create the repository if the project has already one
return 0 unless project && project.repository.nil? return 0 unless project && project.repository.nil?
logger.debug "Repository for #{project.name} was created" 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.save
repository.id || 0 repository.id || 0
end end
+14 -36
View File
@@ -16,9 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TimelogController < ApplicationController class TimelogController < ApplicationController
layout 'base'
menu_item :issues menu_item :issues
before_filter :find_project, :authorize, :only => [:edit, :destroy] before_filter :find_project, :authorize
before_filter :find_optional_project, :only => [:report, :details]
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details } 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 # Add list and boolean custom fields as available criterias
custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
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)", @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, :format => cf.field_format,
:label => cf.name} :label => cf.name}
end if @project end
# Add list and boolean time entry custom fields # Add list and boolean time entry custom fields
TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf| 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 << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id" 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 << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE" sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " (%s) AND" % 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 << " (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" sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
@hours = ActiveRecord::Base.connection.select_all(sql) @hours = ActiveRecord::Base.connection.select_all(sql)
@@ -138,21 +136,11 @@ class TimelogController < ApplicationController
def details def details
sort_init 'spent_on', 'desc' sort_init 'spent_on', 'desc'
sort_update 'spent_on' => 'spent_on', sort_update
'user' => 'user_id',
'activity' => 'activity_id',
'project' => "#{Project.table_name}.name",
'issue' => 'issue_id',
'hours' => 'hours'
cond = ARCondition.new cond = ARCondition.new
if @project.nil? cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
cond << Project.allowed_to_condition(User.current, :view_time_entries) ["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
elsif @issue.nil?
cond << @project.project_condition(Setting.display_subprojects_issues?)
else
cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
end
retrieve_date_range retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to] cond << ['spent_on BETWEEN ? AND ?', @from, @to]
@@ -199,7 +187,7 @@ class TimelogController < ApplicationController
@time_entry.attributes = params[:time_entry] @time_entry.attributes = params[:time_entry]
if request.post? and @time_entry.save if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update) 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 return
end end
end end
@@ -210,7 +198,7 @@ class TimelogController < ApplicationController
@time_entry.destroy @time_entry.destroy
flash[:notice] = l(:notice_successful_delete) flash[:notice] = l(:notice_successful_delete)
redirect_to :back redirect_to :back
rescue ::ActionController::RedirectBackError rescue RedirectBackError
redirect_to :action => 'details', :project_id => @time_entry.project redirect_to :action => 'details', :project_id => @time_entry.project
end end
@@ -232,16 +220,6 @@ private
render_404 render_404
end 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 # Retrieves the date range based on predefined ranges or specific from/to param dates
def retrieve_date_range def retrieve_date_range
@free_period = false @free_period = false
@@ -284,7 +262,7 @@ private
end end
@from, @to = @to, @from if @from && @to && @from > @to @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 @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.allowed_to_condition(User.current, :view_time_entries)) || Date.today) @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
end end
end end
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TrackersController < ApplicationController class TrackersController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
def index def index
+7 -14
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class UsersController < ApplicationController class UsersController < ApplicationController
layout 'base'
before_filter :require_admin before_filter :require_admin
helper :sort helper :sort
@@ -30,22 +31,18 @@ class UsersController < ApplicationController
def list def list
sort_init 'login', 'asc' 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 @status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) conditions = "status <> 0"
conditions = ["status=?", @status] unless @status == 0
unless params[:name].blank? @user_count = User.count(:conditions => conditions)
name = "%#{params[:name].strip.downcase}%"
c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
end
@user_count = User.count(:conditions => c.conditions)
@user_pages = Paginator.new self, @user_count, @user_pages = Paginator.new self, @user_count,
per_page_option, per_page_option,
params['page'] params['page']
@users = User.find :all,:order => sort_clause, @users = User.find :all,:order => sort_clause,
:conditions => c.conditions, :conditions => conditions,
:limit => @user_pages.items_per_page, :limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset :offset => @user_pages.current.offset
@@ -75,11 +72,7 @@ class UsersController < ApplicationController
@user.admin = params[:user][:admin] if params[:user][:admin] @user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login] @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.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] if @user.update_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
flash[:notice] = l(:notice_successful_update) flash[:notice] = l(:notice_successful_update)
# Give a string to redirect_to otherwise it would use status param as the response code # 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])) redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
+8 -1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class VersionsController < ApplicationController class VersionsController < ApplicationController
layout 'base'
menu_item :roadmap menu_item :roadmap
before_filter :find_project, :authorize before_filter :find_project, :authorize
@@ -33,10 +34,16 @@ class VersionsController < ApplicationController
@version.destroy @version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
rescue rescue
flash[:error] = l(:notice_unable_delete_version) flash[:error] = "Unable to delete version"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end 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 def status_by
respond_to do |format| respond_to do |format|
format.html { render :action => 'show' } format.html { render :action => 'show' }
+18 -35
View File
@@ -16,38 +16,31 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WatchersController < ApplicationController class WatchersController < ApplicationController
before_filter :find_project layout 'base'
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch] before_filter :require_login, :find_project, :check_project_privacy
before_filter :authorize, :only => :new
verify :method => :post, def add
:only => [ :watch, :unwatch ], user = User.current
:render => { :nothing => true, :status => :method_not_allowed } @watched.add_watcher(user)
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?
respond_to do |format| respond_to do |format|
format.html { redirect_to :back } format.html { redirect_to :back }
format.js do format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
render :update do |page|
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
end
end
end end
rescue ::ActionController::RedirectBackError rescue RedirectBackError
render :text => 'Watcher added.', :layout => true render :text => 'Watcher added.', :layout => true
end 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 private
def find_project def find_project
klass = Object.const_get(params[:object_type].camelcase) klass = Object.const_get(params[:object_type].camelcase)
@@ -57,14 +50,4 @@ private
rescue rescue
render_404 render_404
end 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 end
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WelcomeController < ApplicationController class WelcomeController < ApplicationController
layout 'base'
def index def index
@news = News.latest User.current @news = News.latest User.current
+24 -31
View File
@@ -18,10 +18,10 @@
require 'diff' require 'diff'
class WikiController < ApplicationController class WikiController < ApplicationController
layout 'base'
before_filter :find_wiki, :authorize 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 helper :attachments
include AttachmentsHelper include AttachmentsHelper
@@ -39,11 +39,6 @@ class WikiController < ApplicationController
end end
return return
end 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]) @content = @page.content_for_version(params[:version])
if params[:export] == 'html' if params[:export] == 'html'
export = render_to_string :action => 'export', :layout => false 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? @page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version]) @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 # don't keep previous comment
@content.comments = nil @content.comments = nil
if request.get? if request.post?
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
else
if !@page.new_record? && @content.text == params[:content][:text] if !@page.new_record? && @content.text == params[:content][:text]
# don't save if text wasn't changed # don't save if text wasn't changed
redirect_to :action => 'index', :id => @project, :page => @page.title redirect_to :action => 'index', :id => @project, :page => @page.title
@@ -92,7 +84,8 @@ class WikiController < ApplicationController
# rename a page # rename a page
def rename def rename
return render_403 unless editable? @page = @wiki.find_page(params[:page])
return render_403 unless editable?
@page.redirect_existing_links = true @page.redirect_existing_links = true
# used to display the *original* title if some AR validation errors occur # used to display the *original* title if some AR validation errors occur
@original_title = @page.pretty_title @original_title = @page.pretty_title
@@ -103,12 +96,15 @@ class WikiController < ApplicationController
end end
def protect def protect
@page.update_attribute :protected, params[:protected] page = @wiki.find_page(params[:page])
redirect_to :action => 'index', :id => @project, :page => @page.title page.update_attribute :protected, params[:protected]
redirect_to :action => 'index', :id => @project, :page => page.title
end end
# show page history # show page history
def history def history
@page = @wiki.find_page(params[:page])
@version_count = @page.content.versions.count @version_count = @page.content.versions.count
@version_pages = Paginator.new self, @version_count, per_page_option, params['p'] @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text # don't load text
@@ -122,19 +118,21 @@ class WikiController < ApplicationController
end end
def diff def diff
@page = @wiki.find_page(params[:page])
@diff = @page.diff(params[:version], params[:version_from]) @diff = @page.diff(params[:version], params[:version_from])
render_404 unless @diff render_404 unless @diff
end end
def annotate def annotate
@page = @wiki.find_page(params[:page])
@annotate = @page.annotate(params[:version]) @annotate = @page.annotate(params[:version])
render_404 unless @annotate
end end
# remove a wiki page and its history # remove a wiki page and its history
def destroy def destroy
return render_403 unless editable? @page = @wiki.find_page(params[:page])
@page.destroy return render_403 unless editable?
@page.destroy if @page
redirect_to :action => 'special', :id => @project, :page => 'Page_index' redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end end
@@ -176,11 +174,19 @@ class WikiController < ApplicationController
end end
def add_attachment def add_attachment
@page = @wiki.find_page(params[:page])
return render_403 unless editable? return render_403 unless editable?
attach_files(@page, params[:attachments]) attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title redirect_to :action => 'index', :page => @page.title
end 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 private
def find_wiki def find_wiki
@@ -191,21 +197,8 @@ private
render_404 render_404
end 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 # Returns true if the current user is allowed to edit the page, otherwise false
def editable?(page = @page) def editable?(page = @page)
page.editable_by?(User.current) page.editable_by?(User.current)
end 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 end
+1
View File
@@ -16,6 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WikisController < ApplicationController class WikisController < ApplicationController
layout 'base'
menu_item :settings menu_item :settings
before_filter :find_project, :authorize before_filter :find_project, :authorize
-45
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
+1 -1
View File
@@ -17,7 +17,7 @@
module AdminHelper module AdminHelper
def project_status_options_for_select(selected) def project_status_options_for_select(selected)
options_for_select([[l(:label_all), ''], options_for_select([[l(:label_all), "*"],
[l(:status_active), 1]], selected) [l(:status_active), 1]], selected)
end end
end end
+38 -125
View File
@@ -17,15 +17,9 @@
require 'coderay' require 'coderay'
require 'coderay/helpers/file_type' require 'coderay/helpers/file_type'
require 'forwardable'
require 'cgi'
module ApplicationHelper module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions include Redmine::WikiFormatting::Macros::Definitions
include GravatarHelper::PublicMethods
extend Forwardable
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
def current_role def current_role
@current_role ||= User.current.role_for_project(@project) @current_role ||= User.current.role_for_project(@project)
@@ -41,15 +35,9 @@ module ApplicationHelper
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
end 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 # Display a link to user's account page
def link_to_user(user, options={}) def link_to_user(user)
(user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous' user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
end end
def link_to_issue(issue, options={}) def link_to_issue(issue, options={})
@@ -101,62 +89,12 @@ module ApplicationHelper
return nil unless time return nil unless time
time = time.to_time if time.is_a?(String) time = time.to_time if time.is_a?(String)
zone = User.current.time_zone 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) @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) @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) include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end 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 # Truncates and returns the string as a single line
def truncate_single_line(string, *args) def truncate_single_line(string, *args)
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ') truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
@@ -166,18 +104,14 @@ module ApplicationHelper
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>') text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end end
def authoring(created, author, options={}) def authoring(created, author)
time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) : time_tag = 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))
author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous') 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 end
def l_or_humanize(s, options={}) def l_or_humanize(s)
k = "#{options[:prefix]}#{s}".to_sym l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
l_has_string?(k) ? l(k) : s.to_s.humanize
end end
def day_name(day) def day_name(day)
@@ -288,23 +222,25 @@ module ApplicationHelper
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments if attachments
attachments = attachments.sort_by(&:created_on).reverse text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
style = $1 style = $1
filename = $6.downcase filename = $6
rf = Regexp.new(filename, Regexp::IGNORECASE)
# search for the picture in attachments # 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 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})" alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!" "!#{style}#{image_url}#{alt}!"
else else
m "!#{style}#{filename}!"
end end
end 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 # different methods for formatting wiki links
case options[:wiki_links] case options[:wiki_links]
@@ -383,9 +319,7 @@ module ApplicationHelper
# source:some/file#L120 -> Link to line 120 of the file # 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 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file # export:some/file -> Force the download of the file
# Forum messages: text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
# 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|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil link = nil
if esc.nil? if esc.nil?
@@ -415,16 +349,6 @@ module ApplicationHelper
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version' :class => 'version'
end 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 end
elsif sep == ':' elsif sep == ':'
# removes the double quotes if any # removes the double quotes if any
@@ -487,11 +411,11 @@ module ApplicationHelper
full_messages = [] full_messages = []
object.errors.each do |attr, msg| object.errors.each do |attr, msg|
next if msg.nil? next if msg.nil?
msg = [msg] unless msg.is_a?(Array) msg = msg.first if msg.is_a? Array
if attr == "base" if attr == "base"
full_messages << l(*msg) full_messages << l(msg)
else 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
end end
# retrieve custom values error messages # retrieve custom values error messages
@@ -499,8 +423,8 @@ module ApplicationHelper
object.custom_values.each do |v| object.custom_values.each do |v|
v.errors.each do |attr, msg| v.errors.each do |attr, msg|
next if msg.nil? next if msg.nil?
msg = [msg] unless msg.is_a?(Array) msg = msg.first if msg.is_a? Array
full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(*msg) full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
end end
end end
end end
@@ -534,8 +458,7 @@ module ApplicationHelper
def back_url_hidden_field_tag def back_url_hidden_field_tag
back_url = params[:back_url] || request.env['HTTP_REFERER'] back_url = params[:back_url] || request.env['HTTP_REFERER']
back_url = CGI.unescape(back_url.to_s) hidden_field_tag('back_url', back_url) unless back_url.blank?
hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
end end
def check_all_links(form_name) def check_all_links(form_name)
@@ -552,9 +475,9 @@ module ApplicationHelper
legend = options[:legend] || '' legend = options[:legend] || ''
content_tag('table', content_tag('table',
content_tag('tr', content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') + (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') + (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '') (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") + ), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent') content_tag('p', legend, :class => 'pourcent')
end end
@@ -593,6 +516,18 @@ module ApplicationHelper
end 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) def content_for(name, content = nil, &block)
@has_content ||= {} @has_content ||= {}
@has_content[name] = true @has_content[name] = true
@@ -602,26 +537,4 @@ module ApplicationHelper
def has_content?(name) def has_content?(name)
(@has_content && @has_content[name]) || false (@has_content && @has_content[name]) || false
end 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 end
+4 -9
View File
@@ -16,15 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module AttachmentsHelper module AttachmentsHelper
# Displays view/delete links to the attachments of the given object # displays the links to a collection of attachments
# Options: def link_to_attachments(attachments, options = {})
# :author -- author names are not displayed if set to false if attachments.any?
def link_to_attachments(container, options = {}) render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => 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}
end end
end end
+85
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
+2 -12
View File
@@ -33,14 +33,6 @@ module IssuesHelper
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}" "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end 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
end
def sidebar_queries def sidebar_queries
unless @sidebar_queries unless @sidebar_queries
# User can see public queries and his own queries # User can see public queries and his own queries
@@ -83,9 +75,6 @@ module IssuesHelper
when 'fixed_version_id' when 'fixed_version_id'
v = Version.find_by_id(detail.value) and value = v.name if detail.value 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 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 end
when 'cf' when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key) custom_field = CustomField.find_by_id(detail.prop_key)
@@ -97,7 +86,8 @@ module IssuesHelper
when 'attachment' when 'attachment'
label = l(:label_attachment) label = l(:label_attachment)
end 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 label ||= detail.prop_key
value ||= detail.value value ||= detail.value
+3 -3
View File
@@ -21,12 +21,12 @@ module JournalsHelper
editable = journal.editable_by?(User.current) editable = journal.editable_by?(User.current)
links = [] links = []
if !journal.notes.blank? 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", links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal }, { :controller => 'journals', :action => 'edit', :id => journal },
:title => l(:button_edit)) if editable :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 end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty? content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes) content << textilizable(journal, :notes)
+162
View File
@@ -21,6 +21,18 @@ module ProjectsHelper
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end 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 def project_settings_tabs
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, 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}, {: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)} tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end 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 end
+2 -4
View File
@@ -22,8 +22,8 @@ module QueriesHelper
end end
def column_header(column) def column_header(column)
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption, column.sortable ? sort_header_tag(column.sortable, :caption => column.caption,
:default_order => column.default_order) : :default_order => column.default_order) :
content_tag('th', column.caption) content_tag('th', column.caption)
end end
@@ -44,8 +44,6 @@ module QueriesHelper
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
when :done_ratio when :done_ratio
progress_bar(value, :width => '80px') progress_bar(value, :width => '80px')
when :fixed_version
link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
else else
h(value) h(value)
end end
-74
View File
@@ -22,12 +22,6 @@ module RepositoriesHelper
txt.to_s[0,8] txt.to_s[0,8]
end 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) def render_properties(properties)
unless properties.nil? || properties.empty? unless properties.nil? || properties.empty?
content = '' content = ''
@@ -38,74 +32,6 @@ module RepositoriesHelper
end end
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) def to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip) @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
-1
View File
@@ -19,7 +19,6 @@ module SettingsHelper
def administration_settings_tabs def administration_settings_tabs
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general}, tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication}, {: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 => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)}, {:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)}, {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},
+5 -13
View File
@@ -67,31 +67,23 @@ module SortHelper
# Updates the sort state. Call this in the controller prior to calling # Updates the sort state. Call this in the controller prior to calling
# sort_clause. # sort_clause.
# sort_keys can be either an array or a hash of allowed keys #
def sort_update(sort_keys) def sort_update()
sort_key = params[:sort_key] if params[:sort_key]
sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key]) sort = {:key => params[:sort_key], :order => params[:sort_order]}
sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC')
if sort_key
sort = {:key => sort_key, :order => sort_order}
elsif session[@sort_name] elsif session[@sort_name]
sort = session[@sort_name] # Previous sort. sort = session[@sort_name] # Previous sort.
else else
sort = @sort_default sort = @sort_default
end end
session[@sort_name] = sort 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 end
# Returns an SQL sort clause corresponding to the current sort state. # Returns an SQL sort clause corresponding to the current sort state.
# Use this to sort the controller's table items collection. # Use this to sort the controller's table items collection.
# #
def sort_clause() def sort_clause()
@sort_clause session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
end end
# Returns a link which sorts by the named column. # Returns a link which sorts by the named column.
+1 -11
View File
@@ -16,16 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module TimelogHelper 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 def activity_collection_for_select_options
activities = Enumeration::get_values('ACTI') activities = Enumeration::get_values('ACTI')
collection = [] collection = []
@@ -83,7 +73,7 @@ module TimelogHelper
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end } csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines # csv lines
entries.each do |entry| entries.each do |entry|
fields = [format_date(entry.spent_on), fields = [l_date(entry.spent_on),
entry.user, entry.user,
entry.activity, entry.activity,
entry.project, entry.project,
+4 -5
View File
@@ -16,12 +16,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module UsersHelper module UsersHelper
def users_status_options_for_select(selected) def status_options_for_select(selected)
user_count_by_status = User.count(:group => 'status').to_hash
options_for_select([[l(:label_all), ''], options_for_select([[l(:label_all), ''],
["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1], [l(:status_active), 1],
["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2], [l(:status_registered), 2],
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected) [l(:status_locked), 3]], selected)
end end
# Options for the new membership projects combo-box # Options for the new membership projects combo-box
+1 -6
View File
@@ -24,7 +24,7 @@ module WatchersHelper
return '' unless user && user.logged? && object.respond_to?('watched_by?') return '' unless user && user.logged? && object.respond_to?('watched_by?')
watched = object.watched_by?(user) watched = object.watched_by?(user)
url = {:controller => 'watchers', url = {:controller => 'watchers',
:action => (watched ? 'unwatch' : 'watch'), :action => (watched ? 'remove' : 'add'),
:object_type => object.class.to_s.underscore, :object_type => object.class.to_s.underscore,
:object_id => object.id} :object_id => object.id}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), 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')) :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
end 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 end
+16
View File
@@ -17,6 +17,22 @@
module WikiHelper 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) def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)} words = wdiff.words.collect{|word| h(word)}
words_add = 0 words_add = 0
-19
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
+1 -18
View File
@@ -30,14 +30,12 @@ class Attachment < ActiveRecord::Base
acts_as_activity_provider :type => 'files', acts_as_activity_provider :type => 'files',
:permission => :view_files, :permission => :view_files,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*", :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 " + :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"} "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity_provider :type => 'documents', acts_as_activity_provider :type => 'documents',
:permission => :view_documents, :permission => :view_documents,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*", :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 " + :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"} "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
@@ -72,7 +70,7 @@ class Attachment < ActiveRecord::Base
File.open(diskfile, "wb") do |f| File.open(diskfile, "wb") do |f|
f.write(@temp_file.read) f.write(@temp_file.read)
end end
self.digest = self.class.digest(diskfile) self.digest = Digest::MD5.hexdigest(File.read(diskfile))
end end
# Don't save the content type if it's longer than the authorized length # Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255 if self.content_type && self.content_type.length > 255
@@ -98,14 +96,6 @@ class Attachment < ActiveRecord::Base
container.project container.project
end end
def visible?(user=User.current)
container.attachments_visible?(user)
end
def deletable?(user=User.current)
container.attachments_deletable?(user)
end
def image? def image?
self.filename =~ /\.(jpe?g|gif|png)$/i self.filename =~ /\.(jpe?g|gif|png)$/i
end end
@@ -141,11 +131,4 @@ private
end end
df df
end end
# Returns the MD5 digest of the file at given path
def self.digest(filename)
File.open(filename, 'rb') do |f|
Digest::MD5.hexdigest(f.read)
end
end
end end
+1 -2
View File
@@ -38,8 +38,7 @@ class AuthSource < ActiveRecord::Base
begin begin
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug? logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
attrs = source.authenticate(login, password) attrs = source.authenticate(login, password)
rescue => e rescue
logger.error "Error during authentication: #{e.message}"
attrs = nil attrs = nil
end end
return attrs if attrs return attrs if attrs
+2 -13
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_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
validates_numericality_of :port, :only_integer => true validates_numericality_of :port, :only_integer => true
before_validation :strip_ldap_attributes
def after_initialize def after_initialize
self.port = 389 if self.port == 0 self.port = 389 if self.port == 0
end end
@@ -73,14 +71,7 @@ class AuthSourceLdap < AuthSource
"LDAP" "LDAP"
end end
private 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
def initialize_ldap_con(ldap_user, ldap_password) def initialize_ldap_con(ldap_user, ldap_password)
options = { :host => self.host, options = { :host => self.host,
:port => self.port, :port => self.port,
@@ -91,8 +82,6 @@ class AuthSourceLdap < AuthSource
end end
def self.get_attr(entry, attr_name) def self.get_attr(entry, attr_name)
if !attr_name.blank? entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
end
end end
end end
+16 -37
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -15,17 +15,15 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'iconv'
class Changeset < ActiveRecord::Base class Changeset < ActiveRecord::Base
belongs_to :repository belongs_to :repository
belongs_to :user
has_many :changes, :dependent => :delete_all has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))}, acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
:description => :comments, :description => :comments,
:datetime => :committed_on, :datetime => :committed_on,
:author => :committer,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments', acts_as_searchable :columns => 'comments',
@@ -34,7 +32,6 @@ class Changeset < ActiveRecord::Base
:date_column => 'committed_on' :date_column => 'committed_on'
acts_as_activity_provider :timestamp => "#{table_name}.committed_on", acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => {:repository => :project}} :find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date validates_presence_of :repository_id, :revision, :committed_on, :commit_date
@@ -46,7 +43,7 @@ class Changeset < ActiveRecord::Base
end end
def comments=(comment) def comments=(comment)
write_attribute(:comments, Changeset.normalize_comments(comment)) write_attribute(:comments, comment.strip)
end end
def committed_on=(date) def committed_on=(date)
@@ -58,14 +55,6 @@ class Changeset < ActiveRecord::Base
repository.project repository.project
end end
def author
user || committer.to_s.split('<').first
end
def before_create
self.user = repository.find_committer_user(committer)
end
def after_create def after_create
scan_comment_for_issue_ids scan_comment_for_issue_ids
end end
@@ -105,11 +94,12 @@ class Changeset < ActiveRecord::Base
issue.reload issue.reload
# don't change the status is the issue is closed # don't change the status is the issue is closed
next if issue.status.is_closed? next if issue.status.is_closed?
user = committer_user || User.anonymous
csettext = "r#{self.revision}" csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/)) if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\"" csettext = "commit:\"#{self.scmid}\""
end 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.status = fix_status
issue.done_ratio = done_ratio if done_ratio issue.done_ratio = done_ratio if done_ratio
issue.save issue.save
@@ -122,6 +112,16 @@ class Changeset < ActiveRecord::Base
self.issues = referenced_issues.uniq self.issues = referenced_issues.uniq
end 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 # Returns the previous changeset
def previous def previous
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC') @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
@@ -131,25 +131,4 @@ class Changeset < ActiveRecord::Base
def next def next
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC') @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
end end
# Strips and reencodes a commit log before insertion into the database
def self.normalize_comments(str)
to_utf8(str.to_s.strip)
end
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 end
+3 -3
View File
@@ -30,9 +30,9 @@ class CustomField < ActiveRecord::Base
}.freeze }.freeze
validates_presence_of :name, :field_format validates_presence_of :name, :field_format
validates_uniqueness_of :name, :scope => :type validates_uniqueness_of :name
validates_length_of :name, :maximum => 30 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 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
def initialize(attributes = nil) def initialize(attributes = nil)
@@ -66,7 +66,7 @@ class CustomField < ActiveRecord::Base
# to move in project_custom_field # to move in project_custom_field
def self.for_all def self.for_all
find(:all, :conditions => ["is_for_all=?", true], :order => 'position') find(:all, :conditions => ["is_for_all=?", true])
end end
def type_name def type_name
+1 -7
View File
@@ -18,7 +18,7 @@
class Document < ActiveRecord::Base class Document < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id" 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_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"}, 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_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60 validates_length_of :title, :maximum => 60
def after_initialize
if new_record?
self.category ||= Enumeration.default('DCAT')
end
end
end end
+1 -3
View File
@@ -44,9 +44,7 @@ class Enumeration < ActiveRecord::Base
end end
def before_save def before_save
if is_default? && is_default_changed? Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt})
end
end end
def objects_count def objects_count
+3 -22
View File
@@ -26,13 +26,13 @@ class Issue < ActiveRecord::Base
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all 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_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_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 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_customizable
acts_as_watchable acts_as_watchable
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], 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}"}, 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}} :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}
:author_key => :author_id
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_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100 validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true validates_numericality_of :estimated_hours, :allow_nil => true
@@ -185,8 +184,6 @@ class Issue < ActiveRecord::Base
@issue_before_change.status = self.status @issue_before_change.status = self.status
@custom_values_before_change = {} @custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } 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 @current_journal
end end
@@ -195,11 +192,6 @@ class Issue < ActiveRecord::Base
self.status.is_closed? self.status.is_closed?
end 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 # Users the issue can be assigned to
def assignable_users def assignable_users
project.assignable_users project.assignable_users
@@ -266,15 +258,4 @@ class Issue < ActiveRecord::Base
def to_s def to_s
"#{tracker} ##{id}: #{subject}" "#{tracker} ##{id}: #{subject}"
end 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 end
+2 -2
View File
@@ -25,8 +25,8 @@ class IssueStatus < ActiveRecord::Base
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i validates_format_of :name, :with => /^[\w\s\'\-]*$/i
def after_save def before_save
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default? IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default?
end end
# Returns the default status for new issues # Returns the default status for new issues
-1
View File
@@ -33,7 +33,6 @@ class Journal < ActiveRecord::Base
acts_as_activity_provider :type => 'issues', acts_as_activity_provider :type => 'issues',
:permission => :view_issues, :permission => :view_issues,
:author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user], :find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
+20 -75
View File
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandler < ActionMailer::Base class MailHandler < ActionMailer::Base
include ActionView::Helpers::SanitizeHelper
class UnauthorizedAction < StandardError; end class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end class MissingInformation < StandardError; end
@@ -32,15 +31,13 @@ class MailHandler < ActionMailer::Base
@@handler_options[:allow_override] ||= [] @@handler_options[:allow_override] ||= []
# Project needs to be overridable if not specified # Project needs to be overridable if not specified
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) @@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 super email
end end
# Processes incoming emails # Processes incoming emails
def receive(email) def receive(email)
@email = 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 unless @user
# Unknown user => the email is ignored # Unknown user => the email is ignored
# TODO: ability to create the user's account # 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) 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))) 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))) 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 # check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority) issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
# check workflow issue.subject = email.subject.chomp
if status && issue.new_statuses_allowed_to(user).include?(status) issue.description = email.plain_text_body.chomp
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.save! issue.save!
add_attachments(issue) add_attachments(issue)
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info 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') Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
issue issue
end end
@@ -118,21 +100,13 @@ class MailHandler < ActionMailer::Base
# Adds a note to an existing issue # Adds a note to an existing issue
def receive_issue_update(issue_id) def receive_issue_update(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
issue = Issue.find_by_id(issue_id) issue = Issue.find_by_id(issue_id)
return unless issue return unless issue
# check permission # check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) 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 # add the note
journal = issue.init_journal(user, plain_text_body) journal = issue.init_journal(user, email.plain_text_body.chomp)
add_attachments(issue) add_attachments(issue)
# check workflow
if status && issue.new_statuses_allowed_to(user).include?(status)
issue.status = status
end
issue.save! issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
@@ -150,51 +124,22 @@ class MailHandler < ActionMailer::Base
end end
end end
# Adds To and Cc as watchers of the given object if the sender has the def get_keyword(attr)
# appropriate permission if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
def add_watchers(obj) $1.strip
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) elsif !@@handler_options[:issue][attr].blank?
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} @@handler_options[:issue][attr]
unless addresses.empty?
watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
watchers.each {|w| obj.add_watcher(w)}
end
end end
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]*(.+)\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 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
+8 -43
View File
@@ -22,19 +22,12 @@ class Mailer < ActionMailer::Base
include ActionController::UrlWriter 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) def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier, redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id, 'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login 'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients recipients issue.recipients
cc(issue.watcher_recipients - @recipients)
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue, body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
@@ -46,7 +39,6 @@ class Mailer < ActionMailer::Base
'Issue-Id' => issue.id, 'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login 'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
@author = journal.user
recipients issue.recipients recipients issue.recipients
# Watchers in cc # Watchers in cc
cc(issue.watcher_recipients - @recipients) cc(issue.watcher_recipients - @recipients)
@@ -65,7 +57,7 @@ class Mailer < ActionMailer::Base
subject l(:mail_subject_reminder, issues.size) subject l(:mail_subject_reminder, issues.size)
body :issues => issues, body :issues => issues,
:days => days, :days => days,
:issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc') :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc')
end end
def document_added(document) def document_added(document)
@@ -81,9 +73,6 @@ class Mailer < ActionMailer::Base
added_to = '' added_to = ''
added_to_url = '' added_to_url = ''
case container.class.name 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' when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}" added_to = "#{l(:label_version)}: #{container.name}"
@@ -127,21 +116,12 @@ class Mailer < ActionMailer::Base
def account_activation_request(user) def account_activation_request(user)
# Send the email to all active administrators # 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) subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user, body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
end 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) def lost_password(token)
set_language_if_valid(token.user.language) set_language_if_valid(token.user.language)
recipients token.user.mail recipients token.user.mail
@@ -203,7 +183,8 @@ class Mailer < ActionMailer::Base
super super
set_language_if_valid Setting.default_language set_language_if_valid Setting.default_language
from Setting.mail_from from Setting.mail_from
default_url_options[:host] = Setting.host_name
default_url_options[:protocol] = Setting.protocol
# Common headers # Common headers
headers 'X-Mailer' => 'Redmine', headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name, 'X-Redmine-Host' => Setting.host_name,
@@ -219,10 +200,9 @@ class Mailer < ActionMailer::Base
def create_mail def create_mail
# Removes the current user from the recipients and cc # Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does # if he doesn't want to receive notifications about what he does
@author ||= User.current if User.current.pref[:no_self_notified]
if @author.pref[:no_self_notified] recipients.delete(User.current.mail) if recipients
recipients.delete(@author.mail) if recipients cc.delete(User.current.mail) if cc
cc.delete(@author.mail) if cc
end end
# Blind carbon copy recipients # Blind carbon copy recipients
if Setting.bcc_recipients? if Setting.bcc_recipients?
@@ -237,22 +217,7 @@ class Mailer < ActionMailer::Base
def render_message(method_name, body) def render_message(method_name, body)
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' 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) 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 end
# Makes partial rendering work with Rails 1.2 (retro-compatibility) # Makes partial rendering work with Rails 1.2 (retro-compatibility)
+3 -21
View File
@@ -19,11 +19,11 @@ class Message < ActiveRecord::Base
belongs_to :board belongs_to :board
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' 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_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' belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'], acts_as_searchable :columns => ['subject', 'content'],
:include => {:board => :project}, :include => {:board, :project},
:project_key => 'project_id', :project_key => 'project_id',
:date_column => "#{table_name}.created_on" :date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, 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} : :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}"})} {:id => o.parent_id, :anchor => "message-#{o.id}"})}
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}, acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}
:author_key => :author_id
acts_as_watchable
attr_protected :locked, :sticky attr_protected :locked, :sticky
validates_presence_of :subject, :content validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255 validates_length_of :subject, :maximum => 255
after_create :add_author_as_watcher
def validate_on_create def validate_on_create
# Can not reply to a locked topic # Can not reply to a locked topic
errors.add_to_base 'Topic is locked' if root.locked? && self != root errors.add_to_base 'Topic is locked' if root.locked? && self != root
@@ -72,18 +68,4 @@ class Message < ActiveRecord::Base
def project def project
board.project board.project
end 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 end
+2 -3
View File
@@ -17,9 +17,8 @@
class MessageObserver < ActiveRecord::Observer class MessageObserver < ActiveRecord::Observer
def after_create(message) def after_create(message)
recipients = [] # send notification to the authors of the thread
# send notification to the topic watchers recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?}
recipients += message.root.watcher_recipients
# send notification to the board watchers # send notification to the board watchers
recipients += message.board.watcher_recipients recipients += message.board.watcher_recipients
# send notification to project members who want to be notified # send notification to project members who want to be notified
+5 -6
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang # Copyright (C) 2006 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -26,11 +26,10 @@ class News < ActiveRecord::Base
acts_as_searchable :columns => ['title', "#{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_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]}, acts_as_activity_provider :find_options => {:include => [:project, :author]}
:author_key => :author_id
# returns latest news for projects visible by user # returns latest news for projects visible by user
def self.latest(user = User.current, count = 5) def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => Project.allowed_to_condition(user, :view_news), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
end end
end end
+2 -18
View File
@@ -44,8 +44,6 @@ class Project < ActiveRecord::Base
:association_foreign_key => 'custom_field_id' :association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true acts_as_tree :order => "name", :counter_cache => true
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files
acts_as_customizable acts_as_customizable
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
@@ -60,13 +58,11 @@ class Project < ActiveRecord::Base
validates_associated :repository, :wiki validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30 validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 255 validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 2..20 validates_length_of :identifier, :in => 3..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members 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) def identifier=(identifier)
super unless identifier_frozen? super unless identifier_frozen?
end end
@@ -110,12 +106,6 @@ class Project < ActiveRecord::Base
def self.allowed_to_condition(user, permission, options={}) def self.allowed_to_condition(user, permission, options={})
statements = [] statements = []
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if perm = Redmine::AccessControl.permission(permission)
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] if options[:project]
project_statement = "#{Project.table_name}.id = #{options[:project].id}" project_statement = "#{Project.table_name}.id = #{options[:project].id}"
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects] 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 # Returns an array of all custom fields enabled for project issues
# (explictly associated custom fields and custom fields enabled for all projects) # (explictly associated custom fields and custom fields enabled for all projects)
def all_issue_custom_fields 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 end
def project def project
@@ -249,12 +239,6 @@ class Project < ActiveRecord::Base
end end
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 protected
def validate def validate
errors.add(parent_id, " must be a root project") if parent and parent.parent errors.add(parent_id, " must be a root project") if parent and parent.parent
+53 -76
View File
@@ -1,5 +1,5 @@
# Redmine - project management software # redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang # Copyright (C) 2006-2007 Jean-Philippe Lang
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
@@ -255,7 +255,8 @@ class Query < ActiveRecord::Base
column_names.nil? || column_names.empty? column_names.nil? || column_names.empty?
end end
def project_statement def statement
# project/subprojects clause
project_clauses = [] project_clauses = []
if project && !@project.active_children.empty? if project && !@project.active_children.empty?
ids = [project.id] ids = [project.id]
@@ -273,15 +274,12 @@ class Query < ActiveRecord::Base
elsif Setting.display_subprojects_issues? elsif Setting.display_subprojects_issues?
ids += project.child_ids ids += project.child_ids
end end
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
elsif project elsif project
project_clauses << "#{Project.table_name}.id = %d" % project.id project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
end end
project_clauses << Project.allowed_to_condition(User.current, :view_issues) project_clauses << Project.visible_by(User.current)
project_clauses.join(' AND ')
end
def statement
# filters clauses # filters clauses
filters_clauses = [] filters_clauses = []
filters.each_key do |field| filters.each_key do |field|
@@ -309,69 +307,60 @@ class Query < ActiveRecord::Base
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me") v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
end 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 << ')' sql << ')'
filters_clauses << sql filters_clauses << sql
end if filters and valid? end if filters and valid?
(filters_clauses << project_statement).join(' AND ') (project_clauses + filters_clauses).join(' AND ')
end end
private 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 = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
when "!~"
sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
end
return sql
end
def add_custom_fields_filters(custom_fields) def add_custom_fields_filters(custom_fields)
@available_filters ||= {} @available_filters ||= {}
@@ -391,16 +380,4 @@ class Query < ActiveRecord::Base
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
end end
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 end
+1 -41
View File
@@ -78,7 +78,7 @@ class Repository < ActiveRecord::Base
# Default behaviour: we search in cached changesets # Default behaviour: we search in cached changesets
def changesets_for_path(path) def changesets_for_path(path)
path = "/#{path}" unless path.starts_with?('/') path = "/#{path}" unless path.starts_with?('/')
Change.find(:all, :include => {:changeset => :user}, Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path], :conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset) :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
end end
@@ -96,45 +96,6 @@ class Repository < ActiveRecord::Base
self.changesets.each(&:scan_comment_for_issue_ids) self.changesets.each(&:scan_comment_for_issue_ids)
end 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 # fetch new changesets for all repositories
# can be called periodically by an external script # can be called periodically by an external script
# eg. ruby script/runner "Repository.fetch_changesets" # eg. ruby script/runner "Repository.fetch_changesets"
@@ -173,7 +134,6 @@ class Repository < ActiveRecord::Base
def clear_changesets 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 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}") connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}")
end end
end end
+1 -1
View File
@@ -109,7 +109,7 @@ class Repository::Cvs < Repository
cs = changesets.find(:first, :conditions=>{ cs = changesets.find(:first, :conditions=>{
:committed_on=>revision.time-time_delta..revision.time+time_delta, :committed_on=>revision.time-time_delta..revision.time+time_delta,
:committer=>revision.author, :committer=>revision.author,
:comments=>Changeset.normalize_comments(revision.message) :comments=>revision.message
}) })
# create a new changeset.... # create a new changeset....
-10
View File
@@ -28,11 +28,6 @@ class Repository::Darcs < Repository
'Darcs' 'Darcs'
end 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) def entries(path=nil, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, patch.nil? ? nil : patch.scmid) entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
@@ -51,11 +46,6 @@ class Repository::Darcs < Repository
entries entries
end 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) def diff(path, rev, rev_to)
patch_from = changesets.find_by_revision(rev) patch_from = changesets.find_by_revision(rev)
return nil if patch_from.nil? return nil if patch_from.nil?
+14 -16
View File
@@ -30,7 +30,7 @@ class Repository::Git < Repository
end end
def changesets_for_path(path) def changesets_for_path(path)
Change.find(:all, :include => {:changeset => :user}, Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path], :conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset) :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
end end
@@ -45,22 +45,20 @@ class Repository::Git < Repository
unless changesets.find_by_scmid(scm_revision) unless changesets.find_by_scmid(scm_revision)
scm.revisions('', db_revision, nil, :reverse => true) do |revision| scm.revisions('', db_revision, nil, :reverse => true) do |revision|
if changesets.find_by_scmid(revision.scmid.to_s).nil? transaction do
transaction do changeset = Changeset.create(:repository => self,
changeset = Changeset.create!(:repository => self, :revision => revision.identifier,
:revision => revision.identifier, :scmid => revision.scmid,
:scmid => revision.scmid, :committer => revision.author,
:committer => revision.author, :committed_on => revision.time,
:committed_on => revision.time, :comments => revision.message)
:comments => revision.message)
revision.paths.each do |change| revision.paths.each do |change|
Change.create!(:changeset => changeset, Change.create(:changeset => changeset,
:action => change[:action], :action => change[:action],
:path => change[:path], :path => change[:path],
:from_path => change[:from_path], :from_path => change[:from_path],
:from_revision => change[:from_revision]) :from_revision => change[:from_revision])
end
end end
end end
end end
+1 -1
View File
@@ -32,7 +32,7 @@ class Repository::Subversion < Repository
def changesets_for_path(path) def changesets_for_path(path)
revisions = scm.revisions(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 end
# Returns a path relative to the url of the repository # Returns a path relative to the url of the repository
+4 -32
View File
@@ -20,20 +20,15 @@ class Role < ActiveRecord::Base
BUILTIN_NON_MEMBER = 1 BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2 BUILTIN_ANONYMOUS = 2
named_scope :builtin, lambda { |*args|
compare = 'not' if args.first == true
{ :conditions => "#{compare} builtin = 0" }
}
before_destroy :check_deletable before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all do has_many :workflows, :dependent => :delete_all do
def copy(role) def copy(role)
raise "Can not copy workflow from a #{role.class}" unless role.is_a?(Role) 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? raise "Can not copy workflow from/to an unsaved role" if proxy_owner.new_record? || role.new_record?
clear 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}" + " SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
" FROM #{Workflow.table_name}" + " FROM workflows" +
" WHERE role_id = #{role.id}" " WHERE role_id = #{role.id}"
end end
end end
@@ -41,7 +36,7 @@ class Role < ActiveRecord::Base
has_many :members has_many :members
acts_as_list acts_as_list
serialize :permissions, Array serialize :permissions
attr_protected :builtin attr_protected :builtin
validates_presence_of :name validates_presence_of :name
@@ -54,33 +49,10 @@ class Role < ActiveRecord::Base
end end
def permissions=(perms) 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) write_attribute(:permissions, perms)
end 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) def <=>(role)
position <=> role.position position <=> role.position
end end
+2 -41
View File
@@ -34,50 +34,11 @@ class Setting < ActiveRecord::Base
'%I:%M %p' '%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 cattr_accessor :available_settings
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) @@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 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 end
validates_uniqueness_of :name validates_uniqueness_of :name
+2 -2
View File
@@ -32,7 +32,7 @@ class TimeEntry < ActiveRecord::Base
:description => :comments :description => :comments
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true, :message => :activerecord_error_invalid validates_numericality_of :hours, :allow_nil => true
validates_length_of :comments, :maximum => 255, :allow_nil => true validates_length_of :comments, :maximum => 255, :allow_nil => true
def after_initialize def after_initialize
@@ -54,7 +54,7 @@ class TimeEntry < ActiveRecord::Base
end end
def hours=(h) 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 end
# tyear, tmonth, tweek assigned where setting spent_on attributes # tyear, tmonth, tweek assigned where setting spent_on attributes
+2 -2
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 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? raise "Can not copy workflow from/to an unsaved tracker" if proxy_owner.new_record? || tracker.new_record?
clear 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" + " SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
" FROM #{Workflow.table_name}" + " FROM workflows" +
" WHERE tracker_id = #{tracker.id}" " WHERE tracker_id = #{tracker.id}"
end end
end end
+22 -23
View File
@@ -33,18 +33,13 @@ class User < ActiveRecord::Base
:username => '#{login}' :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 :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 :members, :dependent => :delete_all
has_many :projects, :through => :memberships has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify 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 :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source belongs_to :auth_source
# Active non-anonymous users scope
named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
acts_as_customizable acts_as_customizable
attr_accessor :password, :password_confirmation attr_accessor :password, :password_confirmation
@@ -75,9 +70,16 @@ class User < ActiveRecord::Base
self.hashed_password = User.hash_password(self.password) if self.password self.hashed_password = User.hash_password(self.password) if self.password
end end
def reload(*args) def self.active
@name = nil with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
super yield
end
end
def self.find_active(*args)
active do
find(*args)
end
end end
# Returns the user that matches provided login and password, or nil # 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 # Return user's full name for display
def name(formatter = nil) def name(formatter = nil)
if formatter f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"') eval '"' + f + '"'
else
@name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
end
end end
def active? def active?
@@ -144,7 +143,7 @@ class User < ActiveRecord::Base
end end
def time_zone 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 end
def wants_comments_in_reverse_order? def wants_comments_in_reverse_order?
@@ -179,14 +178,14 @@ class User < ActiveRecord::Base
token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil token && (token.created_on > Setting.autologin.to_i.day.ago) && token.user.active? ? token.user : nil
end end
# Makes find_by_mail case-insensitive
def self.find_by_mail(mail)
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
end
# Sort users by their display names
def <=>(user) 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 end
def to_s def to_s
@@ -243,7 +242,7 @@ class User < ActiveRecord::Base
elsif options[:global] elsif options[:global]
# authorize if user has at least one role that has this permission # authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.role}.uniq 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 else
false false
end end
+2 -3
View File
@@ -19,13 +19,12 @@ class Version < ActiveRecord::Base
before_destroy :check_integrity before_destroy :check_integrity
belongs_to :project belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id' has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
acts_as_attachable :view_permission => :view_files, has_many :attachments, :as => :container, :dependent => :destroy
:delete_permission => :manage_files
validates_presence_of :name validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id] validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60 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 def start_date
effective_date effective_date
-7
View File
@@ -19,12 +19,5 @@ class Watcher < ActiveRecord::Base
belongs_to :watchable, :polymorphic => true belongs_to :watchable, :polymorphic => true
belongs_to :user belongs_to :user
validates_presence_of :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] 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 end
-19
View File
@@ -43,25 +43,6 @@ class Wiki < ActiveRecord::Base
page page
end 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 # turn a string into a valid page title
def self.titleize(title) def self.titleize(title)
# replace spaces with _ and remove unwanted caracters # replace spaces with _ and remove unwanted caracters
+2 -3
View File
@@ -35,10 +35,9 @@ class WikiContent < ActiveRecord::Base
:type => 'wiki-page', :type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} :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", :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
:author_key => "#{WikiContent.versioned_table_name}.author_id", :permission => :view_wiki_pages,
:permission => :view_wiki_edits,
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + :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}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
+1 -5
View File
@@ -21,7 +21,7 @@ require 'enumerator'
class WikiPage < ActiveRecord::Base class WikiPage < ActiveRecord::Base
belongs_to :wiki belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy 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_tree :order => 'title'
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
@@ -112,10 +112,6 @@ class WikiPage < ActiveRecord::Base
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
end end
def attachments_deletable?(usr=User.current)
editable_by?(usr) && super(usr)
end
def parent_title def parent_title
@parent_title || (self.parent && self.parent.pretty_title) @parent_title || (self.parent && self.parent.pretty_title)
end end
-19
View File
@@ -21,23 +21,4 @@ class Workflow < ActiveRecord::Base
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id' belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
validates_presence_of :role, :old_status, :new_status 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 end
+10 -51
View File
@@ -1,24 +1,16 @@
<div class="contextual"> <h2><%=h @user.name %></h2>
<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>
</div>
<h2><%= avatar @user %> <%=h @user.name %></h2> <p>
<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>
<div class="splitcontentleft">
<ul> <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.empty? %>
<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> <li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
<% unless @user.last_login_on.nil? %> <% for custom_value in @custom_values %>
<li><%=l(:field_last_login_on)%>: <%= format_date(@user.last_login_on) %></li> <% if !custom_value.value.empty? %>
<% end %> <li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
<% end %>
<% end %>
</ul> </ul>
</p>
<% unless @memberships.empty? %> <% unless @memberships.empty? %>
<h3><%=l(:label_project_plural)%></h3> <h3><%=l(:label_project_plural)%></h3>
@@ -29,41 +21,8 @@
<% end %> <% end %>
</ul> </ul>
<% end %> <% end %>
</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> <p>
<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %> <%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
</p> </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 %>
</div>
<% html_title @user.name %>
+1 -11
View File
@@ -19,7 +19,7 @@
<p class="icon22 icon22-tracker"> <p class="icon22 icon22-tracker">
<%= link_to l(:label_tracker_plural), :controller => 'trackers' %> | <%= link_to l(:label_tracker_plural), :controller => 'trackers' %> |
<%= link_to l(:label_issue_status_plural), :controller => 'issue_statuses' %> | <%= 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>
<p class="icon22 icon22-workflow"> <p class="icon22 icon22-workflow">
@@ -34,16 +34,6 @@
<%= link_to l(:label_settings), :controller => 'settings' %> <%= link_to l(:label_settings), :controller => 'settings' %>
</p> </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"> <p class="icon22 icon22-info">
<%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %> <%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %>
</p> </p>
+17 -2
View File
@@ -4,9 +4,24 @@
<table class="list"> <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="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_file_repository_writable) %></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="odd"><td><%= l(:text_rmagick_available) %></td><td><%= image_tag (@flags[:rmagick_available] ? '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> </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)) -%> <% html_title(l(:label_information_plural)) -%>
-19
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 %>
+2 -4
View File
@@ -4,13 +4,11 @@
<h2><%=l(:label_project_plural)%></h2> <h2><%=l(:label_project_plural)%></h2>
<% form_tag({}, :method => :get) do %> <% form_tag() do %>
<fieldset><legend><%= l(:label_filter_plural) %></legend> <fieldset><legend><%= l(:label_filter_plural) %></legend>
<label><%= l(:field_status) %> :</label> <label><%= l(:field_status) %> :</label>
<%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
<label><%= l(:label_project) %>:</label> <%= submit_tag l(:button_apply), :class => "small" %>
<%= text_field_tag 'name', params[:name], :size => 30 %>
<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
</fieldset> </fieldset>
<% end %> <% end %>
&nbsp; &nbsp;
+3 -3
View File
@@ -3,14 +3,14 @@
<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%> <p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
<%= h(" - #{attachment.description}") unless attachment.description.blank? %> <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span> <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
<% if options[:deletable] %> <% if options[:delete_url] %>
<%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment}, <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
:confirm => l(:text_are_you_sure), :confirm => l(:text_are_you_sure),
:method => :post, :method => :post,
:class => 'delete', :class => 'delete',
:title => l(:button_delete) %> :title => l(:button_delete) %>
<% end %> <% end %>
<% if options[:author] %> <% unless options[:no_author] %>
<span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span> <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
<% end %> <% end %>
</p> </p>
+1 -6
View File
@@ -9,7 +9,6 @@
<th><%=l(:field_name)%></th> <th><%=l(:field_name)%></th>
<th><%=l(:field_type)%></th> <th><%=l(:field_type)%></th>
<th><%=l(:field_host)%></th> <th><%=l(:field_host)%></th>
<th><%=l(:label_user_plural)%></th>
<th></th> <th></th>
<th></th> <th></th>
</tr></thead> </tr></thead>
@@ -19,12 +18,8 @@
<td><%= link_to source.name, :action => 'edit', :id => source%></td> <td><%= link_to source.name, :action => 'edit', :id => source%></td>
<td align="center"><%= source.auth_method_name %></td> <td align="center"><%= source.auth_method_name %></td>
<td align="center"><%= source.host %></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"><%= link_to l(:button_test), :action => 'test_connection', :id => source %></td>
<td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, <td align="center"><%= button_to l(:button_delete), { :action => 'destroy', :id => source }, :confirm => l(:text_are_you_sure), :class => "button-small" %></td>
:confirm => l(:text_are_you_sure),
:class => "button-small",
:disabled => source.users.any? %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
+3 -4
View File
@@ -26,16 +26,15 @@
</div> </div>
<h2><%=h @board.name %></h2> <h2><%=h @board.name %></h2>
<p class="subtitle"><%=h @board.description %></p>
<% if @topics.any? %> <% if @topics.any? %>
<table class="list messages"> <table class="list messages">
<thead><tr> <thead><tr>
<th><%= l(:field_subject) %></th> <th><%= l(:field_subject) %></th>
<th><%= l(:field_author) %></th> <th><%= l(:field_author) %></th>
<%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
<%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %> <th><%= l(:label_reply_plural) %></th>
<%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %> <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
</tr></thead> </tr></thead>
<tbody> <tbody>
<% @topics.each do |topic| %> <% @topics.each do |topic| %>
+1 -4
View File
@@ -1,5 +1,4 @@
<% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%> <% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%>
<% diff.each do |table_file| -%>
<div class="autoscroll"> <div class="autoscroll">
<% if diff_type == 'sbs' -%> <% if diff_type == 'sbs' -%>
<table class="filecontent CodeRay"> <table class="filecontent CodeRay">
@@ -63,5 +62,3 @@
</div> </div>
<% end -%> <% end -%>
<%= l(:text_diff_truncated) if diff.truncated? %>
+1 -1
View File
@@ -3,7 +3,7 @@
<tbody> <tbody>
<% line_num = 1 %> <% line_num = 1 %>
<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %> <% 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 %> <% line_num += 1 %>
<% end %> <% end %>
</tbody> </tbody>
+1 -1
View File
@@ -6,7 +6,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
xml.id 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.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
xml.author { xml.name "#{Setting.app_title}" } 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| @items.each do |item|
xml.entry do xml.entry do
url = url_for(item.event_url(:only_path => false)) url = url_for(item.event_url(:only_path => false))
+1 -1
View File
@@ -12,7 +12,7 @@
</div> </div>
<h3><%= l(:label_attachment_plural) %></h3> <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') %> <% 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;", <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="changeset <%= cycle('odd', 'even') %>"> <div class="changeset <%= cycle('odd', 'even') %>">
<p><%= link_to("#{l(:label_revision)} #{changeset.revision}", <p><%= link_to("#{l(:label_revision)} #{changeset.revision}",
:controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %><br /> :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %><br />
<span class="author"><%= authoring(changeset.committed_on, changeset.author) %></span></p> <span class="author"><%= authoring(changeset.committed_on, changeset.committer) %></span></p>
<%= textilizable(changeset, :comments) %> <%= textilizable(changeset, :comments) %>
</div> </div>
<% end %> <% end %>
-1
View File
@@ -35,7 +35,6 @@
<fieldset><legend><%= l(:field_notes) %></legend> <fieldset><legend><%= l(:field_notes) %></legend>
<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
<%= wikitoolbar_for 'notes' %> <%= wikitoolbar_for 'notes' %>
<%= call_hook(:view_issues_edit_notes_bottom, { :issue => @issue, :notes => @notes, :form => f }) %>
<p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p> <p><%=l(:label_attachment_plural)%><br /><%= render :partial => 'attachments/form' %></p>
</fieldset> </fieldset>
+3 -13
View File
@@ -8,7 +8,7 @@
<div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>> <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>>
<p><%= f.text_field :subject, :size => 80, :required => true %></p> <p><%= f.text_field :subject, :size => 80, :required => true %></p>
<p><%= f.text_area :description, <p><%= f.text_area :description, :required => true,
:cols => 60, :cols => 60,
:rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
:accesskey => accesskey(:edit), :accesskey => accesskey(:edit),
@@ -24,13 +24,11 @@
<p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p> <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
<% unless @project.issue_categories.empty? %>
<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
<%= prompt_to_remote(l(:label_issue_category_new), <%= prompt_to_remote(l(:label_issue_category_new),
l(:label_issue_category_new), 'category[name]', l(:label_issue_category_new), 'category[name]',
{:controller => 'projects', :action => 'add_issue_category', :id => @project}, {:controller => 'projects', :action => 'add_issue_category', :id => @project},
:class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p> :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
<% end %>
<%= content_tag('p', f.select(:fixed_version_id, <%= content_tag('p', f.select(:fixed_version_id,
(@project.versions.sort.collect {|v| [v.name, v.id]}), (@project.versions.sort.collect {|v| [v.name, v.id]}),
{ :include_blank => true })) unless @project.versions.empty? %> { :include_blank => true })) unless @project.versions.empty? %>
@@ -44,20 +42,12 @@
</div> </div>
<div style="clear:both;"> </div> <div style="clear:both;"> </div>
<%= render :partial => 'form_custom_fields' %> <%= render :partial => 'form_custom_fields', :locals => {:values => @custom_values} %>
<% if @issue.new_record? %> <% if @issue.new_record? %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
<% end %> <% end %>
<% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%> <%= Redmine::Plugin::Hook::Manager.call_hook(:issue_edit, {:project => @project, :issue => @issue, :form => f }) %>
<p><label><%= l(:label_issue_watchers) %></label>
<% @issue.project.users.sort.each do |user| -%>
<label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label>
<% end -%>
</p>
<% end %>
<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %>
<%= wikitoolbar_for 'issue_description' %> <%= wikitoolbar_for 'issue_description' %>

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