Compare commits

..

1 Commits
0.7.2 ... 0.5.1

Author SHA1 Message Date
Jean-Philippe Lang
366d46c701 tagged version 0.5.1
git-svn-id: http://redmine.rubyforge.org/svn/tags/0.5.1@605 e93f8b46-1217-0410-a6f0-8f06a7374b81
2007-08-12 10:02:32 +00:00
1036 changed files with 12828 additions and 50878 deletions

View File

@@ -20,6 +20,6 @@ class SysApi < ActionWebService::API::Base
:expects => [],
:returns => [[Project]]
api_method :repository_created,
:expects => [:string, :string],
:expects => [:int, :string],
:returns => [:int]
end

View File

@@ -21,16 +21,17 @@ class AccountController < ApplicationController
include CustomFieldsHelper
# prevents login action to be filtered by check_if_login_required application scope filter
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register, :activate]
skip_before_filter :check_if_login_required, :only => [:login, :lost_password, :register]
before_filter :require_login, :only => :logout
# Show user's account
def show
@user = User.find_active(params[:id])
@user = User.find(params[:id])
@custom_values = @user.custom_values.find(:all, :include => :custom_field)
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
membership.project.is_public? || (User.current.member_of?(membership.project))
membership.project.is_public? || (logged_in_user && logged_in_user.role_for_project(membership.project))
end
rescue ActiveRecord::RecordNotFound
render_404
@@ -40,12 +41,12 @@ class AccountController < ApplicationController
def login
if request.get?
# Logout user
self.logged_user = nil
self.logged_in_user = nil
else
# Authenticate user
user = User.try_to_login(params[:username], params[:password])
user = User.try_to_login(params[:login], params[:password])
if user
self.logged_user = user
self.logged_in_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
token = Token.create(:user => user, :action => 'autologin')
@@ -53,27 +54,25 @@ class AccountController < ApplicationController
end
redirect_back_or_default :controller => 'my', :action => 'page'
else
flash.now[:error] = l(:notice_account_invalid_creditentials)
flash.now[:notice] = l(:notice_account_invalid_creditentials)
end
end
rescue User::OnTheFlyCreationFailure
flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.'
end
# Log out current user and redirect to welcome page
def logout
cookies.delete :autologin
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) if User.current.logged?
self.logged_user = nil
redirect_to home_url
Token.delete_all(["user_id = ? AND action = ?", logged_in_user.id, "autologin"]) if logged_in_user
self.logged_in_user = nil
redirect_to :controller => 'welcome'
end
# Enable user to choose a new password
def lost_password
redirect_to(home_url) && return unless Setting.lost_password?
redirect_to :controller => 'welcome' and return unless Setting.lost_password?
if params[:token]
@token = Token.find_by_action_and_value("recovery", params[:token])
redirect_to(home_url) && return unless @token and !@token.expired?
redirect_to :controller => 'welcome' and return unless @token and !@token.expired?
@user = @token.user
if request.post?
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
@@ -90,9 +89,9 @@ class AccountController < ApplicationController
if request.post?
user = User.find_by_mail(params[:mail])
# user not found in db
flash.now[:error] = l(:notice_account_unknown_email) and return unless user
flash.now[:notice] = l(:notice_account_unknown_email) and return unless user
# user uses an external authentification
flash.now[:error] = l(:notice_can_t_change_password) and return if user.auth_source_id
flash.now[:notice] = l(:notice_can_t_change_password) and return if user.auth_source_id
# create a new token for password recovery
token = Token.new(:user => user, :action => "recovery")
if token.save
@@ -107,71 +106,38 @@ class AccountController < ApplicationController
# User self-registration
def register
redirect_to(home_url) && return unless Setting.self_registration?
if request.get?
@user = User.new(:language => Setting.default_language)
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
redirect_to :controller => 'welcome' and return unless Setting.self_registration?
if params[:token]
token = Token.find_by_action_and_value("register", params[:token])
redirect_to :controller => 'welcome' and return unless token and !token.expired?
user = token.user
redirect_to :controller => 'welcome' and return unless user.status == User::STATUS_REGISTERED
user.status = User::STATUS_ACTIVE
if user.save
token.destroy
flash[:notice] = l(:notice_account_activated)
redirect_to :action => 'login'
return
end
else
@user = User.new(params[:user])
@user.admin = false
@user.login = params[:user][:login]
@user.status = User::STATUS_REGISTERED
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x,
:customized => @user,
:value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) }
@user.custom_values = @custom_values
case Setting.self_registration
when '1'
# Email activation
if request.get?
@user = User.new(:language => Setting.default_language)
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
@user = User.new(params[:user])
@user.admin = false
@user.login = params[:user][:login]
@user.status = User::STATUS_REGISTERED
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@user.custom_values = @custom_values
token = Token.new(:user => @user, :action => "register")
if @user.save and token.save
Mailer.deliver_register(token)
flash[:notice] = l(:notice_account_register_done)
redirect_to :action => 'login'
end
when '3'
# Automatic activation
@user.status = User::STATUS_ACTIVE
if @user.save
flash[:notice] = l(:notice_account_activated)
redirect_to :action => 'login'
end
else
# Manual activation by the administrator
if @user.save
# Sends an email to the administrators
Mailer.deliver_account_activation_request(@user)
flash[:notice] = l(:notice_account_pending)
redirect_to :action => 'login'
redirect_to :controller => 'welcome' and return
end
end
end
end
# Token based account activation
def activate
redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
token = Token.find_by_action_and_value('register', params[:token])
redirect_to(home_url) && return unless token and !token.expired?
user = token.user
redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED
user.status = User::STATUS_ACTIVE
if user.save
token.destroy
flash[:notice] = l(:notice_account_activated)
end
redirect_to :action => 'login'
end
private
def logged_user=(user)
if user && user.is_a?(User)
User.current = user
session[:user_id] = user.id
else
User.current = User.anonymous
session[:user_id] = nil
end
end
end

View File

@@ -22,8 +22,7 @@ class AdminController < ApplicationController
helper :sort
include SortHelper
def index
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
def index
end
def projects
@@ -36,7 +35,7 @@ class AdminController < ApplicationController
@project_count = Project.count(:conditions => conditions)
@project_pages = Paginator.new self, @project_count,
per_page_option,
25,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => conditions,
@@ -45,42 +44,23 @@ class AdminController < ApplicationController
render :action => "projects", :layout => false if request.xhr?
end
# Loads the default configuration
# (roles, trackers, statuses, workflow, enumerations)
def default_configuration
def mail_options
@actions = Permission.find(:all, :conditions => ["mail_option=?", true]) || []
if request.post?
begin
Redmine::DefaultData::Loader::load(params[:lang])
flash[:notice] = l(:notice_default_data_loaded)
rescue Exception => e
flash[:error] = l(:error_can_t_load_default_data, e.message)
end
@actions.each { |a|
a.mail_enabled = (params[:action_ids] || []).include? a.id.to_s
a.save
}
flash.now[:notice] = l(:notice_successful_update)
end
redirect_to :action => 'index'
end
def test_email
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
# Force ActionMailer to raise delivery errors so we can catch it
ActionMailer::Base.raise_delivery_errors = true
begin
@test = Mailer.deliver_test(User.current)
flash[:notice] = l(:notice_email_sent, User.current.mail)
rescue Exception => e
flash[:error] = l(:notice_email_error, e.message)
end
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
end
def info
@db_adapter_name = ActiveRecord::Base.connection.adapter_name
@flags = {
:default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
:file_repository_writable => File.writable?(Attachment.storage_path),
:rmagick_available => Object.const_defined?(:Magick)
}
@plugins = Redmine::Plugin.registered_plugins
@flags = Hash.new
@flags[:default_admin_changed] = User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?
@flags[:file_repository_writable] = File.writable?(Attachment.storage_path)
@flags[:textile_available] = ActionView::Helpers::TextHelper.method_defined? "textilize"
end
end

View File

@@ -16,57 +16,52 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ApplicationController < ActionController::Base
before_filter :user_setup, :check_if_login_required, :set_localization
before_filter :check_if_login_required, :set_localization
filter_parameter_logging :password
include Redmine::MenuManager::MenuController
helper Redmine::MenuManager::MenuHelper
REDMINE_SUPPORTED_SCM.each do |scm|
require_dependency "repository/#{scm.underscore}"
end
def current_role
@current_role ||= User.current.role_for_project(@project)
def logged_in_user=(user)
@logged_in_user = user
session[:user_id] = (user ? user.id : nil)
end
def user_setup
# Check the settings cache for each request
Setting.check_cache
# Find the current user
User.current = find_current_user
end
# Returns the current user or nil if no user is logged in
def find_current_user
def logged_in_user
if session[:user_id]
# existing session
(User.find_active(session[:user_id]) rescue nil)
elsif cookies[:autologin] && Setting.autologin?
# auto-login feature
User.find_by_autologin_key(cookies[:autologin])
elsif params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication
User.find_by_rss_key(params[:key])
@logged_in_user ||= User.find(session[:user_id])
else
nil
end
end
# Returns the role that the logged in user has on the current project
# or nil if current user is not a member of the project
def logged_in_user_membership
@user_membership ||= logged_in_user.role_for_project(@project)
end
# check if login is globally required to access the application
def check_if_login_required
# no check needed if user is already logged in
return true if User.current.logged?
return true if logged_in_user
# auto-login feature
autologin_key = cookies[:autologin]
if autologin_key && Setting.autologin?
self.logged_in_user = User.find_by_autologin_key(autologin_key)
end
require_login if Setting.login_required?
end
def set_localization
User.current.language = nil unless User.current.logged?
lang = begin
if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
User.current.language
if self.logged_in_user and self.logged_in_user.language and !self.logged_in_user.language.empty? and GLoc.valid_languages.include? self.logged_in_user.language.to_sym
self.logged_in_user.language
elsif request.env['HTTP_ACCEPT_LANGUAGE']
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
User.current.language = accept_lang
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
accept_lang
end
end
rescue
@@ -76,7 +71,7 @@ class ApplicationController < ActionController::Base
end
def require_login
if !User.current.logged?
unless self.logged_in_user
store_location
redirect_to :controller => "account", :action => "login"
return false
@@ -86,33 +81,49 @@ class ApplicationController < ActionController::Base
def require_admin
return unless require_login
if !User.current.admin?
unless self.logged_in_user.admin?
render_403
return false
end
true
end
# Authorize the user for the requested action
# authorizes the user for the requested action.
def authorize(ctrl = params[:controller], action = params[:action])
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
allowed ? true : (User.current.logged? ? render_403 : require_login)
unless @project.active?
@project = nil
render_404
return false
end
# check if action is allowed on public projects
if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ ctrl, action ]
return true
end
# if action is not public, force login
return unless require_login
# admin is always authorized
return true if self.logged_in_user.admin?
# if not admin, check membership permission
if logged_in_user_membership and Permission.allowed_to_role( "%s/%s" % [ ctrl, action ], logged_in_user_membership )
return true
end
render_403
false
end
# make sure that the user is a member of the project (or admin) if project is private
# used as a before_filter for actions that do not require any particular permission on the project
def check_project_privacy
if @project && @project.active?
if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
true
else
User.current.logged? ? render_403 : require_login
end
else
unless @project.active?
@project = nil
render_404
false
return false
end
return true if @project.is_public?
return false unless logged_in_user
return true if logged_in_user.admin? || logged_in_user_membership
render_403
false
end
# store current uri in session.
@@ -132,69 +143,17 @@ class ApplicationController < ActionController::Base
end
def render_403
@html_title = "403"
@project = nil
render :template => "common/403", :layout => !request.xhr?, :status => 403
render :template => "common/403", :layout => true, :status => 403
return false
end
def render_404
render :template => "common/404", :layout => !request.xhr?, :status => 404
@html_title = "404"
render :template => "common/404", :layout => true, :status => 404
return false
end
def render_error(msg)
flash.now[:error] = msg
render :nothing => true, :layout => !request.xhr?, :status => 500
end
def render_feed(items, options={})
@items = items || []
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
@items = @items.slice(0, Setting.feeds_limit.to_i)
@title = options[:title] || Setting.app_title
render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
end
def self.accept_key_auth(*actions)
actions = actions.flatten.map(&:to_s)
write_inheritable_attribute('accept_key_auth_actions', actions)
end
def accept_key_auth_actions
self.class.read_inheritable_attribute('accept_key_auth_actions') || []
end
# TODO: move to model
def attach_files(obj, attachments)
attached = []
if attachments && attachments.is_a?(Hash)
attachments.each_value do |attachment|
file = attachment['file']
next unless file && file.size > 0
a = Attachment.create(:container => obj,
:file => file,
:description => attachment['description'].to_s.strip,
:author => User.current)
attached << a unless a.new_record?
end
end
attached
end
# Returns the number of objects that should be displayed
# on the paginated list
def per_page_option
per_page = nil
if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
per_page = params[:per_page].to_s.to_i
session[:per_page] = per_page
elsif session[:per_page]
per_page = session[:per_page]
else
per_page = Setting.per_page_options_array.first || 25
end
per_page
end
# qvalues http header parser
# code taken from webrick
@@ -214,9 +173,4 @@ class ApplicationController < ActionController::Base
end
return tmp
end
# Returns a string that can be used as filename value in Content-Disposition header
def filename_for_content_disposition(name)
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
end
end
end

View File

@@ -19,13 +19,18 @@ class AttachmentsController < ApplicationController
layout 'base'
before_filter :find_project, :check_project_privacy
# sends an attachment
def download
# images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
send_file @attachment.diskfile, :filename => @attachment.filename
rescue
render_404
end
# sends an image to be displayed inline
def show
render(:nothing => true, :status => 404) and return unless @attachment.diskfile =~ /\.(jpeg|jpg|gif|png)$/i
send_file @attachment.diskfile, :filename => @attachment.filename, :type => "image/#{$1}", :disposition => 'inline'
rescue
# in case the disk file was deleted
render_404
end

View File

@@ -65,10 +65,10 @@ class AuthSourcesController < ApplicationController
@auth_method = AuthSource.find(params[:id])
begin
@auth_method.test_connection
flash[:notice] = l(:notice_successful_connection)
rescue => text
flash[:error] = "Unable to connect (#{text})"
flash[:notice] = text
end
flash[:notice] ||= l(:notice_successful_connection)
redirect_to :action => 'list'
end

View File

@@ -17,7 +17,9 @@
class BoardsController < ApplicationController
layout 'base'
before_filter :find_project, :authorize
before_filter :find_project
before_filter :authorize, :except => [:index, :show]
before_filter :check_project_privacy, :only => [:index, :show]
helper :messages
include MessagesHelper
@@ -32,6 +34,7 @@ class BoardsController < ApplicationController
if @boards.size == 1
@board = @boards.first
show
render :action => 'show'
end
end
@@ -40,12 +43,12 @@ class BoardsController < ApplicationController
sort_update
@topic_count = @board.topics.count
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
@topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}",
@topic_pages = Paginator.new self, @topic_count, 25, params['page']
@topics = @board.topics.find :all, :order => sort_clause,
:include => [:author, {:last_reply => :author}],
:limit => @topic_pages.items_per_page,
:offset => @topic_pages.current.offset
render :action => 'show', :layout => !request.xhr?
render :action => 'show', :layout => false if request.xhr?
end
verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }

View File

@@ -25,7 +25,7 @@ class CustomFieldsController < ApplicationController
end
def list
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name }
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.type.to_s }
@tab = params[:tab] || 'IssueCustomField'
render :action => "list", :layout => false if request.xhr?
end
@@ -45,7 +45,7 @@ class CustomFieldsController < ApplicationController
end
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'list', :tab => @custom_field.type
end
@trackers = Tracker.find(:all, :order => 'position')
end
@@ -57,31 +57,16 @@ class CustomFieldsController < ApplicationController
@custom_field.trackers = params[:tracker_ids] ? Tracker.find(params[:tracker_ids]) : []
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'list', :tab => @custom_field.type
end
@trackers = Tracker.find(:all, :order => 'position')
end
def move
@custom_field = CustomField.find(params[:id])
case params[:position]
when 'highest'
@custom_field.move_to_top
when 'higher'
@custom_field.move_higher
when 'lower'
@custom_field.move_lower
when 'lowest'
@custom_field.move_to_bottom
end if params[:position]
redirect_to :action => 'list', :tab => @custom_field.class.name
end
def destroy
@custom_field = CustomField.find(params[:id]).destroy
redirect_to :action => 'list', :tab => @custom_field.class.name
redirect_to :action => 'list', :tab => @custom_field.type
rescue
flash[:error] = "Unable to delete custom field"
flash[:notice] = "Unable to delete custom field"
redirect_to :action => 'list'
end
end

View File

@@ -17,42 +17,12 @@
class DocumentsController < ApplicationController
layout 'base'
before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new]
before_filter :authorize
helper :attachments
def index
@sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
documents = @project.documents.find :all, :include => [:attachments, :category]
case @sort_by
when 'date'
@grouped = documents.group_by {|d| d.created_on.to_date }
when 'title'
@grouped = documents.group_by {|d| d.title.first.upcase}
when 'author'
@grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
else
@grouped = documents.group_by(&:category)
end
render :layout => false if request.xhr?
end
before_filter :find_project, :authorize
def show
@attachments = @document.attachments.find(:all, :order => "created_on DESC")
end
def new
@document = @project.documents.build(params[:document])
if request.post? and @document.save
attach_files(@document, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
redirect_to :action => 'index', :project_id => @project
end
end
def edit
@categories = Enumeration::get_values('DCAT')
if request.post? and @document.update_attributes(params[:document])
@@ -63,21 +33,26 @@ class DocumentsController < ApplicationController
def destroy
@document.destroy
redirect_to :controller => 'documents', :action => 'index', :project_id => @project
redirect_to :controller => 'projects', :action => 'list_documents', :id => @project
end
def download
@attachment = @document.attachments.find(params[:attachment_id])
@attachment.increment_download
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type
send_file @attachment.diskfile, :filename => @attachment.filename
rescue
render_404
end
def add_attachment
attachments = attach_files(@document, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
# Save the attachments
@attachments = []
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @document, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @document
end
@@ -88,15 +63,9 @@ class DocumentsController < ApplicationController
private
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_document
@document = Document.find(params[:id])
@project = @document.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
end

View File

@@ -59,27 +59,12 @@ class EnumerationsController < ApplicationController
end
end
def move
@enumeration = Enumeration.find(params[:id])
case params[:position]
when 'highest'
@enumeration.move_to_top
when 'higher'
@enumeration.move_higher
when 'lower'
@enumeration.move_lower
when 'lowest'
@enumeration.move_to_bottom
end if params[:position]
redirect_to :action => 'index'
end
def destroy
Enumeration.find(params[:id]).destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :action => 'list'
rescue
flash[:error] = "Unable to delete enumeration"
flash[:notice] = "Unable to delete enumeration"
redirect_to :action => 'list'
end
end

View File

@@ -0,0 +1,98 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# 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 FeedsController < ApplicationController
before_filter :find_scope
session :off
helper :issues
include IssuesHelper
helper :custom_fields
include CustomFieldsHelper
# news feeds
def news
News.with_scope(:find => @find_options) do
@news = News.find :all, :order => "#{News.table_name}.created_on DESC", :include => [ :author, :project ]
end
headers["Content-Type"] = "application/rss+xml"
render :action => 'news_atom' if 'atom' == params[:format]
end
# issue feeds
def issues
if @project && params[:query_id]
query = Query.find(params[:query_id])
query.executed_by = @user
# ignore query if it's not valid
query = nil unless query.valid?
# override with query conditions
@find_options[:conditions] = query.statement if query.valid? and @project == query.project
end
Issue.with_scope(:find => @find_options) do
@issues = Issue.find :all, :include => [:project, :author, :tracker, :status, :custom_values],
:order => "#{Issue.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (query ? query.name : l(:label_reported_issues))
headers["Content-Type"] = "application/rss+xml"
render :action => 'issues_atom' if 'atom' == params[:format]
end
# issue changes feeds
def history
if @project && params[:query_id]
query = Query.find(params[:query_id])
query.executed_by = @user
# ignore query if it's not valid
query = nil unless query.valid?
# override with query conditions
@find_options[:conditions] = query.statement if query.valid? and @project == query.project
end
Journal.with_scope(:find => @find_options) do
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status, :custom_values]} ],
:order => "#{Journal.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (query ? query.name : l(:label_reported_issues))
headers["Content-Type"] = "application/rss+xml"
render :action => 'history_atom' if 'atom' == params[:format]
end
private
# override for feeds specific authentication
def check_if_login_required
@user = User.find_by_rss_key(params[:key])
render(:nothing => true, :status => 403) and return false if !@user && Setting.login_required?
end
def find_scope
if params[:project_id]
# project feed
# check if project is public or if the user is a member
@project = Project.find(params[:project_id])
render(:nothing => true, :status => 403) and return false unless @project.is_public? || (@user && @user.role_for_project(@project))
scope = ["#{Project.table_name}.id=?", params[:project_id].to_i]
else
# global feed
scope = ["#{Project.table_name}.is_public=?", true]
end
@find_options = {:conditions => scope, :limit => Setting.feeds_limit.to_i}
return true
end
end

View File

@@ -0,0 +1,44 @@
# 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.
class HelpController < ApplicationController
skip_before_filter :check_if_login_required
before_filter :load_help_config
# displays help page for the requested controller/action
def index
# select help page to display
if params[:ctrl] and @help_config['pages'][params[:ctrl]]
if params[:page] and @help_config['pages'][params[:ctrl]][params[:page]]
template = @help_config['pages'][params[:ctrl]][params[:page]]
else
template = @help_config['pages'][params[:ctrl]]['index']
end
end
# choose language according to available help translations
lang = (@help_config['langs'].include? current_language.to_s) ? current_language.to_s : @help_config['langs'].first
url = "/manual/#{lang}/" + (template || "index.html")
redirect_to(request.relative_url_root + url)
end
private
def load_help_config
@help_config = YAML::load(File.open("#{RAILS_ROOT}/config/help.yml"))
end
end

View File

@@ -17,10 +17,7 @@
class IssueCategoriesController < ApplicationController
layout 'base'
menu_item :settings
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy
def edit
if request.post? and @category.update_attributes(params[:category])
@@ -30,17 +27,11 @@ class IssueCategoriesController < ApplicationController
end
def destroy
@issue_count = @category.issues.size
if @issue_count == 0
# No issue assigned to this category
@category.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
elsif params[:todo]
reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
@category.destroy(reassign_to)
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
end
@categories = @project.issue_categories - [@category]
@category.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
rescue
flash[:notice] = "Categorie can't be deleted"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
end
private

View File

@@ -79,7 +79,7 @@ class IssueStatusesController < ApplicationController
IssueStatus.find(params[:id]).destroy
redirect_to :action => 'list'
rescue
flash[:error] = "Unable to delete issue status"
flash[:notice] = "Unable to delete issue status"
redirect_to :action => 'list'
end
end

View File

@@ -16,17 +16,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController
layout 'base'
menu_item :new_issue, :only => :new
layout 'base', :except => :export_pdf
before_filter :find_project, :authorize
before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes
cache_sweeper :issue_sweeper, :only => [ :edit, :change_status, :destroy ]
helper :journals
helper :projects
include ProjectsHelper
helper :custom_fields
@@ -38,384 +32,138 @@ class IssuesController < ApplicationController
helper :watchers
include WatchersHelper
helper :attachments
include AttachmentsHelper
helper :queries
helper :sort
include SortHelper
include IssuesHelper
include AttachmentsHelper
def index
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
if @query.valid?
limit = per_page_option
respond_to do |format|
format.html { }
format.atom { }
format.csv { limit = Setting.issues_export_limit.to_i }
format.pdf { limit = Setting.issues_export_limit.to_i }
end
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, limit, params['page']
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
:conditions => @query.statement,
:limit => limit,
:offset => @issue_pages.current.offset
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
end
else
# Send html if the query is not valid
render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
end
rescue ActiveRecord::RecordNotFound
render_404
end
def changes
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
if @query.valid?
@journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
:conditions => @query.statement,
:limit => 25,
:order => "#{Journal.table_name}.created_on DESC"
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@activities = Enumeration::get_values('ACTI')
@priorities = Enumeration::get_values('IPRI')
@time_entry = TimeEntry.new
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
end
@status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
@custom_values = @issue.custom_values.find(:all, :include => :custom_field)
@journals_count = @issue.journals.count
@journals = @issue.journals.find(:all, :include => [:user, :details], :limit => 15, :order => "#{Journal.table_name}.created_on desc")
end
def history
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on desc")
@journals_count = @journals.length
end
def export_pdf
@custom_values = @issue.custom_values.find(:all, :include => :custom_field)
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "#{@project.name}_#{@issue.id}.pdf"
end
# Add a new issue
# The new issue will be created from an existing one if copy_from parameter is given
def new
@issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
@issue.project = @project
@issue.author = User.current
@issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
if @issue.tracker.nil?
flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
render :nothing => true, :layout => true
return
end
default_status = IssueStatus.default
unless default_status
flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
render :nothing => true, :layout => true
return
end
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
if request.get? || request.xhr?
@issue.start_date ||= Date.today
@custom_values = @issue.custom_values.empty? ?
@project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
@issue.custom_values
else
requested_status = (params[:issue] && params[:issue][:status_id] ? IssueStatus.find_by_id(params[:issue][:status_id]) : default_status)
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
:customized => @issue,
:value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
@issue.custom_values = @custom_values
if @issue.save
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
redirect_to :controller => 'issues', :action => 'show', :id => @issue
return
end
end
@priorities = Enumeration::get_values('IPRI')
render :layout => !request.xhr?
end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
def edit
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@activities = Enumeration::get_values('ACTI')
@priorities = Enumeration::get_values('IPRI')
@custom_values = []
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
attrs = params[:issue].dup
attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.attributes = attrs
end
if request.get?
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
else
# Update custom fields if user has :edit permission
if @edit_allowed && params[:custom_fields]
begin
@issue.init_journal(self.logged_in_user)
# Retrieve custom fields and values
@custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
@issue.custom_values = @custom_values
end
@time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
if @issue.save
# Log spend time
if current_role.allowed_to?(:log_time)
@time_entry.save
end
if !journal.new_record?
# Only send notification if something was actually changed
@issue.attributes = params[:issue]
if @issue.save
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
redirect_to :action => 'show', :id => @issue
end
redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:notice] = l(:notice_locking_conflict)
end
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash.now[:error] = l(:notice_locking_conflict)
end
# Bulk edit a set of issues
def bulk_edit
if request.post?
status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_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])
fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
unsaved_issue_ids = []
@issues.each do |issue|
journal = issue.init_journal(User.current, params[:notes])
issue.priority = priority if priority
issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
issue.category = category if category || params[:category_id] == 'none'
issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
issue.start_date = params[:start_date] unless params[:start_date].blank?
issue.due_date = params[:due_date] unless params[:due_date].blank?
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
# Keep unsaved issue ids to display them in flash error
unsaved_issue_ids << issue.id
end
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
# Find potential statuses the user could be allowed to switch issues to
@available_statuses = Workflow.find(:all, :include => :new_status,
:conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
end
def move
@allowed_projects = []
# find projects to which the user is allowed to move the issue
if User.current.admin?
# admin is allowed to move issues to any active (visible) project
@allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
else
User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
end
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
@target_project ||= @project
@trackers = @target_project.trackers
if request.post?
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
@issues.each do |issue|
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
end
if unsaved_issue_ids.empty?
flash[:notice] = l(:notice_successful_update) unless @issues.empty?
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
redirect_to :controller => 'issues', :action => 'index', :project_id => @project
return
end
render :layout => false if request.xhr?
end
end
def destroy
@hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
if @hours > 0
case params[:todo]
when 'destroy'
# nothing to do
when 'nullify'
TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
when 'reassign'
reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
if reassign_to.nil?
flash.now[:error] = l(:error_issue_not_found_in_project)
return
else
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
end
else
# display the destroy form
def add_note
unless params[:notes].empty?
journal = @issue.init_journal(self.logged_in_user, params[:notes])
if @issue.save
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
return
end
end
@issues.each(&:destroy)
redirect_to :action => 'index', :project_id => @project
show
render :action => 'show'
end
def change_status
@status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user
@new_status = IssueStatus.find(params[:new_status_id])
if params[:confirm]
begin
journal = @issue.init_journal(self.logged_in_user, params[:notes])
@issue.status = @new_status
if @issue.update_attributes(params[:issue])
# Save attachments
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:value => a.filename) unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
# Log time
if logged_in_user.authorized_to(@project, "timelog/edit")
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
@time_entry.save
end
flash[:notice] = l(:notice_successful_update)
Mailer.deliver_issue_edit(journal) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:notice] = l(:notice_locking_conflict)
end
end
@assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user }
@activities = Enumeration::get_values('ACTI')
end
def destroy
@issue.destroy
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project
end
def add_attachment
# Save the attachments
@attachments = []
journal = @issue.init_journal(self.logged_in_user)
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @issue, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:value => a.filename) unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
journal.save if journal.details.any?
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'show', :id => @issue
end
def destroy_attachment
a = @issue.attachments.find(params[:attachment_id])
a.destroy
journal = @issue.init_journal(User.current)
journal = @issue.init_journal(self.logged_in_user)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => a.id,
:old_value => a.filename)
journal.save
redirect_to :action => 'show', :id => @issue
end
def context_menu
@issues = Issue.find_all_by_id(params[:ids], :include => :project)
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@assignables = @issue.assignable_users
@assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
:update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = request.env['HTTP_REFERER']
render :layout => false
end
def update_form
@issue = Issue.new(params[:issue])
render :action => :new, :layout => false
end
def preview
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
@attachements = @issue.attachments if @issue
@text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
render :partial => 'common/preview'
end
private
def find_issue
def find_project
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
@html_title = "#{@project.name} - #{@issue.tracker.name} ##{@issue.id}"
rescue ActiveRecord::RecordNotFound
render_404
end
# Filter for bulk operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
raise ActiveRecord::RecordNotFound if @issues.empty?
projects = @issues.collect(&:project).compact.uniq
if projects.size == 1
@project = projects.first
else
# TODO: let users bulk edit/move/destroy issues from different projects
render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
return true unless params[:project_id]
@project = Project.find(params[:project_id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
# Retrieve query from session or build a new query
def retrieve_query
if !params[:query_id].blank?
cond = "project_id IS NULL"
cond << " OR project_id = #{@project.id}" if @project
@query = Query.find(params[:query_id], :conditions => cond)
@query.project = @project
session[:query] = {:id => @query.id, :project_id => @query.project_id}
else
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
# Give it a name, required to be valid
@query = Query.new(:name => "_")
@query.project = @project
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
else
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
@query.project = @project
end
end
end
end
end

View File

@@ -1,41 +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 JournalsController < ApplicationController
layout 'base'
before_filter :find_journal
def edit
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
render_403 and return false unless @journal.editable_by?(User.current)
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@@ -17,23 +17,13 @@
class MembersController < ApplicationController
layout 'base'
before_filter :find_member, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
before_filter :find_project, :authorize
def new
@project.members << Member.new(params[:member]) if request.post?
respond_to do |format|
format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
def edit
if request.post? and @member.update_attributes(params[:member])
respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/members'} }
end
end
end
@@ -42,18 +32,12 @@ class MembersController < ApplicationController
@member.destroy
respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/members'} }
end
end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_member
@member = Member.find(params[:id])
@project = @member.project
rescue ActiveRecord::RecordNotFound

View File

@@ -17,91 +17,45 @@
class MessagesController < ApplicationController
layout 'base'
menu_item :boards
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => :preview
before_filter :find_project, :check_project_privacy
before_filter :require_login, :only => [:new, :reply]
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
helper :attachments
include AttachmentsHelper
# Show a topic and its replies
def show
@replies = @topic.children
@replies.reverse! if User.current.wants_comments_in_reverse_order?
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr?
end
# Create a new topic
def new
@message = Message.new(params[:message])
@message.author = User.current
@message.board = @board
if params[:message] && User.current.allowed_to?(:edit_messages, @project)
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
end
@message.author = logged_in_user
@message.board = @board
if request.post? && @message.save
attach_files(@message, params[:attachments])
params[:attachments].each { |file|
next unless file.size > 0
Attachment.create(:container => @message, :file => file, :author => logged_in_user)
} if params[:attachments] and params[:attachments].is_a? Array
redirect_to :action => 'show', :id => @message
end
end
# Reply to a topic
def reply
@reply = Message.new(params[:reply])
@reply.author = User.current
@reply.author = logged_in_user
@reply.board = @board
@topic.children << @reply
if !@reply.new_record?
attach_files(@reply, params[:attachments])
end
redirect_to :action => 'show', :id => @topic
end
# Edit a message
def edit
if params[:message] && User.current.allowed_to?(:edit_messages, @project)
@message.locked = params[:message]['locked']
@message.sticky = params[:message]['sticky']
end
if request.post? && @message.update_attributes(params[:message])
attach_files(@message, params[:attachments])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'show', :id => @topic
end
end
# Delete a messages
def destroy
@message.destroy
redirect_to @message.parent.nil? ?
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
{ :action => 'show', :id => @message.parent }
end
def preview
message = @board.messages.find_by_id(params[:id])
@attachements = message.attachments if message
@text = (params[:message] || params[:reply])[:content]
render :partial => 'common/preview'
@message.children << @reply
redirect_to :action => 'show', :id => @message
end
private
def find_message
find_board
@message = @board.messages.find(params[:id], :include => :parent)
@topic = @message.root
rescue ActiveRecord::RecordNotFound
render_404
end
def find_board
def find_project
@board = Board.find(params[:board_id], :include => :project)
@project = @board.project
@message = @board.topics.find(params[:id]) if params[:id]
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MyController < ApplicationController
helper :issues
layout 'base'
before_filter :require_login
@@ -26,8 +24,7 @@ class MyController < ApplicationController
'issueswatched' => :label_watched_issues,
'news' => :label_news_latest,
'calendar' => :label_calendar,
'documents' => :label_document_plural,
'timelog' => :label_spent_time
'documents' => :label_document_plural
}.freeze
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
@@ -45,65 +42,44 @@ class MyController < ApplicationController
# Show user's page
def page
@user = User.current
@user = self.logged_in_user
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
end
# Edit user's account
def account
@user = User.current
@user = self.logged_in_user
@pref = @user.pref
if request.post?
@user.attributes = params[:user]
@user.mail_notification = (params[:notification_option] == 'all')
@user.pref.attributes = params[:pref]
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
if @user.save
@user.pref.save
@user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
set_language_if_valid @user.language
flash[:notice] = l(:notice_account_updated)
redirect_to :action => 'account'
return
end
@user.attributes = params[:user]
@user.pref.attributes = params[:pref]
if request.post? and @user.save and @user.pref.save
set_localization
flash.now[:notice] = l(:notice_account_updated)
self.logged_in_user.reload
end
@notification_options = [[l(:label_user_mail_option_all), 'all'],
[l(:label_user_mail_option_none), 'none']]
# Only users that belong to more than 1 project can select projects for which they are notified
# Note that @user.membership.size would fail since AR ignores :include association option when doing a count
@notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1
@notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected')
end
# Manage user's password
def password
@user = User.current
flash[:error] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
if request.post?
if @user.check_password?(params[:password])
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
if @user.save
flash[:notice] = l(:notice_account_password_updated)
redirect_to :action => 'account'
end
# Change user's password
def change_password
@user = self.logged_in_user
flash[:notice] = l(:notice_can_t_change_password) and redirect_to :action => 'account' and return if @user.auth_source_id
if @user.check_password?(params[:password])
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
if @user.save
flash[:notice] = l(:notice_account_password_updated)
else
flash[:error] = l(:notice_account_wrong_password)
render :action => 'account'
return
end
end
end
# Create a new feeds key
def reset_rss_key
if request.post? && User.current.rss_token
User.current.rss_token.destroy
flash[:notice] = l(:notice_feeds_access_key_reseted)
else
flash[:notice] = l(:notice_account_wrong_password)
end
redirect_to :action => 'account'
end
# User's page layout configuration
def page_layout
@user = User.current
@user = self.logged_in_user
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
session[:page_layout] = @blocks
%w(top left right).each {|f| session[:page_layout][f] ||= [] }
@@ -117,7 +93,7 @@ class MyController < ApplicationController
def add_block
block = params[:block]
render(:nothing => true) and return unless block && (BLOCKS.keys.include? block)
@user = User.current
@user = self.logged_in_user
# remove if already present in a group
%w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block }
# add it on top
@@ -152,7 +128,7 @@ class MyController < ApplicationController
# Save user's page layout
def page_layout_save
@user = User.current
@user = self.logged_in_user
@user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout]
@user.pref.save
session[:page_layout] = nil

View File

@@ -17,41 +17,11 @@
class NewsController < ApplicationController
layout 'base'
before_filter :find_news, :except => [:new, :index, :preview]
before_filter :find_project, :only => :new
before_filter :authorize, :except => [:index, :preview]
before_filter :find_optional_project, :only => :index
accept_key_auth :index
def index
@news_pages, @newss = paginate :news,
:per_page => 10,
:conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
:include => [:author, :project],
:order => "#{News.table_name}.created_on DESC"
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
end
end
before_filter :find_project, :authorize
def show
@comments = @news.comments
@comments.reverse! if User.current.wants_comments_in_reverse_order?
end
def new
@news = News.new(:project => @project, :author => User.current)
if request.post?
@news.attributes = params[:news]
if @news.save
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
redirect_to :controller => 'news', :action => 'index', :project_id => @project
end
end
end
def edit
if request.post? and @news.update_attributes(params[:news])
flash[:notice] = l(:notice_successful_update)
@@ -61,7 +31,7 @@ class NewsController < ApplicationController
def add_comment
@comment = Comment.new(params[:comment])
@comment.author = User.current
@comment.author = logged_in_user
if @news.comments << @comment
flash[:notice] = l(:label_comment_added)
redirect_to :action => 'show', :id => @news
@@ -77,33 +47,14 @@ class NewsController < ApplicationController
def destroy
@news.destroy
redirect_to :action => 'index', :project_id => @project
end
def preview
@text = (params[:news] ? params[:news][:description] : nil)
render :partial => 'common/preview'
redirect_to :controller => 'projects', :action => 'list_news', :id => @project
end
private
def find_news
def find_project
@news = News.find(params[:id])
@project = @news.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
return true unless params[:project_id]
@project = Project.find(params[:project_id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
end
end

View File

@@ -15,67 +15,70 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'csv'
class ProjectsController < ApplicationController
layout 'base'
menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap
menu_item :files, :only => [:list_files, :add_file]
menu_item :settings, :only => :settings
menu_item :issues, :only => [:changelog]
before_filter :find_project, :except => [ :index, :list, :add, :activity ]
before_filter :find_optional_project, :only => :activity
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
before_filter :find_project, :except => [ :index, :list, :add ]
before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
accept_key_auth :activity, :calendar
cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
cache_sweeper :issue_sweeper, :only => [ :add_issue ]
helper :sort
include SortHelper
helper :custom_fields
include CustomFieldsHelper
helper :ifpdf
include IfpdfHelper
helper :issues
helper IssuesHelper
helper :queries
include QueriesHelper
helper :repositories
include RepositoriesHelper
include ProjectsHelper
def index
list
render :action => 'list' unless request.xhr?
end
# Lists visible projects
# Lists public projects
def list
projects = Project.find :all,
:conditions => Project.visible_by(User.current),
:include => :parent
@project_tree = projects.group_by {|p| p.parent || p}
@project_tree.each_key {|p| @project_tree[p] -= [p]}
sort_init "#{Project.table_name}.name", "asc"
sort_update
@project_count = Project.count(:all, :conditions => Project.visible_by(logged_in_user))
@project_pages = Paginator.new self, @project_count,
15,
params['page']
@projects = Project.find :all, :order => sort_clause,
:conditions => Project.visible_by(logged_in_user),
:include => :parent,
:limit => @project_pages.items_per_page,
:offset => @project_pages.current.offset
render :action => "list", :layout => false if request.xhr?
end
# Add a new project
def add
@custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@root_projects = Project.find(:all,
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
:order => 'name')
@custom_fields = IssueCustomField.find(:all)
@root_projects = Project.find(:all, :conditions => "parent_id is null")
@project = Project.new(params[:project])
if request.get?
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
else
@project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@project.custom_values = @custom_values
@project.enabled_module_names = params[:enabled_modules]
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@project.custom_values = @custom_values
if params[:repository_enabled] && params[:repository_enabled] == "1"
@project.repository = Repository.factory(params[:repository_scm])
@project.repository.attributes = params[:repository]
end
if "1" == params[:wiki_enabled]
@project.wiki = Wiki.new
@project.wiki.attributes = params[:wiki]
end
if @project.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
@@ -85,49 +88,49 @@ class ProjectsController < ApplicationController
# Show @project
def show
@custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
@custom_values = @project.custom_values.find(:all, :include => :custom_field)
@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.active_children
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers
cond = @project.project_condition(Setting.display_subprojects_issues?)
Issue.visible_by(User.current) do
@open_issues_by_tracker = Issue.count(:group => :tracker,
:include => [:project, :status, :tracker],
:conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
@total_issues_by_tracker = Issue.count(:group => :tracker,
:include => [:project, :status, :tracker],
:conditions => cond)
end
TimeEntry.visible_by(User.current) do
@total_hours = TimeEntry.sum(:hours,
:include => :project,
:conditions => cond).to_f
end
@key = User.current.rss_key
@trackers = Tracker.find(:all, :order => 'position')
@open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
@total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
end
def settings
@root_projects = Project.find(:all,
:conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
:order => 'name')
@root_projects = Project::find(:all, :conditions => ["parent_id is null and id <> ?", @project.id])
@custom_fields = IssueCustomField.find(:all)
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
@trackers = Tracker.all
@custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
@repository ||= @project.repository
@wiki ||= @project.wiki
@custom_values ||= ProjectCustomField.find(:all).collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
end
# Edit @project
def edit
if request.post?
@project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
if params[:custom_fields]
@custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@custom_values = ProjectCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
@project.custom_values = @custom_values
end
if params[:repository_enabled]
case params[:repository_enabled]
when "0"
@project.repository = nil
when "1"
@project.repository ||= Repository.factory(params[:repository_scm])
@project.repository.update_attributes params[:repository] if @project.repository
end
end
if params[:wiki_enabled]
case params[:wiki_enabled]
when "0"
@project.wiki.destroy if @project.wiki
when "1"
@project.wiki ||= Wiki.new
@project.wiki.update_attributes params[:wiki]
end
end
@project.attributes = params[:project]
if @project.save
flash[:notice] = l(:notice_successful_update)
@@ -138,11 +141,6 @@ class ProjectsController < ApplicationController
end
end
end
def modules
@project.enabled_module_names = params[:enabled_modules]
redirect_to :action => 'settings', :id => @project, :tab => 'modules'
end
def archive
@project.archive if request.post? && @project.active?
@@ -169,18 +167,8 @@ class ProjectsController < ApplicationController
def add_issue_category
@category = @project.issue_categories.build(params[:category])
if request.post? and @category.save
respond_to do |format|
format.html do
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'categories', :id => @project
end
format.js do
# IE doesn't support the replace_html rjs method for select box options
render(:update) {|page| page.replace "issue_category_id",
content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
}
end
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'settings', :tab => 'categories', :id => @project
end
end
@@ -193,136 +181,359 @@ class ProjectsController < ApplicationController
end
end
# Add a new member to @project
def add_member
@member = @project.members.build(params[:member])
if request.post? && @member.save
respond_to do |format|
format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'members'} }
end
else
settings
render :action => 'settings'
end
end
# Show members list of @project
def list_members
@members = @project.members.find(:all)
end
# Add a new document to @project
def add_document
@categories = Enumeration::get_values('DCAT')
@document = @project.documents.build(params[:document])
if request.post? and @document.save
# Save the attachments
params[:attachments].each { |a|
Attachment.create(:container => @document, :file => a, :author => logged_in_user) unless a.size == 0
} if params[:attachments] and params[:attachments].is_a? Array
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_document_add(@document) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'list_documents', :id => @project
end
end
# Show documents list of @project
def list_documents
@documents = @project.documents.find :all, :include => :category
end
# Add a new issue to @project
def add_issue
@tracker = Tracker.find(params[:tracker_id])
@priorities = Enumeration::get_values('IPRI')
default_status = IssueStatus.default
unless default_status
flash.now[:notice] = 'No default issue status defined. Please check your configuration.'
render :nothing => true, :layout => true
return
end
@issue = Issue.new(:project => @project, :tracker => @tracker)
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker))if logged_in_user
if request.get?
@issue.start_date = Date.today
@custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) }
else
@issue.attributes = params[:issue]
requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
@issue.author_id = self.logged_in_user.id if self.logged_in_user
# Multiple file upload
@attachments = []
params[:attachments].each { |a|
@attachments << Attachment.new(:container => @issue, :file => a, :author => logged_in_user) unless a.size == 0
} if params[:attachments] and params[:attachments].is_a? Array
@custom_values = @project.custom_fields_for_issues(@tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
@issue.custom_values = @custom_values
if @issue.save
@attachments.each(&:save)
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :action => 'list_issues', :id => @project
end
end
end
# Show filtered/sorted issues list of @project
def list_issues
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
@results_per_page_options = [ 15, 25, 50, 100 ]
if params[:per_page] and @results_per_page_options.include? params[:per_page].to_i
@results_per_page = params[:per_page].to_i
session[:results_per_page] = @results_per_page
else
@results_per_page = session[:results_per_page] || 25
end
if @query.valid?
@issue_count = Issue.count(:include => [:status, :project, :custom_values], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, @results_per_page, params['page']
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :status, :tracker, :project, :priority, :custom_values ],
:conditions => @query.statement,
:limit => @issue_pages.items_per_page,
:offset => @issue_pages.current.offset
end
render :layout => false if request.xhr?
end
# Export filtered/sorted issues list to CSV
def export_issues_csv
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
render :action => 'list_issues' and return unless @query.valid?
@issues = Issue.find :all, :order => sort_clause,
:include => [ :assigned_to, :author, :status, :tracker, :priority, :project, {:custom_values => :custom_field} ],
:conditions => @query.statement,
:limit => Setting.issues_export_limit.to_i
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#", l(:field_status),
l(:field_project),
l(:field_tracker),
l(:field_priority),
l(:field_subject),
l(:field_assigned_to),
l(:field_author),
l(:field_start_date),
l(:field_due_date),
l(:field_done_ratio),
l(:field_created_on),
l(:field_updated_on)
]
for custom_field in @project.all_custom_fields
headers << custom_field.name
end
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
@issues.each do |issue|
fields = [issue.id, issue.status.name,
issue.project.name,
issue.tracker.name,
issue.priority.name,
issue.subject,
(issue.assigned_to ? issue.assigned_to.name : ""),
issue.author.name,
issue.start_date ? l_date(issue.start_date) : nil,
issue.due_date ? l_date(issue.due_date) : nil,
issue.done_ratio,
l_datetime(issue.created_on),
l_datetime(issue.updated_on)
]
for custom_field in @project.all_custom_fields
fields << (show_value issue.custom_value_for(custom_field))
end
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
end
# Export filtered/sorted issues to PDF
def export_issues_pdf
sort_init "#{Issue.table_name}.id", "desc"
sort_update
retrieve_query
render :action => 'list_issues' and return unless @query.valid?
@issues = Issue.find :all, :order => sort_clause,
:include => [ :author, :status, :tracker, :priority, :project, :custom_values ],
:conditions => @query.statement,
:limit => Setting.issues_export_limit.to_i
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "export.pdf"
render :layout => false
end
def move_issues
@issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
redirect_to :action => 'list_issues', :id => @project and return unless @issues
@projects = []
# find projects to which the user is allowed to move the issue
@logged_in_user.memberships.each {|m| @projects << m.project if Permission.allowed_to_role("projects/move_issues", m.role)}
# issue can be moved to any tracker
@trackers = Tracker.find(:all)
if request.post? and params[:new_project_id] and params[:new_tracker_id]
new_project = Project.find(params[:new_project_id])
new_tracker = Tracker.find(params[:new_tracker_id])
@issues.each { |i|
# project dependent properties
unless i.project_id == new_project.id
i.category = nil
i.fixed_version = nil
# delete issue relations
i.relations_from.clear
i.relations_to.clear
end
# move the issue
i.project = new_project
i.tracker = new_tracker
i.save
}
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list_issues', :id => @project
end
end
# Add a news to @project
def add_news
@news = News.new(:project => @project)
if request.post?
@news.attributes = params[:news]
@news.author_id = self.logged_in_user.id if self.logged_in_user
if @news.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list_news', :id => @project
end
end
end
# Show news list of @project
def list_news
@news_pages, @news = paginate :news, :per_page => 10, :conditions => ["project_id=?", @project.id], :include => :author, :order => "#{News.table_name}.created_on DESC"
render :action => "list_news", :layout => false if request.xhr?
end
def add_file
if request.post?
@version = @project.versions.find_by_id(params[:version_id])
attachments = attach_files(@version, params[:attachments])
Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
# Save the attachments
@attachments = []
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @version, :file => file, :author => logged_in_user)
@attachments << a unless a.new_record?
} if params[:attachments] and params[:attachments].is_a? Array
Mailer.deliver_attachments_add(@attachments) if !@attachments.empty? and Permission.find_by_controller_and_action(params[:controller], params[:action]).mail_enabled?
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
@versions = @project.versions.sort
end
def list_files
sort_init "#{Attachment.table_name}.filename", "asc"
sort_update
@versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
render :layout => !request.xhr?
@versions = @project.versions.sort
end
# Show changelog for @project
def changelog
@trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
@trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
end
def roadmap
@trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
@trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
retrieve_selected_tracker_ids(@trackers)
@versions = @project.versions.sort
@versions = @versions.select {|v| !v.completed? } unless params[:completed]
conditions = ("1" == params[:completed] ? nil : [ "#{Version.table_name}.effective_date > ? OR #{Version.table_name}.effective_date IS NULL", Date.today])
@versions = @project.versions.find(:all, :conditions => conditions).sort
end
def activity
@days = Setting.activity_days_default.to_i
if params[:from]
begin; @date_to = params[:from].to_date; rescue; end
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
@date_to ||= Date.today + 1
@date_from = @date_to - @days
@date_from = Date.civil(@year, @month, 1)
@date_to = @date_from >> 1
@event_types = %w(issues news files documents changesets wiki_pages messages)
if @project
@event_types.delete('wiki_pages') unless @project.wiki
@event_types.delete('changesets') unless @project.repository
@event_types.delete('messages') unless @project.boards.any?
# only show what the user is allowed to view
@event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
end
@scope = @event_types.select {|t| params["show_#{t}"]}
# default events if none is specified in parameters
@scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
@events_by_day = {}
@events = []
if @scope.include?('issues')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions)
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{JournalDetail.table_name}.prop_key = 'status_id' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions)
unless params[:show_issues] == "0"
@project.issues.find(:all, :include => [:author], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_issues = 1
end
if @scope.include?('news')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions)
unless params[:show_news] == "0"
@project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_news = 1
end
if @scope.include?('files')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id",
:conditions => cond.conditions)
unless params[:show_files] == "0"
Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_files = 1
end
if @scope.include?('documents')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Document.find(:all, :include => :project, :conditions => cond.conditions)
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id",
:conditions => cond.conditions)
unless params[:show_documents] == "0"
@project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author ).each { |i|
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_documents = 1
end
if @scope.include?('wiki_pages')
unless @project.wiki.nil? || params[:show_wiki_edits] == "0"
select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
"#{WikiContent.versioned_table_name}.id"
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title"
joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
@project.id, @date_from, @date_to]
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to])
@events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions)
WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions).each { |i|
# We provide this alias so all events can be treated in the same manner
def i.created_on
self.updated_on
end
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_wiki_edits = 1
end
if @scope.include?('changesets')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions)
unless @project.repository.nil? || params[:show_changesets] == "0"
@project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]).each { |i|
def i.created_on
self.committed_on
end
@events_by_day[i.created_on.to_date] ||= []
@events_by_day[i.created_on.to_date] << i
}
@show_changesets = 1
end
if @scope.include?('messages')
cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects))
cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
@events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions)
end
@events_by_day = @events.group_by(&:event_date)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") }
end
render :layout => false if request.xhr?
end
def calendar
@trackers = @project.rolled_up_trackers
@trackers = Tracker.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers)
if params[:year] and params[:year].to_i > 1900
@@ -332,25 +543,32 @@ class ProjectsController < ApplicationController
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,
@month ||= Date.today.month
@date_from = Date.civil(@year, @month, 1)
@date_to = (@date_from >> 1)-1
# start on monday
@date_from = @date_from - (@date_from.cwday-1)
# finish on sunday
@date_to = @date_to + (7-@date_to.cwday)
@events = []
@project.issues_with_subprojects(params[: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]
:conditions => ["((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?)) and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", @date_from, @date_to, @date_from, @date_to]
) unless @selected_tracker_ids.empty?
events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
end
@calendar.events = events
@events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
@ending_events_by_days = @events.group_by {|event| event.due_date}
@starting_events_by_days = @events.group_by {|event| event.start_date}
render :layout => false if request.xhr?
end
def gantt
@trackers = @project.rolled_up_trackers
@trackers = Tracker.find(:all, :order => 'position')
retrieve_selected_tracker_ids(@trackers)
if params[:year] and params[:year].to_i >0
@@ -361,49 +579,40 @@ class ProjectsController < ApplicationController
@month_from = 1
end
else
@month_from ||= Date.today.month
@year_from ||= Date.today.year
@month_from ||= (Date.today << 1).month
@year_from ||= (Date.today << 1).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
@zoom = (params[:zoom].to_i > 0 and params[:zoom].to_i < 5) ? params[:zoom].to_i : 2
@months = (params[:months].to_i > 0 and params[:months].to_i < 25) ? params[:months].to_i : 6
@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
@project.issues_with_subprojects(params[:with_subprojects]) do
@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?
@events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
end
@events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
@events.sort! {|x,y| x.start_date <=> y.start_date }
if params[:format]=='pdf'
if params[:output]=='pdf'
@options_for_rfpdf ||= {}
@options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
@options_for_rfpdf[:file_name] = "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
def feeds
@queries = @project.queries.find :all, :conditions => ["is_public=? or user_id=?", true, (logged_in_user ? logged_in_user.id : 0)]
@key = logged_in_user.get_or_create_rss_key.value if logged_in_user
end
private
# Find project of id params[:id]
@@ -411,18 +620,11 @@ private
# Used as a before_filter
def find_project
@project = Project.find(params[:id])
@html_title = @project.name
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
return true unless params[:id]
@project = Project.find(params[:id])
authorize
rescue ActiveRecord::RecordNotFound
render_404
end
def retrieve_selected_tracker_ids(selectable_trackers)
if ids = params[:tracker_ids]
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
@@ -430,4 +632,31 @@ private
@selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
end
end
# Retrieve query from session or build a new query
def retrieve_query
if params[:query_id]
@query = @project.queries.find(params[:query_id])
@query.executed_by = logged_in_user
session[:query] = @query
else
if params[:set_filter] or !session[:query] or session[:query].project_id != @project.id
# Give it a name, required to be valid
@query = Query.new(:name => "_", :executed_by => logged_in_user)
@query.project = @project
if params[:fields] and params[:fields].is_a? Array
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end
else
@query.available_filters.keys.each do |field|
@query.add_short_filter(field, params[field]) if params[field]
end
end
session[:query] = @query
else
@query = session[:query]
end
end
end
end

View File

@@ -16,25 +16,30 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueriesController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_query, :except => :new
before_filter :find_optional_project, :only => :new
layout 'base'
before_filter :require_login, :except => :index
before_filter :find_project, :check_project_privacy
def index
@queries = @project.queries.find(:all,
:order => "name ASC",
:conditions => ["is_public = ? or user_id = ?", true, (logged_in_user ? logged_in_user.id : 0)])
end
def new
@query = Query.new(params[:query])
@query.project = params[:query_is_for_all] ? nil : @project
@query.user = User.current
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
@query.project = @project
@query.user = logged_in_user
@query.executed_by = logged_in_user
@query.is_public = false unless logged_in_user.authorized_to(@project, 'projects/add_query')
params[:fields].each do |field|
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
if request.post? && params[:confirm] && @query.save
if request.post? and @query.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => @query
return
end
render :layout => false if request.xhr?
@@ -47,34 +52,30 @@ class QueriesController < ApplicationController
@query.add_filter(field, params[:operators][field], params[:values][field])
end if params[:fields]
@query.attributes = params[:query]
@query.project = nil if params[:query_is_for_all]
@query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
@query.column_names = nil if params[:default_columns]
@query.is_public = false unless logged_in_user.authorized_to(@project, 'projects/add_query')
if @query.save
flash[:notice] = l(:notice_successful_update)
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
redirect_to :controller => 'projects', :action => 'list_issues', :id => @project, :query_id => @query
end
end
end
def destroy
@query.destroy if request.post?
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
redirect_to :controller => 'queries', :project_id => @project
end
private
def find_query
@query = Query.find(params[:id])
@project = @query.project
render_403 unless @query.editable_by?(User.current)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_optional_project
@project = Project.find(params[:project_id]) if params[:project_id]
User.current.allowed_to?(:save_queries, @project, :global => true)
def find_project
if params[:id]
@query = Query.find(params[:id])
@query.executed_by = logged_in_user
@project = @query.project
render_403 unless @query.editable_by?(logged_in_user)
else
@project = Project.find(params[:project_id])
end
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -17,7 +17,6 @@
class ReportsController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_project, :authorize
def issue_report
@@ -26,7 +25,7 @@ class ReportsController < ApplicationController
case params[:detail]
when "tracker"
@field = "tracker_id"
@rows = @project.trackers
@rows = Tracker.find :all, :order => 'position'
@data = issues_by_tracker
@report_title = l(:field_tracker)
render :template => "reports/issue_report_details"
@@ -48,12 +47,6 @@ class ReportsController < ApplicationController
@data = issues_by_category
@report_title = l(:field_category)
render :template => "reports/issue_report_details"
when "assigned_to"
@field = "assigned_to_id"
@rows = @project.members.collect { |m| m.user }
@data = issues_by_assigned_to
@report_title = l(:field_assigned_to)
render :template => "reports/issue_report_details"
when "author"
@field = "author_id"
@rows = @project.members.collect { |m| m.user }
@@ -67,21 +60,19 @@ class ReportsController < ApplicationController
@report_title = l(:field_subproject)
render :template => "reports/issue_report_details"
else
@trackers = @project.trackers
@trackers = Tracker.find(:all, :order => 'position')
@versions = @project.versions.sort
@priorities = Enumeration::get_values('IPRI')
@categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user }
@authors = @project.members.collect { |m| m.user }
@subprojects = @project.active_children
issues_by_tracker
issues_by_version
issues_by_priority
issues_by_category
issues_by_assigned_to
issues_by_author
issues_by_subproject
@total_hours = @project.time_entries.sum(:hours)
render :template => "reports/issue_report"
end
end
@@ -189,22 +180,7 @@ private
and i.project_id=#{@project.id}
group by s.id, s.is_closed, c.id")
end
def issues_by_assigned_to
@issues_by_assigned_to ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,
s.is_closed as closed,
a.id as assigned_to_id,
count(i.id) as total
from
#{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
where
i.status_id=s.id
and i.assigned_to_id=a.id
and i.project_id=#{@project.id}
group by s.id, s.is_closed, a.id")
end
def issues_by_author
@issues_by_author ||=
ActiveRecord::Base.connection.select_all("select s.id as status_id,

View File

@@ -19,145 +19,76 @@ require 'SVG/Graph/Bar'
require 'SVG/Graph/BarHorizontal'
require 'digest/sha1'
class ChangesetNotFound < Exception; end
class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
layout 'base'
menu_item :repository
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
before_filter :authorize
accept_key_auth :revisions
def edit
@repository = @project.repository
if !@repository
@repository = Repository.factory(params[:repository_scm])
@repository.project = @project
end
if request.post?
@repository.attributes = params[:repository]
@repository.save
end
render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
end
def destroy
@repository.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
end
before_filter :find_project, :except => [:update_form]
before_filter :authorize, :except => [:update_form, :stats, :graph]
before_filter :check_project_privacy, :only => [:stats, :graph]
def show
# check if new revisions have been committed in the repository
@repository.fetch_changesets if Setting.autofetch_changesets?
# root entries
@entries = @repository.entries('', @rev)
# get entries for the browse frame
@entries = @repository.entries('')
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error_not_found unless @entries || @changesets.any?
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
show_error and return unless @entries || @changesets.any?
end
def browse
@entries = @repository.entries(@path, @rev)
if request.xhr?
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
show_error_not_found and return unless @entries
render :action => 'browse'
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
show_error and return unless @entries
end
def changes
@entry = @repository.scm.entry(@path, @rev)
show_error_not_found and return unless @entry
@changesets = @repository.changesets_for_path(@path)
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
show_error and return unless @entry
@changes = Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", @repository.id, @path.with_leading_slash],
:order => "committed_on DESC")
end
def revisions
@changeset_count = @repository.changesets.count
@changeset_pages = Paginator.new self, @changeset_count,
per_page_option,
25,
params['page']
@changesets = @repository.changesets.find(:all,
:limit => @changeset_pages.items_per_page,
:offset => @changeset_pages.current.offset)
respond_to do |format|
format.html { render :layout => false if request.xhr? }
format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
end
render :action => "revisions", :layout => false if request.xhr?
end
def entry
@entry = @repository.scm.entry(@path, @rev)
show_error_not_found and return unless @entry
# If the entry is a dir, show the browser
browse and return if @entry.is_dir?
@content = @repository.scm.cat(@path, @rev)
show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data?
# Force the download if it's a binary file
show_error and return unless @content
if 'raw' == params[:format]
send_data @content, :filename => @path.split('/').last
else
# Prevent empty lines when displaying a file with Windows style eol
@content.gsub!("\r\n", "\n")
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def annotate
@annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def revision
@changeset = @repository.changesets.find_by_revision(@rev)
raise ChangesetNotFound unless @changeset
show_error and return unless @changeset
@changes_count = @changeset.changes.size
@changes_pages = Paginator.new self, @changes_count, 150, params['page']
@changes = @changeset.changes.find(:all,
:limit => @changes_pages.items_per_page,
:offset => @changes_pages.current.offset)
respond_to do |format|
format.html
format.js {render :layout => false}
end
rescue ChangesetNotFound
show_error_not_found
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
render :action => "revision", :layout => false if request.xhr?
end
def diff
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
# Save diff type as user preference
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
User.current.pref[:diff_type] = @diff_type
User.current.preference.save
end
@rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
@diff_type = ('sbs' == params[:type]) ? 'sbs' : 'inline'
@cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
unless read_fragment(@cache_key)
@diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
show_error_not_found unless @diff
@diff = @repository.diff(@path, @rev, @rev_to, type)
show_error and return unless @diff
end
rescue Redmine::Scm::Adapters::CommandFailed => e
show_error_command_failed(e.message)
end
def stats
@@ -179,47 +110,36 @@ class RepositoriesController < ApplicationController
end
end
def update_form
@repository = Repository.factory(params[:repository_scm])
render :partial => 'projects/repository', :locals => {:repository => @repository}
end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
REV_PARAM_RE = %r{^[a-f0-9]*$}
def find_repository
@project = Project.find(params[:id])
@repository = @project.repository
render_404 and return false unless @repository
@path = params[:path].join('/') unless params[:path].nil?
@path = params[:path].squeeze('/') if params[:path]
@path ||= ''
@rev = params[:rev]
@rev_to = params[:rev_to]
raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
@rev = params[:rev].to_i if params[:rev]
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
show_error_not_found
end
def show_error_not_found
render_error l(:error_scm_not_found)
end
def show_error_command_failed(msg)
render_error l(:error_scm_command_failed, msg)
def show_error
flash.now[:notice] = l(:notice_scm_error)
render :nothing => true, :layout => true
end
def graph_commits_per_month(repository)
@date_to = Date.today
@date_from = @date_to << 11
@date_from = Date.civil(@date_from.year, @date_from.month, 1)
@date_from = @date_to << 12
commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
commits_by_month = [0] * 12
commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
changes_by_day = repository.changes.count(:all, :group => :commit_date)
changes_by_month = [0] * 12
changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
@@ -267,9 +187,6 @@ private
commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
# Remove email adress in usernames
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
graph = SVG::Graph::BarHorizontal.new(
:height => 300,
:width => 500,

View File

@@ -28,41 +28,41 @@ class RolesController < ApplicationController
end
def list
@role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
@role_pages, @roles = paginate :roles, :per_page => 25, :order => "position"
render :action => "list", :layout => false if request.xhr?
end
def new
# Prefills the form with 'Non member' role permissions
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
if request.post? && @role.save
# workflow copy
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
@role.workflows.copy(copy_from)
@role = Role.new(params[:role])
if request.post?
@role.permissions = Permission.find(params[:permission_ids]) if params[:permission_ids]
if @role.save
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
@permissions = @role.setable_permissions
@roles = Role.find :all, :order => 'builtin, position'
@permissions = Permission.find(:all, :conditions => ["is_public=?", false], :order => 'sort ASC')
end
def edit
@role = Role.find(params[:id])
if request.post? and @role.update_attributes(params[:role])
@role.permissions = Permission.find(params[:permission_ids] || [])
Permission.allowed_to_role_expired
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list'
end
@permissions = @role.setable_permissions
@permissions = Permission.find(:all, :conditions => ["is_public=?", false], :order => 'sort ASC')
end
def destroy
@role = Role.find(params[:id])
@role.destroy
unless @role.members.empty?
flash[:notice] = 'Some members have this role. Can\'t delete it.'
else
@role.destroy
end
redirect_to :action => 'list'
rescue
flash[:error] = 'This role is in use and can not be deleted.'
redirect_to :action => 'index'
end
def move
@@ -93,22 +93,21 @@ class RolesController < ApplicationController
}
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')
@roles = Role.find(:all, :order => 'position')
@trackers = Tracker.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :order => 'position')
@statuses = IssueStatus.find(:all, :include => :workflows, :order => 'position')
end
def report
@roles = Role.find(:all, :order => 'builtin, position')
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
@roles = Role.find(:all, :order => 'position')
@permissions = Permission.find :all, :conditions => ["is_public=?", false], :order => 'sort'
if request.post?
@roles.each do |role|
role.permissions = params[:permissions][role.id.to_s]
role.save
role.permissions = Permission.find(params[:permission_ids] ? (params[:permission_ids][role.id.to_s] || []) : [] )
end
Permission.allowed_to_role_expired
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'list'
end

View File

@@ -17,8 +17,6 @@
class SearchController < ApplicationController
layout 'base'
before_filter :find_optional_project
helper :messages
include MessagesHelper
@@ -27,84 +25,56 @@ class SearchController < ApplicationController
@question = params[:q] || ""
@question.strip!
@all_words = params[:all_words] || (params[:submit] ? false : true)
@titles_only = !params[:titles_only].nil?
offset = nil
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
@scope = params[:scope] || (params[:submit] ? [] : %w(projects issues changesets news documents wiki messages) )
# quick jump to an issue
if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
if @scope.include?('issues') && @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(logged_in_user))
redirect_to :controller => "issues", :action => "show", :id => $1
return
end
if @project
# only show what the user is allowed to view
@object_types = %w(issues news documents changesets wiki_pages messages)
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
@scope = @object_types.select {|t| params[t]}
@scope = @object_types if @scope.empty?
else
@object_types = @scope = %w(projects)
if params[:id]
find_project
return unless check_project_privacy
end
# extract tokens from the question
# eg. hello "bye bye" => ["hello", "bye bye"]
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
# tokens must be at least 3 character long
@tokens = @tokens.uniq.select {|w| w.length > 2 }
@tokens = @question.split.uniq.select {|w| w.length > 2 }
if !@tokens.empty?
# no more than 5 tokens to search for
@tokens.slice! 5..-1 if @tokens.size > 5
# strings used in sql like statement
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
@results = []
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
operator = @all_words ? " AND " : " OR "
limit = 10
if @project
@scope.each do |s|
@results += s.singularize.camelcase.constantize.search(like_tokens, @project,
:all_words => @all_words,
:titles_only => @titles_only,
:limit => (limit+1),
:offset => offset,
:before => params[:previous].nil?)
end
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
if params[:previous].nil?
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
if @results.size > limit
@pagination_next_date = @results[limit-1].event_datetime
@results = @results[0, limit]
end
else
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
if @results.size > limit
@pagination_previous_date = @results[-(limit)].event_datetime
@results = @results[-(limit), limit]
end
@results = []
if @project
@results += @project.issues.find(:all, :limit => limit, :include => :author, :conditions => [ (["(LOWER(subject) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'issues'
@results += @project.news.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort], :include => :author ) if @scope.include? 'news'
@results += @project.documents.find(:all, :limit => limit, :conditions => [ (["(LOWER(title) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'documents'
@results += @project.wiki.pages.find(:all, :limit => limit, :include => :content, :conditions => [ (["(LOWER(title) like ? OR LOWER(text) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @project.wiki && @scope.include?('wiki')
@results += @project.repository.changesets.find(:all, :limit => limit, :conditions => [ (["(LOWER(comments) like ?)"] * like_tokens.size).join(operator), * (like_tokens).sort] ) if @project.repository && @scope.include?('changesets')
Message.with_scope :find => {:conditions => ["#{Board.table_name}.project_id = ?", @project.id]} do
@results += Message.find(:all, :include => :board, :limit => limit, :conditions => [ (["(LOWER(subject) like ? OR LOWER(content) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'messages'
end
else
operator = @all_words ? ' AND ' : ' OR '
@results += Project.find(:all,
:limit => limit,
:conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort]
) if @scope.include? 'projects'
Project.with_scope(:find => {:conditions => Project.visible_by(logged_in_user)}) do
@results += Project.find(:all, :limit => limit, :conditions => [ (["(LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort] ) if @scope.include? 'projects'
end
# if only one project is found, user is redirected to its overview
redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
end
@question = @tokens.join(" ")
else
@question = ""
end
render :layout => false if request.xhr?
end
private
def find_optional_project
return true unless params[:id]
def find_project
@project = Project.find(params[:id])
check_project_privacy
@html_title = @project.name
rescue ActiveRecord::RecordNotFound
render_404
end

View File

@@ -25,31 +25,10 @@ class SettingsController < ApplicationController
end
def edit
@notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted)
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
settings = (params[:settings] || {}).dup.symbolize_keys
settings.each do |name, value|
# remove blank values in array settings
value.delete_if {|v| v.blank? } if value.is_a?(Array)
Setting[name] = value
end
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'edit', :tab => params[:tab]
return
if request.post? and params[:settings] and params[:settings].is_a? Hash
params[:settings].each { |name, value| Setting[name] = value }
redirect_to :action => 'edit' and return
end
@options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
end
def plugin
plugin_id = params[:id].to_sym
@plugin = Redmine::Plugin.registered_plugins[plugin_id]
if request.post?
Setting["plugin_#{plugin_id}"] = params[:settings]
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => params[:id]
end
@partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial]
@settings = Setting["plugin_#{plugin_id}"]
@textile_available = ActionView::Helpers::TextHelper.method_defined?("textilize")
end
end

View File

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

View File

@@ -17,70 +17,56 @@
class TimelogController < ApplicationController
layout 'base'
menu_item :issues
before_filter :find_project, :authorize
verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
before_filter :find_project
before_filter :authorize, :only => :edit
before_filter :check_project_privacy, :except => :edit
helper :sort
include SortHelper
helper :issues
include TimelogHelper
helper :custom_fields
include CustomFieldsHelper
def report
@available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
:klass => Project,
:label => :label_project},
'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:klass => Version,
@available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
:values => @project.versions,
:label => :label_version},
'category' => {:sql => "#{Issue.table_name}.category_id",
:klass => IssueCategory,
:values => @project.issue_categories,
:label => :field_category},
'member' => {:sql => "#{TimeEntry.table_name}.user_id",
:klass => User,
:values => @project.users,
:label => :label_member},
'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
:klass => Tracker,
:values => Tracker.find(:all),
:label => :label_tracker},
'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
:klass => Enumeration,
:label => :label_activity},
'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
:klass => Issue,
:label => :label_issue}
:values => Enumeration::get_values('ACTI'),
:label => :label_activity}
}
# Add list and boolean custom fields as available criterias
@project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
@available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
:format => cf.field_format,
:label => cf.name}
end
@criterias = params[:criterias] || []
@criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
@criterias.uniq!
@criterias = @criterias[0,3]
@columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
@columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
retrieve_date_range
if params[:date_from]
begin; @date_from = params[:date_from].to_date; rescue; end
end
if params[:date_to]
begin; @date_to = params[:date_to].to_date; rescue; end
end
@date_from ||= Date.civil(Date.today.year, 1, 1)
@date_to ||= Date.civil(Date.today.year, Date.today.month+1, 1) - 1
unless @criterias.empty?
sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name}"
sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE (%s)" % @project.project_condition(Setting.display_subprojects_issues?)
sql << " AND (%s)" % Project.allowed_to_condition(User.current, :view_time_entries)
sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
@hours = ActiveRecord::Base.connection.select_all(sql)
@@ -92,101 +78,54 @@ class TimelogController < ApplicationController
row['month'] = "#{row['tyear']}-#{row['tmonth']}"
when 'week'
row['week'] = "#{row['tyear']}-#{row['tweek']}"
when 'day'
row['day'] = "#{row['spent_on']}"
end
end
@total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
@periods = []
# Date#at_beginning_of_ not supported in Rails 1.2.x
date_from = @from.to_time
# 100 columns max
while date_from <= @to.to_time && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = (date_from + 1.year).at_beginning_of_year
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = (date_from + 1.month).at_beginning_of_month
when 'week'
@periods << "#{date_from.year}-#{date_from.to_date.cweek}"
date_from = (date_from + 7.day).at_beginning_of_week
when 'day'
@periods << "#{date_from.to_date}"
date_from = date_from + 1.day
end
end
@periods = []
date_from = @date_from
# 100 columns max
while date_from < @date_to && @periods.length < 100
case @columns
when 'year'
@periods << "#{date_from.year}"
date_from = date_from >> 12
when 'month'
@periods << "#{date_from.year}-#{date_from.month}"
date_from = date_from >> 1
when 'week'
@periods << "#{date_from.year}-#{date_from.cweek}"
date_from = date_from + 7
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
end
render :layout => false if request.xhr?
end
def details
sort_init 'spent_on', 'desc'
sort_update
cond = ARCondition.new
cond << (@issue.nil? ? @project.project_condition(Setting.display_subprojects_issues?) :
["#{TimeEntry.table_name}.issue_id = ?", @issue.id])
@entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
@total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
@owner_id = logged_in_user ? logged_in_user.id : 0
retrieve_date_range
cond << ['spent_on BETWEEN ? AND ?', @from, @to]
TimeEntry.visible_by(User.current) do
respond_to do |format|
format.html {
# Paginate results
@entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => :tracker}],
:conditions => cond.conditions,
:order => sort_clause,
:limit => @entry_pages.items_per_page,
:offset => @entry_pages.current.offset)
@total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
render :layout => !request.xhr?
}
format.csv {
# Export all entries
@entries = TimeEntry.find(:all,
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
:conditions => cond.conditions,
:order => sort_clause)
send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
}
end
end
send_csv and return if 'csv' == params[:export]
render :action => 'details', :layout => false if request.xhr?
end
def edit
render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
render_404 and return if @time_entry && @time_entry.user != logged_in_user
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => logged_in_user, :spent_on => Date.today)
@time_entry.attributes = params[:time_entry]
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
redirect_to(params[:back_url] || {:action => 'details', :project_id => @time_entry.project})
redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
return
end
@activities = Enumeration::get_values('ACTI')
end
def destroy
render_404 and return unless @time_entry
render_403 and return unless @time_entry.editable_by?(User.current)
@time_entry.destroy
flash[:notice] = l(:notice_successful_delete)
redirect_to :back
rescue RedirectBackError
redirect_to :action => 'details', :project_id => @time_entry.project
end
private
def find_project
@@ -202,53 +141,34 @@ private
render_404
return false
end
rescue ActiveRecord::RecordNotFound
render_404
end
# Retrieves the date range based on predefined ranges or specific from/to param dates
def retrieve_date_range
@free_period = false
@from, @to = nil, nil
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
case params[:period].to_s
when 'today'
@from = @to = Date.today
when 'yesterday'
@from = @to = Date.today - 1
when 'current_week'
@from = Date.today - (Date.today.cwday - 1)%7
@to = @from + 6
when 'last_week'
@from = Date.today - 7 - (Date.today.cwday - 1)%7
@to = @from + 6
when '7_days'
@from = Date.today - 7
@to = Date.today
when 'current_month'
@from = Date.civil(Date.today.year, Date.today.month, 1)
@to = (@from >> 1) - 1
when 'last_month'
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
@to = (@from >> 1) - 1
when '30_days'
@from = Date.today - 30
@to = Date.today
when 'current_year'
@from = Date.civil(Date.today.year, 1, 1)
@to = Date.civil(Date.today.year, 12, 31)
def send_csv
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [l(:field_spent_on),
l(:field_user),
l(:field_activity),
l(:field_issue),
l(:field_hours),
l(:field_comments)
]
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
@entries.each do |entry|
fields = [l_date(entry.spent_on),
entry.user.name,
entry.activity.name,
(entry.issue ? entry.issue.id : nil),
entry.hours,
entry.comments
]
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
@free_period = true
else
# default
end
@from, @to = @to, @from if @from && @to && @from > @to
@from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today) - 1
@to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => @project.project_condition(Setting.display_subprojects_issues?)) || Date.today)
export.rewind
send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
end
end

View File

@@ -36,13 +36,15 @@ class TrackersController < ApplicationController
@tracker = Tracker.new(params[:tracker])
if request.post? and @tracker.save
# workflow copy
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
@tracker.workflows.copy(copy_from)
if params[:copy_workflow_from] && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
copy_from.workflows.each do |w|
@tracker.workflows << w.clone
end
end
flash[:notice] = l(:notice_successful_create)
redirect_to :action => 'list'
end
@trackers = Tracker.find :all, :order => 'position'
@trackers = Tracker.find :all
end
def edit
@@ -71,7 +73,7 @@ class TrackersController < ApplicationController
def destroy
@tracker = Tracker.find(params[:id])
unless @tracker.issues.empty?
flash[:error] = "This tracker contains issues and can\'t be deleted."
flash[:notice] = "This tracker contains issues and can\'t be deleted."
else
@tracker.destroy
end

View File

@@ -33,13 +33,13 @@ class UsersController < ApplicationController
sort_init 'login', 'asc'
sort_update
@status = params[:status] ? params[:status].to_i : 1
conditions = "status <> 0"
@status = params[:status] ? params[:status].to_i : 1
conditions = nil
conditions = ["status=?", @status] unless @status == 0
@user_count = User.count(:conditions => conditions)
@user_pages = Paginator.new self, @user_count,
per_page_option,
15,
params['page']
@users = User.find :all,:order => sort_clause,
:conditions => conditions,
@@ -52,13 +52,13 @@ class UsersController < ApplicationController
def add
if request.get?
@user = User.new(:language => Setting.default_language)
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
@user = User.new(params[:user])
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@user.custom_values = @custom_values
if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
@@ -72,23 +72,22 @@ class UsersController < ApplicationController
def edit
@user = User.find(params[:id])
if request.get?
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
@custom_values = UserCustomField.find(:all).collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
else
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
if params[:custom_fields]
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@user.custom_values = @custom_values
end
if @user.update_attributes(params[:user])
flash[:notice] = l(:notice_successful_update)
# Give a string to redirect_to otherwise it would use status param as the response code
redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
redirect_to :action => 'list'
end
end
@auth_sources = AuthSource.find(:all)
@roles = Role.find_all_givable
@roles = Role.find(:all, :order => 'position')
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
@membership ||= Member.new
end
@@ -110,4 +109,12 @@ class UsersController < ApplicationController
end
redirect_to :action => 'edit', :id => @user and return
end
def destroy
User.find(params[:id]).destroy
redirect_to :action => 'list'
rescue
flash[:notice] = "Unable to delete user"
redirect_to :action => 'list'
end
end

View File

@@ -17,12 +17,8 @@
class VersionsController < ApplicationController
layout 'base'
menu_item :roadmap
before_filter :find_project, :authorize
def show
end
def edit
if request.post? and @version.update_attributes(params[:version])
flash[:notice] = l(:notice_successful_update)
@@ -34,15 +30,14 @@ class VersionsController < ApplicationController
@version.destroy
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
rescue
flash[:error] = "Unable to delete version"
flash[:notice] = "Unable to delete version"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
def download
@attachment = @version.attachments.find(params[:attachment_id])
@attachment.increment_download
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type
send_file @attachment.diskfile, :filename => @attachment.filename
rescue
render_404
end
@@ -52,13 +47,6 @@ class VersionsController < ApplicationController
flash[:notice] = l(:notice_successful_delete)
redirect_to :controller => 'projects', :action => 'list_files', :id => @project
end
def status_by
respond_to do |format|
format.html { render :action => 'show' }
format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
end
end
private
def find_project

View File

@@ -20,7 +20,7 @@ class WatchersController < ApplicationController
before_filter :require_login, :find_project, :check_project_privacy
def add
user = User.current
user = logged_in_user
@watched.add_watcher(user)
respond_to do |format|
format.html { render :text => 'Watcher added.', :layout => true }
@@ -29,7 +29,7 @@ class WatchersController < ApplicationController
end
def remove
user = User.current
user = logged_in_user
@watched.remove_watcher(user)
respond_to do |format|
format.html { render :text => 'Watcher removed.', :layout => true }

View File

@@ -19,7 +19,7 @@ class WelcomeController < ApplicationController
layout 'base'
def index
@news = News.latest User.current
@projects = Project.latest User.current
@news = News.latest logged_in_user
@projects = Project.latest logged_in_user
end
end

View File

@@ -19,7 +19,8 @@ require 'diff'
class WikiController < ApplicationController
layout 'base'
before_filter :find_wiki, :authorize
before_filter :find_wiki, :check_project_privacy
before_filter :authorize, :only => [:destroy, :add_attachment, :destroy_attachment]
verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index }
@@ -31,13 +32,8 @@ class WikiController < ApplicationController
page_title = params[:page]
@page = @wiki.find_or_new_page(page_title)
if @page.new_record?
if User.current.allowed_to?(:edit_wiki_pages, @project)
edit
render :action => 'edit'
else
render_404
end
return
edit
render :action => 'edit' and return
end
@content = @page.content_for_version(params[:version])
if params[:export] == 'html'
@@ -61,7 +57,7 @@ class WikiController < ApplicationController
# don't keep previous comment
@content.comments = nil
if request.post?
if !@page.new_record? && @content.text == params[:content][:text]
if @content.text == params[:content][:text]
# don't save if text wasn't changed
redirect_to :action => 'index', :id => @project, :page => @page.title
return
@@ -69,7 +65,7 @@ class WikiController < ApplicationController
#@content.text = params[:content][:text]
#@content.comments = params[:content][:comments]
@content.attributes = params[:content]
@content.author = User.current
@content.author = logged_in_user
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
redirect_to :action => 'index', :id => @project, :page => @page.title
@@ -77,19 +73,7 @@ class WikiController < ApplicationController
end
rescue ActiveRecord::StaleObjectError
# Optimistic locking exception
flash[:error] = l(:notice_locking_conflict)
end
# rename a page
def rename
@page = @wiki.find_page(params[:page])
@page.redirect_existing_links = true
# used to display the *original* title if some AR validation errors occur
@original_title = @page.pretty_title
if request.post? && @page.update_attributes(params[:wiki_page])
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'index', :id => @project, :page => @page.title
end
flash[:notice] = l(:notice_locking_conflict)
end
# show page history
@@ -97,7 +81,7 @@ class WikiController < ApplicationController
@page = @wiki.find_page(params[:page])
@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, 25, params['p']
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, author_id, comments, updated_on, version",
@@ -114,11 +98,6 @@ class WikiController < ApplicationController
render_404 unless @diff
end
def annotate
@page = @wiki.find_page(params[:page])
@annotate = @page.annotate(params[:version])
end
# remove a wiki page and its history
def destroy
@page = @wiki.find_page(params[:page])
@@ -131,12 +110,11 @@ class WikiController < ApplicationController
page_title = params[:page].downcase
case page_title
# show pages index, sorted by title
when 'page_index', 'date_index'
when 'page_index'
# eager load information about last updates, without loading text
@pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
# export wiki to a single html file
when 'export'
@pages = @wiki.pages.find :all, :order => 'title'
@@ -154,12 +132,16 @@ class WikiController < ApplicationController
page = @wiki.find_page(params[:page])
@attachements = page.attachments if page
@text = params[:content][:text]
render :partial => 'common/preview'
render :partial => 'preview'
end
def add_attachment
@page = @wiki.find_page(params[:page])
attach_files(@page, params[:attachments])
# Save the attachments
params[:attachments].each { |file|
next unless file.size > 0
a = Attachment.create(:container => @page, :file => file, :author => logged_in_user)
} if params[:attachments] and params[:attachments].is_a? Array
redirect_to :action => 'index', :page => @page.title
end

View File

@@ -15,30 +15,51 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
class RedCloth
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
def hard_break( text )
text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
end
end
def current_role
@current_role ||= User.current.role_for_project(@project)
module ApplicationHelper
# Return current logged in user or nil
def loggedin?
@logged_in_user
end
# Return true if user is logged in and is admin, otherwise false
def admin_loggedin?
@logged_in_user and @logged_in_user.admin?
end
# Return true if user is authorized for controller/action, otherwise false
def authorize_for(controller, action)
User.current.allowed_to?({:controller => controller, :action => action}, @project)
def authorize_for(controller, action)
# check if action is allowed on public projects
if @project.is_public? and Permission.allowed_to_public "%s/%s" % [ controller, action ]
return true
end
# check if user is authorized
if @logged_in_user and (@logged_in_user.admin? or Permission.allowed_to_role( "%s/%s" % [ controller, action ], @logged_in_user.role_for_project(@project) ) )
return true
end
return false
end
# Display a link if user is authorized
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
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], options[:action])
end
# Display a link to user's account page
def link_to_user(user)
user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
link_to user.name, :controller => 'account', :action => 'show', :id => user
end
def link_to_issue(issue, options={})
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
def link_to_issue(issue)
link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
end
def toggle_link(name, id, options={})
@@ -48,14 +69,6 @@ module ApplicationHelper
link_to(name, "#", :onclick => onclick)
end
def show_and_goto_link(name, id, options={})
onclick = "Element.show('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
onclick << "Element.scrollTo('#{id}'); "
onclick << "return false;"
link_to(name, "#", options.merge(:onclick => onclick))
end
def image_to_function(name, function, html_options = {})
html_options.symbolize_keys!
tag(:input, html_options.merge({
@@ -64,48 +77,17 @@ module ApplicationHelper
}))
end
def prompt_to_remote(name, text, param, url, html_options = {})
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
link_to name, {}, html_options
end
def format_date(date)
return nil unless date
# "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
date.strftime(@date_format)
@date_format_setting ||= Setting.date_format.to_i
@date_format_setting == 0 ? l_date(date) : date.strftime("%Y-%m-%d")
end
def format_time(time, include_date = true)
def format_time(time)
return nil unless time
@date_format_setting ||= Setting.date_format.to_i
time = time.to_time if time.is_a?(String)
zone = User.current.time_zone
if time.utc?
local = zone ? zone.adjust(time) : time.getlocal
else
local = zone ? zone.adjust(time.getutc) : time
end
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
end
def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end
def authoring(created, author)
time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
l(:label_added_time_by, author || 'Anonymous', time_tag)
end
def l_or_humanize(s)
l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize
@date_format_setting == 0 ? l_datetime(time) : (time.strftime("%Y-%m-%d") + ' ' + l_time(time))
end
def day_name(day)
@@ -116,261 +98,82 @@ module ApplicationHelper
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end
def pagination_links_full(paginator, count=nil, options={})
def pagination_links_full(paginator, options={}, html_options={})
page_param = options.delete(:page_param) || :page
url_param = params.dup
# don't reuse params if filters are present
url_param.clear if url_param.has_key?(:set_filter)
html = ''
html << link_to_remote(('&#171; ' + l(:label_previous)),
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.previous),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
{:update => "content", :url => options.merge(page_param => paginator.current.previous)},
{:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
html << (pagination_links_each(paginator, options) do |n|
link_to_remote(n.to_s,
{:url => {:params => url_param.merge(page_param => n)},
:update => 'content',
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => n))})
{:url => {:params => options.merge(page_param => n)}, :update => 'content'},
{:href => url_for(:params => options.merge(page_param => n))})
end || '')
html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
{:update => 'content',
:url => url_param.merge(page_param => paginator.current.next),
:complete => 'window.scrollTo(0,0)'},
{:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
unless count.nil?
html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
end
{:update => "content", :url => options.merge(page_param => paginator.current.next)},
{:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
html
end
def per_page_links(selected=nil)
url_param = params.dup
url_param.clear if url_param.has_key?(:set_filter)
# textilize text according to system settings and RedCloth availability
def textilizable(text, options = {})
return "" if text.blank?
links = Setting.per_page_options_array.collect do |n|
n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
{:href => url_for(url_param.merge(:per_page => n))})
end
links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
end
def breadcrumb(*args)
content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb')
end
def html_title(*args)
if args.empty?
title = []
title << @project.name if @project
title += @html_title if @html_title
title << Setting.app_title
title.compact.join(' - ')
else
@html_title ||= []
@html_title += args
end
end
def accesskey(s)
Redmine::AccessKeys.key_for s
end
# Formats text according to system settings.
# 2 ways to call this method:
# * with a String: textilizable(text, options)
# * with an object and one of its attribute: textilizable(issue, :description, options)
def textilizable(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
case args.size
when 1
obj = nil
text = args.shift
when 2
obj = args.shift
text = obj.send(args.shift).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
only_path = options.delete(:only_path) == false ? false : true
# when using an image link, try to use an attachment, if possible
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments
text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
style = $1
filename = $6
rf = Regexp.new(filename, Regexp::IGNORECASE)
# search for the picture in attachments
if found = attachments.detect { |att| att.filename =~ rf }
image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
alt = desc.blank? ? nil : "(#{desc})"
"!#{style}#{image_url}#{alt}!"
else
"!#{style}#{filename}!"
end
end
end
text = (Setting.text_formatting == 'textile') ?
Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
simple_format(auto_link(h(text)))
# different methods for formatting wiki links
case options[:wiki_links]
when :local
# used for local links to html files
format_wiki_link = Proc.new {|project, title| "#{title}.html" }
format_wiki_link = Proc.new {|title| "#{title}.html" }
when :anchor
# used for single-file wiki export
format_wiki_link = Proc.new {|project, title| "##{title}" }
format_wiki_link = Proc.new {|title| "##{title}" }
else
format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
if @project
format_wiki_link = Proc.new {|title| url_for :controller => 'wiki', :action => 'index', :id => @project, :page => title }
else
format_wiki_link = Proc.new {|title| title }
end
end
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
# turn wiki links into textile links:
# example:
# [[link]] -> "link":link
# [[link|title]] -> "title":link
text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) {|m| "\"#{$3 || $1}\":" + format_wiki_link.call(Wiki.titleize($1)) }
# turn issue ids into links
# example:
# #52 -> <a href="/issues/show/52">#52</a>
text = text.gsub(/#(\d+)(?=\b)/) {|m| link_to "##{$1}", :controller => 'issues', :action => 'show', :id => $1}
# turn revision ids into links (@project needed)
# example:
# r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (@project.id is 6)
text = text.gsub(/(?=\b)r(\d+)(?=\b)/) {|m| link_to "r#{$1}", :controller => 'repositories', :action => 'revision', :id => @project.id, :rev => $1} if @project
# Wiki links
#
# Examples:
# [[mypage]]
# [[mypage|mytext]]
# wiki links can refer other project wikis, using project name or identifier:
# [[project:]] -> wiki starting page
# [[project:|mytext]]
# [[project:mypage]]
# [[project:mypage|mytext]]
text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
link_project = project
esc, all, page, title = $1, $2, $3, $5
if esc.nil?
if page =~ /^([^\:]+)\:(.*)$/
link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
page = $2
title ||= $1 if page.blank?
end
if link_project && link_project.wiki
# check if page exists
wiki_page = link_project.wiki.find_page(page)
link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
:class => ('wiki-page' + (wiki_page ? '' : ' new')))
# when using an image link, try to use an attachment, if possible
attachments = options[:attachments]
if attachments
text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
align = $1
filename = $2
rf = Regexp.new(filename, Regexp::IGNORECASE)
# search for the picture in attachments
if found = attachments.detect { |att| att.filename =~ rf }
image_url = url_for :controller => 'attachments', :action => 'show', :id => found.id
"!#{align}#{image_url}!"
else
# project or wiki doesn't exist
title || page
"!#{align}#{filename}!"
end
else
all
end
end
# Redmine links
#
# Examples:
# Issues:
# #52 -> Link to issue #52
# Changesets:
# r52 -> Link to revision 52
# commit:a85130f -> Link to scmid starting with a85130f
# Documents:
# document#17 -> Link to document with id 17
# document:Greetings -> Link to the document with title "Greetings"
# document:"Some document" -> Link to the document with title "Some document"
# Versions:
# version#3 -> Link to version with id 3
# version:1.0.0 -> Link to version named "1.0.0"
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
# Attachments:
# attachment:file.zip -> Link to the attachment of the current object named file.zip
# Source files:
# source:some/file -> Link to the file located at /some/file in the project's repository
# source:some/file@52 -> Link to the file's revision 52
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil
if esc.nil?
if prefix.nil? && sep == 'r'
if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, 100))
end
elsif sep == '#'
oid = oid.to_i
case prefix
when nil
if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
:class => (issue.closed? ? 'issue closed' : 'issue'),
:title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
link = content_tag('del', link) if issue.closed?
end
when 'document'
if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
:class => 'document'
end
when 'version'
if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version'
end
end
elsif sep == ':'
# removes the double quotes if any
name = oid.gsub(%r{^"(.*)"$}, "\\1")
case prefix
when 'document'
if project && document = project.documents.find_by_title(name)
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
:class => 'document'
end
when 'version'
if project && version = project.versions.find_by_name(name)
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version'
end
when 'commit'
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
:class => 'changeset',
:title => truncate_single_line(changeset.comments, 100)
end
when 'source', 'export'
if project && project.repository
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
path, rev, anchor = $1, $3, $5
link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :path => path,
:rev => rev,
:anchor => anchor,
:format => (prefix == 'export' ? 'raw' : nil)},
:class => (prefix == 'export' ? 'source download' : 'source')
end
when 'attachment'
if attachments && attachment = attachments.detect {|a| a.filename == name }
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
:class => 'attachment'
end
end
end
end
leading + (link || "#{prefix}#{sep}#{oid}")
end
text
# finally textilize text
@do_textilize ||= (Setting.text_formatting == 'textile') && (ActionView::Helpers::TextHelper.method_defined? "textilize")
text = @do_textilize ? auto_link(RedCloth.new(text, [:hard_breaks]).to_html) : simple_format(auto_link(h(text)))
end
# Same as Rails' simple_format helper without using paragraphs
@@ -408,7 +211,7 @@ module ApplicationHelper
end
content_tag("div",
content_tag(
options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
options[:header_tag] || "h2", lwr(:gui_validation_error, full_messages.length) + " :"
) +
content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
"id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
@@ -420,7 +223,7 @@ module ApplicationHelper
def lang_options_for_select(blank=true)
(blank ? [["(auto)", ""]] : []) +
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
end
def label_tag_for(name, option_tags = nil, options = {})
@@ -430,88 +233,58 @@ module ApplicationHelper
def labelled_tabular_form_for(name, object, options, &proc)
options[:html] ||= {}
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
options[:html].store :class, "tabular"
form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
end
def back_url_hidden_field_tag
hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER'])
end
def check_all_links(form_name)
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
" | " +
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
end
def progress_bar(pcts, options={})
pcts = [pcts, pcts] unless pcts.is_a?(Array)
pcts[1] = pcts[1] - pcts[0]
pcts << (100 - pcts[1] - pcts[0])
width = options[:width] || '100px;'
legend = options[:legend] || ''
content_tag('table',
content_tag('tr',
(pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
(pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
(pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
), :class => 'progress', :style => "width: #{width};") +
content_tag('p', legend, :class => 'pourcent')
end
def context_menu_link(name, url, options={})
options[:class] ||= ''
if options.delete(:selected)
options[:class] << ' icon-checked disabled'
options[:disabled] = true
end
if options.delete(:disabled)
options.delete(:method)
options.delete(:confirm)
options.delete(:onclick)
options[:class] << ' disabled'
url = '#'
end
link_to name, url, options
end
def calendar_for(field_id)
include_calendar_headers_tags
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
end
def include_calendar_headers_tags
unless @calendar_headers_tags_included
@calendar_headers_tags_included = true
content_for :header_tags do
javascript_include_tag('calendar/calendar') +
javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
javascript_include_tag('calendar/calendar-setup') +
stylesheet_link_tag('calendar')
end
end
end
def wikitoolbar_for(field_id)
return '' unless Setting.text_formatting == 'textile'
help_link = l(:setting_text_formatting) + ': ' +
link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
:onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
javascript_include_tag('jstoolbar/jstoolbar') +
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
end
def content_for(name, content = nil, &block)
@has_content ||= {}
@has_content[name] = true
super(name, content, &block)
end
def has_content?(name)
(@has_content && @has_content[name]) || false
javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
end
end
class TabularFormBuilder < ActionView::Helpers::FormBuilder
include GLoc
def initialize(object_name, object, template, options, proc)
set_language_if_valid options.delete(:lang)
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
end
(field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
src = <<-END_SRC
def #{selector}(field, options = {})
return super if options.delete :no_label
label_text = l(options[:label]) if options[:label]
label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
label = @template.content_tag("label", label_text,
:class => (@object && @object.errors[field] ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
label + super
end
END_SRC
class_eval src, __FILE__, __LINE__
end
def select(field, choices, options = {}, html_options = {})
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
label = @template.content_tag("label", label_text,
:class => (@object && @object.errors[field] ? "error" : nil),
:for => (@object_name.to_s + "_" + field.to_s))
label + super
end
end

View File

@@ -17,13 +17,6 @@
module CustomFieldsHelper
def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{:name => 'ProjectCustomField', :label => :label_project_plural},
{:name => 'UserCustomField', :label => :label_user_plural}
]
end
# Return custom field html tag corresponding to its format
def custom_field_tag(custom_value)
custom_field = custom_value.custom_field
@@ -31,17 +24,17 @@ module CustomFieldsHelper
field_id = "custom_fields_#{custom_field.id}"
case custom_field.field_format
when "string", "int"
text_field 'custom_value', 'value', :name => field_name, :id => field_id
when "date"
text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) +
calendar_for(field_id)
when "text"
text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%'
text_area 'custom_value', 'value', :name => field_name, :id => field_id, :cols => 60, :rows => 3
when "bool"
check_box 'custom_value', 'value', :name => field_name, :id => field_id
when "list"
select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id
else
text_field 'custom_value', 'value', :name => field_name, :id => field_id
end
end
@@ -69,7 +62,7 @@ module CustomFieldsHelper
return "" unless value && !value.empty?
case field_format
when "date"
begin; format_date(value.to_date); rescue; value end
begin; l_date(value.to_date); rescue; value end
when "bool"
l_YesNo(value == "1")
else

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# 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
@@ -15,9 +15,5 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class EnabledModule < ActiveRecord::Base
belongs_to :project
validates_presence_of :name
validates_uniqueness_of :name, :scope => :project_id
module FeedsHelper
end

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# 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
@@ -15,9 +15,5 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WikiRedirect < ActiveRecord::Base
belongs_to :wiki
validates_presence_of :title, :redirects_to
validates_length_of :title, :redirects_to, :maximum => 255
module HelpHelper
end

View File

@@ -27,18 +27,13 @@ module IfpdfHelper
def initialize(lang)
super()
set_language_if_valid lang
case current_language.to_s
when 'ja'
case current_language
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'
when :zh
extend(PDF_Chinese)
AddBig5Font()
@font_for_content = 'Big5'
@@ -57,16 +52,11 @@ module IfpdfHelper
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(/\\/, "\\\\\\\\")
@ic.iconv(txt)
rescue
txt
end || ''
end
super w,h,txt,border,ln,align,fill,link
end

View File

@@ -15,36 +15,7 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'csv'
module IssuesHelper
include ApplicationHelper
def render_issue_tooltip(issue)
@cached_label_start_date ||= l(:field_start_date)
@cached_label_due_date ||= l(:field_due_date)
@cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority)
link_to_issue(issue) + ": #{h(issue.subject)}<br /><br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
"<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
"<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
end
def sidebar_queries
unless @sidebar_queries
# User can see public queries and his own queries
visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
# Project specific queries and global queries
visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
@sidebar_queries = Query.find(:all,
:order => "name ASC",
:conditions => visible.conditions)
end
@sidebar_queries
end
def show_detail(detail, no_html=false)
case detail.property
@@ -89,18 +60,13 @@ module IssuesHelper
label = content_tag('strong', label)
old_value = content_tag("i", h(old_value)) if detail.old_value
old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
# Link to the attachment if it has not been removed
value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
else
value = content_tag("i", h(value)) if value
end
value = content_tag("i", h(value)) if value
end
if !detail.value.blank?
if detail.value and !detail.value.to_s.empty?
case detail.property
when 'attr', 'cf'
if !detail.old_value.blank?
if old_value
label + " " + l(:text_journal_changed, old_value, value)
else
label + " " + l(:text_journal_set_to, value)
@@ -117,61 +83,4 @@ module IssuesHelper
end
end
end
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [ "#",
l(:field_status),
l(:field_project),
l(:field_tracker),
l(:field_priority),
l(:field_subject),
l(:field_assigned_to),
l(:field_category),
l(:field_fixed_version),
l(:field_author),
l(:field_start_date),
l(:field_due_date),
l(:field_done_ratio),
l(:field_estimated_hours),
l(:field_created_on),
l(:field_updated_on)
]
# Export project custom fields if project is given
# otherwise export custom fields marked as "For all projects"
custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
custom_fields.each {|f| headers << f.name}
# Description in the last column
headers << l(:field_description)
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
issues.each do |issue|
fields = [issue.id,
issue.status.name,
issue.project.name,
issue.tracker.name,
issue.priority.name,
issue.subject,
issue.assigned_to,
issue.category,
issue.fixed_version,
issue.author.name,
format_date(issue.start_date),
format_date(issue.due_date),
issue.done_ratio,
issue.estimated_hours,
format_time(issue.created_on),
format_time(issue.updated_on)
]
custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
fields << issue.description
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
end

View File

@@ -1,37 +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 JournalsHelper
def render_notes(journal, options={})
content = ''
editable = journal.editable_by?(User.current)
if editable && !journal.notes.blank?
links = []
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
:title => l(:button_edit))
content << content_tag('div', links.join(' '), :class => 'contextual')
end
content << textilizable(journal, :notes)
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki'))
end
def link_to_in_place_notes_editor(text, field_id, url, options={})
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
link_to text, '#', options.merge(:onclick => onclick)
end
end

View File

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

View File

@@ -1,53 +1,6 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# 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 QueriesHelper
def operators_for_select(filter_type)
Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
end
def column_header(column)
column.sortable ? sort_header_tag(column.sortable, :caption => column.caption,
:default_order => column.default_order) :
content_tag('th', column.caption)
end
def column_content(column, issue)
if column.is_a?(QueryCustomFieldColumn)
cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
show_value(cv)
else
value = issue.send(column.name)
if value.is_a?(Date)
format_date(value)
elsif value.is_a?(Time)
format_time(value)
else
case column.name
when :subject
h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
when :done_ratio
progress_bar(value, :width => '80px')
else
h(value)
end
end
end
end
end

View File

@@ -29,8 +29,4 @@ module ReportsHelper
a
end
def aggregate_link(data, criteria, *args)
a = aggregate data, criteria
a > 0 ? link_to(a, *args) : '-'
end
end

View File

@@ -15,64 +15,32 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'coderay'
require 'coderay/helpers/file_type'
require 'iconv'
module RepositoriesHelper
def syntax_highlight(name, content)
type = CodeRay::FileType[name]
type ? CodeRay.scan(content, type).html : h(content)
end
def format_revision(txt)
txt.to_s[0,8]
end
def to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@encodings.each do |encoding|
begin
return Iconv.conv('UTF-8', encoding, str)
rescue Iconv::Failure
# do nothing here and try the next encoding
end
end
str
end
def repository_field_tags(form, repository)
method = repository.class.name.demodulize.underscore + "_field_tags"
send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method)
end
def scm_select_tag(repository)
def scm_select_tag
container = [[]]
REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
select_tag('repository_scm',
options_for_select(container, repository.class.name.demodulize),
:disabled => (repository && !repository.new_record?),
:onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
options_for_select(container, @project.repository.class.name.demodulize),
:disabled => (@project.repository && !@project.repository.new_record?),
:onchange => remote_function(:update => "repository_fields", :url => { :controller => 'repositories', :action => 'update_form', :id => @project }, :with => "Form.serialize(this.form)")
)
end
def with_leading_slash(path)
path.to_s.starts_with?('/') ? path : "/#{path}"
end
def without_leading_slash(path)
path.gsub(%r{^/+}, '')
path ||= ''
path.starts_with?("/") ? "/#{path}" : path
end
def subversion_field_tags(form, repository)
content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
'<br />(http://, https://, svn://, file:///)') +
content_tag('p', form.text_field(:login, :size => 30)) +
content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
:onfocus => "this.value=''; this.name='repository[password]';",
:onchange => "this.name='repository[password]';"))
content_tag('p', form.password_field(:password, :size => 30))
end
def darcs_field_tags(form, repository)
@@ -83,16 +51,8 @@ module RepositoriesHelper
content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
end
def git_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
end
def cvs_field_tags(form, repository)
content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
end
def bazaar_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
end
end

View File

@@ -17,21 +17,11 @@
module SearchHelper
def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty?
return text unless tokens && !tokens.empty?
regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE
result = ''
text.split(regexp).each_with_index do |words, i|
if result.length > 1200
# maximum length of the preview reached
result << '...'
break
end
if i.even?
result << h(words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words)
else
t = (tokens.index(words.downcase) || 0) % 4
result << content_tag('span', h(words), :class => "highlight token-#{t}")
end
result << (i.even? ? (words.length > 100 ? "#{words[0..44]} ... #{words[-45..-1]}" : words) : content_tag('span', words, :class => 'highlight'))
end
result
end

View File

@@ -16,12 +16,4 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module SettingsHelper
def administration_settings_tabs
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
]
end
end

View File

@@ -83,7 +83,7 @@ module SortHelper
# Use this to sort the controller's table items collection.
#
def sort_clause()
session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
session[@sort_name][:key] + ' ' + session[@sort_name][:order]
end
# Returns a link which sorts by the named column.
@@ -92,7 +92,7 @@ module SortHelper
# - The optional caption explicitly specifies the displayed link text.
# - A sort icon image is positioned to the right of the sort link.
#
def sort_link(column, caption, default_order)
def sort_link(column, caption=nil)
key, order = session[@sort_name][:key], session[@sort_name][:order]
if key == column
if order.downcase == 'asc'
@@ -104,17 +104,15 @@ module SortHelper
end
else
icon = nil
order = default_order
order = 'desc' # changed for desc order by default
end
caption = titleize(Inflector::humanize(column)) unless caption
sort_options = { :sort_key => column, :sort_order => order }
# don't reuse params if filters are present
url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
url = {:sort_key => column, :sort_order => order, :issue_id => params[:issue_id], :project_id => params[:project_id]}
link_to_remote(caption,
{:update => "content", :url => url_options},
{:href => url_for(url_options)}) +
{:update => "content", :url => url},
{:href => url_for(url)}) +
(icon ? nbsp(2) + image_tag(icon) : '')
end
@@ -139,10 +137,14 @@ module SortHelper
# </th>
#
def sort_header_tag(column, options = {})
caption = options.delete(:caption) || titleize(Inflector::humanize(column))
default_order = options.delete(:default_order) || 'asc'
options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
content_tag('th', sort_link(column, caption, default_order), options)
if options[:caption]
caption = options[:caption]
options.delete(:caption)
else
caption = titleize(Inflector::humanize(column))
end
options[:title]= "Sort by #{caption}" unless options[:title]
content_tag('th', sort_link(column, caption), options)
end
private

View File

@@ -17,7 +17,7 @@
module TimelogHelper
def select_hours(data, criteria, value)
data.select {|row| row[criteria] == value}
data.select {|row| row[criteria] == value.to_s}
end
def sum_hours(data)
@@ -27,109 +27,4 @@ module TimelogHelper
end
sum
end
def options_for_period_select(value)
options_for_select([[l(:label_all_time), 'all'],
[l(:label_today), 'today'],
[l(:label_yesterday), 'yesterday'],
[l(:label_this_week), 'current_week'],
[l(:label_last_week), 'last_week'],
[l(:label_last_n_days, 7), '7_days'],
[l(:label_this_month), 'current_month'],
[l(:label_last_month), 'last_month'],
[l(:label_last_n_days, 30), '30_days'],
[l(:label_this_year), 'current_year']],
value)
end
def entries_to_csv(entries)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
headers = [l(:field_spent_on),
l(:field_user),
l(:field_activity),
l(:field_project),
l(:field_issue),
l(:field_tracker),
l(:field_subject),
l(:field_hours),
l(:field_comments)
]
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
entries.each do |entry|
fields = [l_date(entry.spent_on),
entry.user,
entry.activity,
entry.project,
(entry.issue ? entry.issue.id : nil),
(entry.issue ? entry.issue.tracker : nil),
(entry.issue ? entry.issue.subject : nil),
entry.hours,
entry.comments
]
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
export.rewind
export
end
def format_criteria_value(criteria, value)
value.blank? ? l(:label_none) : ((k = @available_criterias[criteria][:klass]) ? k.find_by_id(value.to_i) : format_value(value, @available_criterias[criteria][:format]))
end
def report_to_csv(criterias, periods, hours)
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# Column headers
headers = criterias.collect {|criteria| l(@available_criterias[criteria][:label]) }
headers += periods
headers << l(:label_total)
csv << headers.collect {|c| to_utf8(c) }
# Content
report_criteria_to_csv(csv, criterias, periods, hours)
# Total row
row = [ l(:label_total) ] + [''] * (criterias.size - 1)
total = 0
periods.each do |period|
sum = sum_hours(select_hours(hours, @columns, period.to_s))
total += sum
row << (sum > 0 ? "%.2f" % sum : '')
end
row << "%.2f" %total
csv << row
end
export.rewind
export
end
def report_criteria_to_csv(csv, criterias, periods, hours, level=0)
hours.collect {|h| h[criterias[level]].to_s}.uniq.each do |value|
hours_for_value = select_hours(hours, criterias[level], value)
next if hours_for_value.empty?
row = [''] * level
row << to_utf8(format_criteria_value(criterias[level], value))
row += [''] * (criterias.length - level - 1)
total = 0
periods.each do |period|
sum = sum_hours(select_hours(hours_for_value, @columns, period.to_s))
total += sum
row << (sum > 0 ? "%.2f" % sum : '')
end
row << "%.2f" %total
csv << row
if criterias.length > level + 1
report_criteria_to_csv(csv, criterias, periods, hours_for_value, level + 1)
end
end
end
def to_utf8(s)
@ic ||= Iconv.new(l(:general_csv_encoding), 'UTF-8')
begin; @ic.iconv(s.to_s); rescue; s.to_s; end
end
end

View File

@@ -17,21 +17,9 @@
module UsersHelper
def status_options_for_select(selected)
options_for_select([[l(:label_all), ''],
options_for_select([[l(:label_all), "*"],
[l(:status_active), 1],
[l(:status_registered), 2],
[l(:status_locked), 3]], selected)
end
def change_status_link(user)
url = {:action => 'edit', :id => user, :page => params[:page], :status => params[:status]}
if user.locked?
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user.registered?
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user != User.current
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
end
end
end

View File

@@ -16,32 +16,4 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module VersionsHelper
STATUS_BY_CRITERIAS = %w(category tracker priority author assigned_to)
def render_issue_status_by(version, criteria)
criteria ||= 'category'
raise 'Unknown criteria' unless STATUS_BY_CRITERIAS.include?(criteria)
h = Hash.new {|k,v| k[v] = [0, 0]}
begin
# Total issue count
Issue.count(:group => criteria,
:conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
# Open issues count
Issue.count(:group => criteria,
:include => :status,
:conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s}
rescue ActiveRecord::RecordNotFound
# When grouping by an association, Rails throws this exception if there's no result (bug)
end
counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
max = counts.collect {|c| c[:total]}.max
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
end
def status_by_options_for_select(value)
options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
end
end

View File

@@ -21,7 +21,7 @@ module WatchersHelper
end
def watcher_link(object, user)
return '' unless user && user.logged? && object.respond_to?('watched_by?')
return '' unless user && object.respond_to?('watched_by?')
watched = object.watched_by?(user)
url = {:controller => 'watchers',
:action => (watched ? 'remove' : 'add'),

View File

@@ -21,13 +21,8 @@ class Attachment < ActiveRecord::Base
belongs_to :container, :polymorphic => true
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
validates_presence_of :container, :filename, :author
validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255
acts_as_event :title => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}}
validates_presence_of :container, :filename
cattr_accessor :storage_path
@@storage_path = "#{RAILS_ROOT}/files"
@@ -35,82 +30,67 @@ class Attachment < ActiveRecord::Base
errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
end
def file=(incoming_file)
unless incoming_file.nil?
@temp_file = incoming_file
if @temp_file.size > 0
self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = Attachment.disk_filename(filename)
self.content_type = @temp_file.content_type.to_s.chomp
self.filesize = @temp_file.size
end
end
end
def file=(incomming_file)
unless incomming_file.nil?
@temp_file = incomming_file
if @temp_file.size > 0
self.filename = sanitize_filename(@temp_file.original_filename)
self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename
self.content_type = @temp_file.content_type
self.filesize = @temp_file.size
end
end
end
def file
nil
end
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
File.open(diskfile, "wb") do |f|
f.write(@temp_file.read)
end
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
end
# Don't save the content type if it's longer than the authorized length
if self.content_type && self.content_type.length > 255
self.content_type = nil
end
end
# Deletes file on the disk
def after_destroy
if self.filename?
File.delete(diskfile) if File.exist?(diskfile)
end
end
# Returns file's location on disk
def diskfile
"#{@@storage_path}/#{self.disk_filename}"
end
def file
nil
end
# Copy temp file to its final location
def before_save
if @temp_file && (@temp_file.size > 0)
logger.debug("saving '#{self.diskfile}'")
File.open(diskfile, "wb") do |f|
f.write(@temp_file.read)
end
self.digest = Digest::MD5.hexdigest(File.read(diskfile))
end
end
# Deletes file on the disk
def after_destroy
if self.filename?
File.delete(diskfile) if File.exist?(diskfile)
end
end
# Returns file's location on disk
def diskfile
"#{@@storage_path}/#{self.disk_filename}"
end
def increment_download
increment!(:downloads)
end
# returns last created projects
def self.most_downloaded
find(:all, :limit => 5, :order => "downloads DESC")
end
def project
container.project
end
def image?
self.filename =~ /\.(jpe?g|gif|png)$/i
container.is_a?(Project) ? container : container.project
end
private
def sanitize_filename(value)
# get only the filename, not the whole path
just_filename = value.gsub(/^.*(\\|\/)/, '')
# NOTE: File.basename doesn't work right with Windows paths on Unix
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
# get only the filename, not the whole path
just_filename = value.gsub(/^.*(\\|\/)/, '')
# NOTE: File.basename doesn't work right with Windows paths on Unix
# INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
# Finally, replace all non alphanumeric, hyphens or periods with underscore
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
end
# Returns an ASCII or hashed filename
def self.disk_filename(filename)
df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
df << filename
else
df << Digest::MD5.hexdigest(filename)
# keep the extension if any
df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
end
df
# Finally, replace all non alphanumeric, underscore or periods with underscore
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
end
end

View File

@@ -20,10 +20,6 @@ class AuthSource < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :host, :maximum => 60
validates_length_of :account_password, :maximum => 60, :allow_nil => true
validates_length_of :account, :base_dn, :maximum => 255
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30
def authenticate(login, password)
end

View File

@@ -20,14 +20,12 @@ require 'iconv'
class AuthSourceLdap < AuthSource
validates_presence_of :host, :port, :attr_login
validates_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? }
def after_initialize
self.port = 389 if self.port == 0
end
def authenticate(login, password)
return nil if login.blank? || password.blank?
attrs = []
# get user's DN
ldap_con = initialize_ldap_con(self.account, self.account_password)
@@ -36,13 +34,12 @@ class AuthSourceLdap < AuthSource
dn = String.new
ldap_con.search( :base => self.base_dn,
:filter => object_filter & login_filter,
# only ask for the DN if on-the-fly registration is disabled
:attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry|
:attributes=> ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]) do |entry|
dn = entry.dn
attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
:auth_source_id => self.id ] if onthefly_register?
:auth_source_id => self.id ]
end
return nil if dn.empty?
logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
@@ -70,12 +67,10 @@ class AuthSourceLdap < AuthSource
private
def initialize_ldap_con(ldap_user, ldap_password)
options = { :host => self.host,
:port => self.port,
:encryption => (self.tls ? :simple_tls : nil)
}
options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
Net::LDAP.new options
Net::LDAP.new( {:host => self.host,
:port => self.port,
:auth => { :method => :simple, :username => Iconv.new('iso-8859-15', 'utf-8').iconv(ldap_user), :password => Iconv.new('iso-8859-15', 'utf-8').iconv(ldap_password) }}
)
end
def self.get_attr(entry, attr_name)

View File

@@ -19,8 +19,4 @@ class Change < ActiveRecord::Base
belongs_to :changeset
validates_presence_of :changeset_id, :action, :path
def relative_path
changeset.repository.relative_path(path)
end
end

View File

@@ -19,65 +19,35 @@ class Changeset < ActiveRecord::Base
belongs_to :repository
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
:description => :comments,
:datetime => :committed_on,
:author => :committer,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
:include => :repository,
:project_key => "#{Repository.table_name}.project_id",
:date_column => 'committed_on'
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_numericality_of :revision, :only_integer => true
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
def revision=(r)
write_attribute :revision, (r.nil? ? nil : r.to_s)
end
def comments=(comment)
write_attribute(:comments, comment.strip)
end
def committed_on=(date)
self.commit_date = date
super
end
def project
repository.project
end
def after_create
scan_comment_for_issue_ids
end
require 'pp'
def scan_comment_for_issue_ids
return if comments.blank?
# keywords used to reference issues
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
ref_keywords = Setting.commit_ref_keywords.downcase.split(",")
# keywords used to fix issues
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
# status and optional done ratio applied
fix_keywords = Setting.commit_fix_keywords.downcase.split(",")
# status applied
fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw.strip)}.join("|")
return if kw_regexp.blank?
referenced_issues = []
if ref_keywords.delete('*')
# find any issue ID in the comments
target_issue_ids = []
comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
end
# remove any associated issues
self.issues.clear
comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
action = match[0]
@@ -87,45 +57,13 @@ class Changeset < ActiveRecord::Base
# update status of issues
logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
target_issues.each do |issue|
# the issue may have been updated by the closure of another one (eg. duplicate)
issue.reload
# don't change the status is the issue is closed
# don't change the status is the issue is already closed
next if issue.status.is_closed?
user = committer_user || User.anonymous
csettext = "r#{self.revision}"
if self.scmid && (! (csettext =~ /^r[0-9]+$/))
csettext = "commit:\"#{self.scmid}\""
end
journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
issue.status = fix_status
issue.done_ratio = done_ratio if done_ratio
issue.save
Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
end
end
referenced_issues += target_issues
self.issues << target_issues
end
self.issues = referenced_issues.uniq
end
# Returns the Redmine User corresponding to the committer
def committer_user
if committer && committer.strip =~ /^([^<]+)(<(.*)>)?$/
username, email = $1.strip, $3
u = User.find_by_login(username)
u ||= User.find_by_mail(email) unless email.blank?
u
end
end
# Returns the previous changeset
def previous
@previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
end
# Returns the next changeset
def next
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
end
end

View File

@@ -17,21 +17,18 @@
class CustomField < ActiveRecord::Base
has_many :custom_values, :dependent => :delete_all
acts_as_list :scope => 'type = \'#{self.class}\''
serialize :possible_values
FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
"text" => { :name => :label_text, :order => 2 },
"int" => { :name => :label_integer, :order => 3 },
"float" => { :name => :label_float, :order => 4 },
"list" => { :name => :label_list, :order => 5 },
"date" => { :name => :label_date, :order => 6 },
"bool" => { :name => :label_boolean, :order => 7 }
"list" => { :name => :label_list, :order => 4 },
"date" => { :name => :label_date, :order => 5 },
"bool" => { :name => :label_boolean, :order => 6 }
}.freeze
validates_presence_of :name, :field_format
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
@@ -43,9 +40,6 @@ class CustomField < ActiveRecord::Base
def before_validation
# remove empty values
self.possible_values = self.possible_values.collect{|v| v unless v.empty?}.compact
# make sure these fields are not searchable
self.searchable = false if %w(int float date bool).include?(field_format)
true
end
def validate
@@ -53,17 +47,8 @@ class CustomField < ActiveRecord::Base
errors.add(:possible_values, :activerecord_error_blank) if self.possible_values.nil? || self.possible_values.empty?
errors.add(:possible_values, :activerecord_error_invalid) unless self.possible_values.is_a? Array
end
# validate default value
v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
v.custom_field.is_required = false
errors.add(:default_value, :activerecord_error_invalid) unless v.valid?
end
def <=>(field)
position <=> field.position
end
# to move in project_custom_field
def self.for_all
find(:all, :conditions => ["is_for_all=?", true])

View File

@@ -19,32 +19,20 @@ class CustomValue < ActiveRecord::Base
belongs_to :custom_field
belongs_to :customized, :polymorphic => true
def after_initialize
if custom_field && new_record? && (customized_type.blank? || (customized && customized.new_record?))
self.value ||= custom_field.default_value
end
end
protected
def validate
if value.blank?
errors.add(:value, :activerecord_error_blank) if custom_field.is_required? and value.blank?
else
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length
errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
# Format specific validations
case custom_field.field_format
when 'int'
errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[+-]?\d+$/
when 'float'
begin; Kernel.Float(value); rescue; errors.add(:value, :activerecord_error_invalid) end
when 'date'
errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/
when 'list'
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include?(value)
end
errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.empty?
errors.add(:value, :activerecord_error_invalid) unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
errors.add(:value, :activerecord_error_too_short) if custom_field.min_length > 0 and value.length < custom_field.min_length and value.length > 0
errors.add(:value, :activerecord_error_too_long) if custom_field.max_length > 0 and value.length > custom_field.max_length
case custom_field.field_format
when "int"
errors.add(:value, :activerecord_error_not_a_number) unless value =~ /^[0-9]*$/
when "date"
errors.add(:value, :activerecord_error_not_a_date) unless value =~ /^\d{4}-\d{2}-\d{2}$/ or value.empty?
when "list"
errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include? value or value.empty?
end
end
end

View File

@@ -20,11 +20,5 @@ class Document < ActiveRecord::Base
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_searchable :columns => ['title', 'description']
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
end

View File

@@ -16,41 +16,25 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Enumeration < ActiveRecord::Base
acts_as_list :scope => 'opt = \'#{opt}\''
before_destroy :check_integrity
validates_presence_of :opt, :name
validates_uniqueness_of :name, :scope => [:opt]
validates_length_of :name, :maximum => 30
OPTIONS = {
"IPRI" => :enumeration_issue_priorities,
"DCAT" => :enumeration_doc_categories,
"ACTI" => :enumeration_activities
}.freeze
def self.get_values(option)
find(:all, :conditions => {:opt => option}, :order => 'position')
end
def self.default(option)
find(:first, :conditions => {:opt => option, :is_default => true}, :order => 'position')
end
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
OPTIONS = {
"IPRI" => :enumeration_issue_priorities,
"DCAT" => :enumeration_doc_categories,
"ACTI" => :enumeration_activities
}.freeze
def self.get_values(option)
find(:all, :conditions => ['opt=?', option])
end
def option_name
OPTIONS[self.opt]
end
def before_save
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
end
def <=>(enumeration)
position <=> enumeration.position
end
def to_s; name end
private
def check_integrity

View File

@@ -27,7 +27,7 @@ class Issue < ActiveRecord::Base
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 => :nullify
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :custom_fields, :through => :custom_values
has_and_belongs_to_many :changesets, :order => "revision ASC"
@@ -36,68 +36,16 @@ class Issue < ActiveRecord::Base
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
acts_as_watchable
acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
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}}
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
validates_presence_of :subject, :description, :priority, :tracker, :author, :status
validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true
validates_associated :custom_values, :on => :update
def after_initialize
if new_record?
# set default values for new records only
self.status ||= IssueStatus.default
self.priority ||= Enumeration.default('IPRI')
end
# set default status for new issues
def before_validation
self.status = IssueStatus.default if status.nil?
end
def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
self.attributes = issue.attributes.dup
self.custom_values = issue.custom_values.collect {|v| v.clone}
self
end
# Move an issue to a new project and tracker
def move_to(new_project, new_tracker = nil)
transaction do
if new_project && project_id != new_project.id
# delete issue relations
unless Setting.cross_project_issue_relations?
self.relations_from.clear
self.relations_to.clear
end
# issue is moved to another project
self.category = nil
self.fixed_version = nil
self.project = new_project
end
if new_tracker
self.tracker = new_tracker
end
if save
# Manually update project_id on related time entries
TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
else
rollback_db_transaction
return false
end
end
return true
end
def priority_id=(pid)
self.priority = nil
write_attribute(:priority_id, pid)
end
def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end
def validate
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
errors.add :due_date, :activerecord_error_not_a_date
@@ -112,10 +60,6 @@ class Issue < ActiveRecord::Base
end
end
def validate_on_create
errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
end
def before_create
# default assignment based on category
if assigned_to.nil? && category && category.assigned_to
@@ -134,38 +78,17 @@ class Issue < ActiveRecord::Base
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
:value => c.value) unless @custom_values_before_change[c.custom_field_id]==c.value
}
@current_journal.save
@current_journal.save unless @current_journal.details.empty? and @current_journal.notes.empty?
end
# Save the issue even if the journal is not saved (because empty)
true
end
def after_save
# Reload is needed in order to get the right status
reload
# Update start/due dates of following issues
relations_from.each(&:set_issue_to_dates)
# Close duplicates if the issue was closed
if @issue_before_change && !@issue_before_change.closed? && self.closed?
duplicates.each do |duplicate|
# Reload is need in case the duplicate was updated by a previous duplicate
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
duplicate.init_journal(@current_journal.user, @current_journal.notes)
duplicate.update_attribute :status, self.status
end
end
end
def custom_value_for(custom_field)
@@ -176,38 +99,11 @@ class Issue < ActiveRecord::Base
def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone
@issue_before_change.status = self.status
@custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
@current_journal
end
# Return true if the issue is closed, otherwise false
def closed?
self.status.is_closed?
end
# Users the issue can be assigned to
def assignable_users
project.assignable_users
end
# Returns an array of status that user is able to apply
def new_statuses_allowed_to(user)
statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
statuses << status unless statuses.empty?
statuses.uniq.sort
end
# Returns the mail adresses of users that should be notified for the issue
def recipients
recipients = project.recipients
# Author and assignee are always notified unless they have been locked
recipients << author.mail if author && author.active?
recipients << assigned_to.mail if assigned_to && assigned_to.active?
recipients.compact.uniq
end
def spent_hours
@spent_hours ||= time_entries.sum(:hours) || 0
end
@@ -225,11 +121,6 @@ class Issue < ActiveRecord::Base
dependencies
end
# Returns an array of the duplicate issues
def duplicates
relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
end
def duration
(start_date && due_date) ? due_date - start_date : 0
end
@@ -237,14 +128,4 @@ class Issue < ActiveRecord::Base
def soonest_start
@soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
end
def self.visible_by(usr)
with_scope(:find => { :conditions => Project.visible_by(usr) }) do
yield
end
end
def to_s
"#{tracker} ##{id}: #{subject}"
end
end

View File

@@ -16,28 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueCategory < ActiveRecord::Base
before_destroy :check_integrity
belongs_to :project
belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 30
alias :destroy_without_reassign :destroy
# Destroy the category
# If a category is specified, issues are reassigned to this category
def destroy(reassign_to = nil)
if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
end
destroy_without_reassign
private
def check_integrity
raise "Can't delete category" if Issue.find(:first, :conditions => ["category_id=?", self.id])
end
def <=>(category)
name <=> category.name
end
def to_s; name end
end

View File

@@ -38,7 +38,7 @@ class IssueRelation < ActiveRecord::Base
def validate
if issue_from && issue_to
errors.add :issue_to_id, :activerecord_error_invalid if issue_from_id == issue_to_id
errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
errors.add :issue_to_id, :activerecord_error_not_same_project unless issue_from.project_id == issue_to.project_id
errors.add_to_base :activerecord_error_circular_dependency if issue_to.all_dependent_issues.include? issue_from
end
end

View File

@@ -22,8 +22,9 @@ class IssueStatus < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_length_of :html_color, :is => 6
validates_format_of :html_color, :with => /^[a-f0-9]*$/i
def before_save
IssueStatus.update_all "is_default=#{connection.quoted_false}" if self.is_default?
@@ -50,18 +51,6 @@ class IssueStatus < ActiveRecord::Base
new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
end
def new_status_allowed_to?(status, role, tracker)
status && role && tracker ?
!workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
false
end
def <=>(status)
position <=> status.position
end
def to_s; name end
private
def check_integrity
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])

View File

@@ -23,44 +23,4 @@ class Journal < ActiveRecord::Base
belongs_to :user
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
attr_accessor :indice
acts_as_searchable :columns => 'notes',
:include => :issue,
:project_key => "#{Issue.table_name}.project_id",
:date_column => "#{Issue.table_name}.created_on"
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
:description => :notes,
:author => :user,
:type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
def save
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end
# Returns the new status if the journal contains a status change, otherwise nil
def new_status
c = details.detect {|detail| detail.prop_key == 'status_id'}
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
end
def new_value_for(prop)
c = details.detect {|detail| detail.prop_key == prop}
c ? c.value : nil
end
def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
end
def project
journalized.respond_to?(:project) ? journalized.project : nil
end
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
end

View File

@@ -31,7 +31,7 @@ class MailHandler < ActionMailer::Base
user = User.find_active(:first, :conditions => {:mail => email.from.first})
return unless user
# check permission
return unless user.allowed_to?(:add_issue_notes, issue.project)
return unless Permission.allowed_to_role("issues/add_note", user.role_for_project(issue.project))
# add the note
issue.init_journal(user, email.body.chomp)

View File

@@ -16,179 +16,101 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Mailer < ActionMailer::Base
helper :application
helper :issues
helper :custom_fields
helper IssuesHelper
include ActionController::UrlWriter
def issue_add(issue)
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
body :issue => issue,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def issue_edit(journal)
issue = journal.journalized
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
recipients issue.recipients
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
s << issue.subject
subject s
body :issue => issue,
:journal => journal,
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.project.recipients
subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
body :document => document,
:document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
end
def attachments_added(attachments)
container = attachments.first.container
added_to = ''
added_to_url = ''
case container.class.name
when 'Version'
added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
added_to = "#{l(:label_version)}: #{container.name}"
when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
end
redmine_headers 'Project' => container.project.identifier
recipients container.project.recipients
subject "[#{container.project.name}] #{l(:label_attachment_new)}"
body :attachments => attachments,
:added_to => added_to,
:added_to_url => added_to_url
end
def news_added(news)
redmine_headers 'Project' => news.project.identifier
recipients news.project.recipients
subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
body :news => news,
:news_url => url_for(:controller => 'news', :action => 'show', :id => news)
end
def message_posted(message, recipients)
redmine_headers 'Project' => message.project.identifier,
'Topic-Id' => (message.parent_id || message.id)
recipients(recipients)
subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
body :message => message,
:message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
end
def account_information(user, password)
set_language_if_valid user.language
recipients user.mail
subject l(:mail_subject_register, Setting.app_title)
body :user => user,
:password => password,
:login_url => url_for(:controller => 'account', :action => 'login')
end
def account_activation_request(user)
# Send the email to all active administrators
recipients User.find_active(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
subject l(:mail_subject_account_activation_request, Setting.app_title)
body :user => user,
:url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
from Setting.mail_from
subject l(:mail_subject_register)
body :user => user, :password => password
end
def issue_add(issue)
set_language_if_valid(Setting.default_language)
# Sends to all project members
@recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }.compact
# Sends to author and assignee (even if they turned off mail notification)
@recipients << issue.author.mail if issue.author
@recipients << issue.assigned_to.mail if issue.assigned_to
@recipients.compact!
@recipients.uniq!
@from = Setting.mail_from
@subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
@body['issue'] = issue
end
def issue_edit(journal)
set_language_if_valid(Setting.default_language)
# Sends to all project members
issue = journal.journalized
@recipients = issue.project.members.collect { |m| m.user.mail if m.user.mail_notification }
# Sends to author and assignee (even if they turned off mail notification)
@recipients << issue.author.mail if issue.author
@recipients << issue.assigned_to.mail if issue.assigned_to
@recipients.compact!
@recipients.uniq!
# Watchers in cc
@cc = issue.watcher_recipients - @recipients
@from = Setting.mail_from
@subject = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}"
@body['issue'] = issue
@body['journal']= journal
end
def document_add(document)
set_language_if_valid(Setting.default_language)
@recipients = document.project.users.collect { |u| u.mail if u.mail_notification }.compact
@from = Setting.mail_from
@subject = "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
@body['document'] = document
end
def attachments_add(attachments)
set_language_if_valid(Setting.default_language)
container = attachments.first.container
url = "http://#{Setting.host_name}/"
added_to = ""
case container.class.to_s
when 'Version'
url << "projects/list_files/#{container.project_id}"
added_to = "#{l(:label_version)}: #{container.name}"
when 'Document'
url << "documents/show/#{container.id}"
added_to = "#{l(:label_document)}: #{container.title}"
when 'Issue'
url << "issues/show/#{container.id}"
added_to = "#{container.tracker.name} ##{container.id}: #{container.subject}"
end
@recipients = container.project.users.collect { |u| u.mail if u.mail_notification }.compact
@from = Setting.mail_from
@subject = "[#{container.project.name}] #{l(:label_attachment_new)}"
@body['attachments'] = attachments
@body['url'] = url
@body['added_to'] = added_to
end
def lost_password(token)
set_language_if_valid(token.user.language)
recipients token.user.mail
subject l(:mail_subject_lost_password, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
@recipients = token.user.mail
@from = Setting.mail_from
@subject = l(:mail_subject_lost_password)
@body['token'] = token
end
def register(token)
set_language_if_valid(token.user.language)
recipients token.user.mail
subject l(:mail_subject_register, Setting.app_title)
body :token => token,
:url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
@recipients = token.user.mail
@from = Setting.mail_from
@subject = l(:mail_subject_register)
@body['token'] = token
end
def test(user)
set_language_if_valid(user.language)
recipients user.mail
subject 'Redmine test'
body :url => url_for(:controller => 'welcome')
def message_posted(message, recipients)
set_language_if_valid(Setting.default_language)
@recipients = recipients
@from = Setting.mail_from
@subject = "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
@body['message'] = message
end
# Overrides default deliver! method to prevent from sending an email
# with no recipient, cc or bcc
def deliver!(mail = @mail)
return false if (recipients.nil? || recipients.empty?) &&
(cc.nil? || cc.empty?) &&
(bcc.nil? || bcc.empty?)
super
end
private
def initialize_defaults(method_name)
super
set_language_if_valid Setting.default_language
from Setting.mail_from
default_url_options[:host] = Setting.host_name
default_url_options[:protocol] = Setting.protocol
# Common headers
headers 'X-Mailer' => 'Redmine',
'X-Redmine-Host' => Setting.host_name,
'X-Redmine-Site' => Setting.app_title
end
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
def redmine_headers(h)
h.each { |k,v| headers["X-Redmine-#{k}"] = v }
end
# Overrides the create_mail method
def create_mail
# Removes the current user from the recipients and cc
# if he doesn't want to receive notifications about what he does
if User.current.pref[:no_self_notified]
recipients.delete(User.current.mail) if recipients
cc.delete(User.current.mail) if cc
end
# Blind carbon copy recipients
if Setting.bcc_recipients?
bcc([recipients, cc].flatten.compact.uniq)
recipients []
cc []
end
super
end
# Renders a message with the corresponding layout
def render_message(method_name, body)
layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
body[:content_for_layout] = render(:file => method_name, :body => body)
ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}")
end
# Makes partial rendering work with Rails 1.2 (retro-compatibility)
def self.controller_path
''
end unless respond_to?('controller_path')
end

View File

@@ -23,20 +23,12 @@ class Member < ActiveRecord::Base
validates_presence_of :role, :user, :project
validates_uniqueness_of :user_id, :scope => :project_id
def validate
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
end
def name
self.user.name
end
def <=>(member)
role == member.role ? (user <=> member.user) : (role <=> member.role)
end
def before_destroy
# remove category based auto assignments for this member
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
project.issue_categories.update_all "assigned_to_id = NULL", ["assigned_to_id = ?", self.user.id]
end
end

View File

@@ -22,24 +22,9 @@ class Message < ActiveRecord::Base
has_many :attachments, :as => :container, :dependent => :destroy
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'],
:include => :board,
:project_key => 'project_id',
:date_column => 'created_on'
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
attr_protected :locked, :sticky
validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255
def validate_on_create
# Can not reply to a locked topic
errors.add_to_base 'Topic is locked' if root.locked? && self != root
end
def after_create
board.update_attribute(:last_message_id, self.id)
board.increment! :messages_count
@@ -50,18 +35,6 @@ class Message < ActiveRecord::Base
end
end
def after_destroy
# The following line is required so that the previous counter
# updates (due to children removal) are not overwritten
board.reload
board.decrement! :messages_count
board.decrement! :topics_count unless parent
end
def sticky?
sticky == 1
end
def project
board.project
end

View File

@@ -17,13 +17,8 @@
class MessageObserver < ActiveRecord::Observer
def after_create(message)
# send notification to the authors of the thread
recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?}
# send notification to the board watchers
recipients += message.board.watcher_recipients
# send notification to project members who want to be notified
recipients += message.board.project.recipients
recipients = recipients.compact.uniq
Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted')
# send notification to board watchers
recipients = message.board.watcher_recipients
Mailer.deliver_message_posted(message, recipients) unless recipients.empty?
end
end

View File

@@ -21,12 +21,7 @@ class News < ActiveRecord::Base
has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
validates_presence_of :title, :description
validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255
acts_as_searchable :columns => ['title', 'description']
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
# returns latest news for projects visible by user
def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")

68
app/models/permission.rb Normal file
View File

@@ -0,0 +1,68 @@
# 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.
class Permission < ActiveRecord::Base
has_and_belongs_to_many :roles
validates_presence_of :controller, :action, :description
GROUPS = {
100 => :label_project,
200 => :label_member_plural,
300 => :label_version_plural,
400 => :label_issue_category_plural,
600 => :label_query_plural,
1000 => :label_issue_plural,
1100 => :label_news_plural,
1200 => :label_document_plural,
1300 => :label_attachment_plural,
1400 => :label_repository,
1500 => :label_time_tracking,
1700 => :label_wiki_page_plural,
2000 => :label_board_plural
}.freeze
@@cached_perms_for_public = nil
@@cached_perms_for_roles = nil
def name
self.controller + "/" + self.action
end
def group_id
(self.sort / 100)*100
end
def self.allowed_to_public(action)
@@cached_perms_for_public ||= find(:all, :conditions => ["is_public=?", true]).collect {|p| "#{p.controller}/#{p.action}"}
@@cached_perms_for_public.include? action
end
def self.allowed_to_role(action, role)
@@cached_perms_for_roles ||=
begin
perms = {}
find(:all, :include => :roles).each {|p| perms.store "#{p.controller}/#{p.action}", p.roles.collect {|r| r.id } }
perms
end
allowed_to_public(action) or (role && @@cached_perms_for_roles[action] && @@cached_perms_for_roles[action].include?(role.id))
end
def self.allowed_to_role_expired
@@cached_perms_for_roles = nil
end
end

View File

@@ -20,49 +20,34 @@ class Project < ActiveRecord::Base
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :members, :dependent => :delete_all, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :users, :through => :members
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
has_many :issue_changes, :through => :issues, :source => :journals
has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
has_many :time_entries, :dependent => :delete_all
has_many :queries, :dependent => :delete_all
has_many :documents, :dependent => :destroy
has_many :news, :dependent => :delete_all, :include => :author
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
has_many :boards, :dependent => :destroy, :order => "position ASC"
has_many :boards, :order => "position ASC"
has_one :repository, :dependent => :destroy
has_many :changesets, :through => :repository
has_one :wiki, :dependent => :destroy
# Custom field for the project issues
has_and_belongs_to_many :custom_fields,
:class_name => 'IssueCustomField',
:order => "#{CustomField.table_name}.position",
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
:association_foreign_key => 'custom_field_id'
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
attr_protected :status, :enabled_module_names
validates_presence_of :name, :identifier
attr_protected :status
validates_presence_of :name, :description, :identifier
validates_uniqueness_of :name, :identifier
validates_associated :custom_values, :on => :update
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
validates_length_of :homepage, :maximum => 60
validates_length_of :identifier, :in => 3..20
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
validates_length_of :description, :maximum => 255
validates_length_of :identifier, :in => 3..12
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
before_destroy :delete_all_members
def identifier=(identifier)
super unless identifier_frozen?
end
@@ -73,19 +58,16 @@ class Project < ActiveRecord::Base
def issues_with_subprojects(include_subprojects=false)
conditions = nil
if include_subprojects
ids = [id] + child_ids
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
if include_subprojects && !active_children.empty?
ids = [id] + active_children.collect {|c| c.id}
conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"]
end
conditions ||= ["#{Project.table_name}.id = ?", id]
# Quick and dirty fix for Rails 2 compatibility
Issue.send(:with_scope, :find => { :conditions => conditions }) do
Version.send(:with_scope, :find => { :conditions => conditions }) do
yield
end
conditions ||= ["#{Issue.table_name}.project_id = ?", id]
Issue.with_scope :find => { :conditions => conditions } do
yield
end
end
# returns latest created projects
# non public projects will be returned only if user is a member of those
def self.latest(user=nil, count=5)
@@ -93,61 +75,15 @@ class Project < ActiveRecord::Base
end
def self.visible_by(user=nil)
user ||= User.current
if user && user.admin?
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
return ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
elsif user && user.memberships.any?
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
return ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = ? or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))", true]
else
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
return ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = ?", true]
end
end
def self.allowed_to_condition(user, permission, options={})
statements = []
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
if options[:project]
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects]
base_statement = "(#{project_statement}) AND (#{base_statement})"
end
if user.admin?
# no restriction
elsif user.logged?
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
elsif Role.anonymous.allowed_to?(permission)
# anonymous user allowed on public project
statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
else
# anonymous user is not authorized
statements << "1=0"
end
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end
def project_condition(with_subprojects)
cond = "#{Project.table_name}.id = #{id}"
cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects
cond
end
def self.find(*args)
if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
project = find_by_identifier(*args)
raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
project
else
super
end
end
def to_param
# id is used for projects with a numeric identifier (compatibility)
@to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
end
def active?
self.status == STATUS_ACTIVE
end
@@ -169,30 +105,6 @@ class Project < ActiveRecord::Base
children.select {|child| child.active?}
end
# Returns an array of the trackers used by the project and its sub projects
def rolled_up_trackers
@rolled_up_trackers ||=
Tracker.find(:all, :include => :projects,
:select => "DISTINCT #{Tracker.table_name}.*",
:conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id],
:order => "#{Tracker.table_name}.position")
end
# Deletes all project's members
def delete_all_members
Member.delete_all(['project_id = ?', id])
end
# Users issues can be assigned to
def assignable_users
members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
end
# Returns the mail adresses of users that should be always notified on project events
def recipients
members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
end
# Returns an array of all custom fields enabled for project issues
# (explictly associated custom fields and custom fields enabled for all projects)
def custom_fields_for_issues(tracker)
@@ -202,57 +114,10 @@ class Project < ActiveRecord::Base
def all_custom_fields
@all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
end
def <=>(project)
name.downcase <=> project.name.downcase
end
def to_s
name
end
# Returns a short description of the projects (first lines)
def short_description(length = 255)
description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description
end
def allows_to?(action)
if action.is_a? Hash
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
else
allowed_permissions.include? action
end
end
def module_enabled?(module_name)
module_name = module_name.to_s
enabled_modules.detect {|m| m.name == module_name}
end
def enabled_module_names=(module_names)
enabled_modules.clear
module_names = [] unless module_names && module_names.is_a?(Array)
module_names.each do |name|
enabled_modules << EnabledModule.new(:name => name.to_s)
end
end
protected
def validate
errors.add(parent_id, " must be a root project") if parent and parent.parent
errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0
errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
end
private
def allowed_permissions
@allowed_permissions ||= begin
module_names = enabled_modules.collect {|m| m.name}
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
end
end
def allowed_actions
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
end
end

View File

@@ -15,49 +15,15 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueryColumn
attr_accessor :name, :sortable, :default_order
include GLoc
def initialize(name, options={})
self.name = name
self.sortable = options[:sortable]
self.default_order = options[:default_order]
end
def caption
set_language_if_valid(User.current.language)
l("field_#{name}")
end
end
class QueryCustomFieldColumn < QueryColumn
def initialize(custom_field)
self.name = "cf_#{custom_field.id}".to_sym
self.sortable = false
@cf = custom_field
end
def caption
@cf.name
end
def custom_field
@cf
end
end
class Query < ActiveRecord::Base
belongs_to :project
belongs_to :user
serialize :filters
serialize :column_names
attr_protected :project_id, :user_id
attr_protected :project, :user
attr_accessor :executed_by
validates_presence_of :name, :on => :save
validates_length_of :name, :maximum => 255
@@operators = { "=" => :label_equals,
"!" => :label_not_equals,
@@ -65,13 +31,10 @@ class Query < ActiveRecord::Base
"c" => :label_closed_issues,
"!*" => :label_none,
"*" => :label_all,
">=" => '>=',
"<=" => '<=',
"<t+" => :label_in_less_than,
">t+" => :label_in_more_than,
"t+" => :label_in,
"t" => :label_today,
"w" => :label_this_week,
">t-" => :label_less_than_ago,
"<t-" => :label_more_than_ago,
"t-" => :label_ago,
@@ -83,97 +46,67 @@ class Query < ActiveRecord::Base
@@operators_by_filter_type = { :list => [ "=", "!" ],
:list_status => [ "o", "=", "!", "c", "*" ],
:list_optional => [ "=", "!", "!*", "*" ],
:list_subprojects => [ "*", "!*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
:list_one_or_more => [ "*", "=" ],
:date => [ "<t+", ">t+", "t+", "t", ">t-", "<t-", "t-" ],
:date_past => [ ">t-", "<t-", "t-", "t" ],
:string => [ "=", "~", "!", "!~" ],
:text => [ "~", "!~" ],
:integer => [ "=", ">=", "<=" ] }
:text => [ "~", "!~" ] }
cattr_reader :operators_by_filter_type
@@available_columns = [
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
QueryColumn.new(:author),
QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
]
cattr_reader :available_columns
def initialize(attributes = nil)
super attributes
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
set_language_if_valid(User.current.language)
end
def after_initialize
# Store the fact that project is nil (used in #editable_by?)
@is_for_all = project.nil?
def executed_by=(user)
@executed_by = user
set_language_if_valid(user.language) if user
end
def validate
filters.each_key do |field|
errors.add label_for(field), :activerecord_error_blank unless
# filter requires one or more values
(values_for(field) and !values_for(field).first.blank?) or
(values_for(field) and !values_for(field).first.empty?) or
# filter doesn't require any value
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
["o", "c", "!*", "*", "t"].include? operator_for(field)
end if filters
end
def editable_by?(user)
return false unless user
# Admin can edit them all and regular users can edit their private queries
return true if user.admin? || (!is_public && self.user_id == user.id)
# Members can not edit public queries that are for all project (only admin is allowed to)
is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
return true if !is_public && self.user_id == user.id
is_public && user.authorized_to(project, "projects/add_query")
end
def available_filters
return @available_filters if @available_filters
trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
"priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"tracker_id" => { :type => :list, :order => 2, :values => Tracker.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
"priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI']).collect{|s| [s.name, s.id.to_s] } },
"subject" => { :type => :text, :order => 8 },
"created_on" => { :type => :date_past, :order => 9 },
"updated_on" => { :type => :date_past, :order => 10 },
"start_date" => { :type => :date, :order => 11 },
"due_date" => { :type => :date, :order => 12 },
"done_ratio" => { :type => :integer, :order => 13 }}
user_values = []
user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
if project
user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
else
# members of the user's projects
user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
end
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
if project
# project specific filters
"due_date" => { :type => :date, :order => 12 } }
unless project.nil?
# project specific filters
user_values = []
user_values << ["<< #{l(:label_me)} >>", "me"] if executed_by
user_values += @project.users.collect{|s| [s.name, s.id.to_s] }
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values }
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values }
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
unless @project.active_children.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
@available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
end
@project.all_custom_fields.select(&:is_filter?).each do |field|
case field.field_format
when "string", "int"
options = { :type => :string, :order => 20 }
when "text"
options = { :type => :text, :order => 20 }
when "list"
@@ -182,8 +115,6 @@ class Query < ActiveRecord::Base
options = { :type => :date, :order => 20 }
when "bool"
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
else
options = { :type => :string, :order => 20 }
end
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
end
@@ -227,109 +158,56 @@ class Query < ActiveRecord::Base
end
def label_for(field)
label = available_filters[field][:name] if available_filters.has_key?(field)
label = @available_filters[field][:name] if @available_filters.has_key?(field)
label ||= field.gsub(/\_id$/, "")
end
def available_columns
return @available_columns if @available_columns
@available_columns = Query.available_columns
@available_columns += (project ?
project.all_custom_fields :
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
).collect {|cf| QueryCustomFieldColumn.new(cf) }
end
def columns
if has_default_columns?
available_columns.select {|c| Setting.issue_list_default_columns.include?(c.name.to_s) }
else
# preserve the column_names order
column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
end
end
def column_names=(names)
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
write_attribute(:column_names, names)
end
def has_column?(column)
column_names && column_names.include?(column.name)
end
def has_default_columns?
column_names.nil? || column_names.empty?
end
def statement
# project/subprojects clause
project_clauses = []
if project && !@project.active_children.empty?
ids = [project.id]
if has_filter?("subproject_id")
case operator_for("subproject_id")
when '='
# include the selected subprojects
ids += values_for("subproject_id").each(&:to_i)
when '!*'
# main project only
else
# all subprojects
ids += project.child_ids
end
elsif Setting.display_subprojects_issues?
ids += project.child_ids
sql = "1=1"
if has_filter?("subproject_id")
subproject_ids = []
if operator_for("subproject_id") == "="
subproject_ids = values_for("subproject_id").each(&:to_i)
else
subproject_ids = project.active_children.collect{|p| p.id}
end
project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
elsif project
project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
sql << " AND #{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project
else
sql << " AND #{Issue.table_name}.project_id=%d" % project.id if project
end
project_clauses << Project.visible_by(User.current)
# filters clauses
filters_clauses = []
filters.each_key do |field|
next if field == "subproject_id"
v = values_for(field).clone
next unless v and !v.empty?
sql = ''
is_custom_filter = false
sql = sql + " AND " unless sql.empty?
sql << "("
if field =~ /^cf_(\d+)$/
# custom field
db_table = CustomValue.table_name
db_field = 'value'
is_custom_filter = true
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
db_field = "value"
sql << "#{db_table}.custom_field_id = #{$1} AND "
else
# regular field
db_table = Issue.table_name
db_field = field
sql << '('
end
# "me" value subsitution
if %w(assigned_to_id author_id).include?(field)
v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
v.push(executed_by ? executed_by.id.to_s : "0") if v.delete("me")
end
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(",") + "))"
sql = sql + "#{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"
@@ -348,22 +226,14 @@ class Query < ActiveRecord::Base
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date((Date.today + v.first.to_i).to_time), connection.quoted_date((Date.today + v.first.to_i + 1).to_time)]
when "t"
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Date.today.to_time), connection.quoted_date((Date.today+1).to_time)]
when "w"
from = l(:general_first_day_of_week) == '7' ?
# week starts on sunday
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
# week starts on monday (Rails default)
Time.now.at_beginning_of_week
sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
when "~"
sql = sql + "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(v.first)}%'"
when "!~"
sql = sql + "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(v.first)}%'"
end
sql << ')'
filters_clauses << sql
sql << ")"
end if filters and valid?
(project_clauses + filters_clauses).join(' AND ')
sql
end
end

View File

@@ -17,19 +17,9 @@
class Repository < ActiveRecord::Base
belongs_to :project
has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
has_many :changes, :through => :changesets
# Removes leading and trailing whitespace
def url=(arg)
write_attribute(:url, arg ? arg.to_s.strip : nil)
end
# Removes leading and trailing whitespace
def root_url=(arg)
write_attribute(:root_url, arg ? arg.to_s.strip : nil)
end
def scm
@scm ||= self.scm_adapter.new url, root_url, login, password
update_attribute(:root_url, @scm.root_url) if root_url.blank?
@@ -43,10 +33,6 @@ class Repository < ActiveRecord::Base
def supports_cat?
scm.supports_cat?
end
def supports_annotate?
scm.supports_annotate?
end
def entries(path=nil, identifier=nil)
scm.entries(path, identifier)
@@ -56,19 +42,6 @@ class Repository < ActiveRecord::Base
scm.diff(path, rev, rev_to, type)
end
# Default behaviour: we search in cached changesets
def changesets_for_path(path)
path = "/#{path}" unless path.starts_with?('/')
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
end
# Returns a path relative to the url of the repository
def relative_path(path)
path
end
def latest_changeset
@latest_changeset ||= changesets.find(:first)
end
@@ -103,13 +76,4 @@ class Repository < ActiveRecord::Base
rescue
nil
end
private
def before_save
# Strips url and root_url
url.strip!
root_url.strip!
true
end
end

View File

@@ -1,86 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# 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 'redmine/scm/adapters/bazaar_adapter'
class Repository::Bazaar < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
Redmine::Scm::Adapters::BazaarAdapter
end
def self.scm_name
'Bazaar'
end
def entries(path=nil, identifier=nil)
entries = scm.entries(path, identifier)
if entries
entries.each do |e|
next if e.lastrev.revision.blank?
c = Change.find(:first,
:include => :changeset,
:conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
:order => "#{Changeset.table_name}.revision DESC")
if c
e.lastrev.identifier = c.changeset.revision
e.lastrev.name = c.changeset.revision
e.lastrev.author = c.changeset.committer
end
end
end
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
# latest revision in the repository
scm_revision = scm_info.lastrev.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
while (identifier_from <= scm_revision)
# loads changesets by batches of 200
identifier_to = [identifier_from + 199, scm_revision].min
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:committer => revision.author,
:committed_on => revision.time,
:scmid => revision.scmid,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:revision => change[:revision])
end
end
end unless revisions.nil?
identifier_from = identifier_to + 1
end
end
end
end
end

View File

@@ -35,8 +35,7 @@ class Repository::Cvs < Repository
end
def entries(path=nil, identifier=nil)
rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
entries=scm.entries(path, identifier)
if entries
entries.each() do |entry|
unless entry.lastrev.nil? || entry.lastrev.identifier
@@ -76,14 +75,16 @@ class Repository::Cvs < Repository
unless revision_to
revision_to=scm.get_previous_revision(revision_from)
end
file_diff = scm.diff(change_from.path, revision_from, revision_to)
diff = diff + file_diff unless file_diff.nil?
diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
end
end
return diff
end
def fetch_changesets
#not the preferred way with CVS. maybe we should introduce always a cron-job for this
last_commit = changesets.maximum(:committed_on)
# some nifty bits to introduce a commit-id with cvs
# natively cvs doesn't provide any kind of changesets, there is only a revision per file.
# we now take a guess using the author, the commitlog and the commit-date.
@@ -93,31 +94,31 @@ class Repository::Cvs < Repository
# we use a small delta here, to merge all changes belonging to _one_ changeset
time_delta=10.seconds
fetch_since = latest_changeset ? latest_changeset.committed_on : nil
transaction do
tmp_rev_num = 1
scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
# only add the change to the database, if it doen't exists. the cvs log
# is not exclusive at all.
unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
revision
cs = changesets.find(:first, :conditions=>{
cs=Changeset.find(:first, :conditions=>{
:committed_on=>revision.time-time_delta..revision.time+time_delta,
:committer=>revision.author,
:comments=>revision.message
})
# create a new changeset....
unless cs
# we use a temporaray revision number here (just for inserting)
unless cs
# we use a negative changeset-number here (just for inserting)
# later on, we calculate a continous positive number
latest = changesets.find(:first, :order => 'id DESC')
cs = Changeset.create(:repository => self,
:revision => "_#{tmp_rev_num}",
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
tmp_rev_num += 1
next_rev = changesets.minimum(:revision)
next_rev = 0 if next_rev.nil? or next_rev > 0
next_rev = next_rev - 1
cs=Changeset.create(:repository => self,
:revision => next_rev,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
end
#convert CVS-File-States to internal Action-abbrevations
@@ -138,19 +139,12 @@ class Repository::Cvs < Repository
end
end
# Renumber new changesets in chronological order
changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
changeset.update_attribute :revision, next_revision_number
next_rev = [changesets.maximum(:revision) || 0, 0].max
changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
next_rev = next_rev + 1
changeset.revision = next_rev
changeset.save!
end
end # transaction
end
private
# Returns the next revision number to assign to a CVS changeset
def next_revision_number
# Need to retrieve existing revision numbers to sort them as integers
@current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0)
@current_revision_number += 1
end
end
end

View File

@@ -29,8 +29,7 @@ class Repository::Darcs < Repository
end
def entries(path=nil, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
entries=scm.entries(path, identifier)
if entries
entries.each do |entry|
# Search the DB for the entry's last change
@@ -48,19 +47,18 @@ class Repository::Darcs < Repository
def diff(path, rev, rev_to, type)
patch_from = changesets.find_by_revision(rev)
return nil if patch_from.nil?
patch_to = changesets.find_by_revision(rev_to) if rev_to
if path.blank?
path = patch_from.changes.collect{|change| change.path}.join(' ')
end
patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
scm.diff(path, patch_from.scmid, patch_to.scmid, type)
end
def fetch_changesets
scm_info = scm.info
if scm_info
db_last_id = latest_changeset ? latest_changeset.scmid : nil
next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
next_rev = latest_changeset ? latest_changeset.revision + 1 : 1
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
unless changesets.find_by_scmid(scm_revision)
@@ -73,7 +71,9 @@ class Repository::Darcs < Repository
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
next if changeset.new_record?
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],

View File

@@ -1,70 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
# Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
# 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 'redmine/scm/adapters/git_adapter'
class Repository::Git < Repository
attr_protected :root_url
validates_presence_of :url
def scm_adapter
Redmine::Scm::Adapters::GitAdapter
end
def self.scm_name
'Git'
end
def changesets_for_path(path)
Change.find(:all, :include => :changeset,
:conditions => ["repository_id = ? AND path = ?", id, path],
:order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision : nil
# latest revision in the repository
scm_revision = scm_info.lastrev.scmid
unless changesets.find_by_scmid(scm_revision)
revisions = scm.revisions('', db_revision, nil)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end
end
end
end
end
end

View File

@@ -34,11 +34,6 @@ class Repository::Mercurial < Repository
if entries
entries.each do |entry|
next unless entry.is_file?
# Set the filesize unless browsing a specific revision
if identifier.nil?
full_path = File.join(root_url, entry.path)
entry.size = File.stat(full_path).size if File.file?(full_path)
end
# Search the DB for the entry's last change
change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
if change
@@ -56,37 +51,29 @@ class Repository::Mercurial < Repository
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
db_revision = latest_changeset ? latest_changeset.revision : nil
# latest revision in the repository
latest_revision = scm_info.lastrev
return if latest_revision.nil?
scm_revision = latest_revision.identifier.to_i
if db_revision < scm_revision
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
identifier_from = db_revision + 1
while (identifier_from <= scm_revision)
# loads changesets by batches of 100
identifier_to = [identifier_from + 99, scm_revision].min
revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
transaction do
revisions.each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
scm_revision = scm_info.lastrev.identifier.to_i
unless changesets.find_by_revision(scm_revision)
revisions = scm.revisions('', db_revision, nil)
transaction do
revisions.reverse_each do |revision|
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
:committer => revision.author,
:committed_on => revision.time,
:comments => revision.message)
revision.paths.each do |change|
Change.create(:changeset => changeset,
:action => change[:action],
:path => change[:path],
:from_path => change[:from_path],
:from_revision => change[:from_revision])
end
end unless revisions.nil?
identifier_from = identifier_to + 1
end
end
end
end

View File

@@ -20,7 +20,7 @@ require 'redmine/scm/adapters/subversion_adapter'
class Repository::Subversion < Repository
attr_protected :root_url
validates_presence_of :url
validates_format_of :url, :with => /^(http|https|svn|svn\+ssh|file):\/\/.+/i
validates_format_of :url, :with => /^(http|https|svn|file):\/\/.+/i
def scm_adapter
Redmine::Scm::Adapters::SubversionAdapter
@@ -30,21 +30,11 @@ class Repository::Subversion < Repository
'Subversion'
end
def changesets_for_path(path)
revisions = scm.revisions(path)
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
end
# Returns a path relative to the url of the repository
def relative_path(path)
path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
end
def fetch_changesets
scm_info = scm.info
if scm_info
# latest revision found in database
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
db_revision = latest_changeset ? latest_changeset.revision : 0
# latest revision in the repository
scm_revision = scm_info.lastrev.identifier.to_i
if db_revision < scm_revision
@@ -76,14 +66,4 @@ class Repository::Subversion < Repository
end
end
end
private
# Returns the relative url of the repository
# Eg: root_url = file:///var/svn/foo
# url = file:///var/svn/foo/bar
# => returns /bar
def relative_url
@relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url)}"), '')
end
end

View File

@@ -16,104 +16,22 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Role < ActiveRecord::Base
# Built-in roles
BUILTIN_NON_MEMBER = 1
BUILTIN_ANONYMOUS = 2
before_destroy :check_deletable
has_many :workflows, :dependent => :delete_all do
def copy(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?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT tracker_id, old_status_id, new_status_id, #{proxy_owner.id}" +
" FROM workflows" +
" WHERE role_id = #{role.id}"
end
end
before_destroy :check_integrity
has_and_belongs_to_many :permissions
has_many :workflows, :dependent => :delete_all
has_many :members
acts_as_list
serialize :permissions
attr_protected :builtin
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
def permissions
read_attribute(:permissions) || []
end
def permissions=(perms)
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact if perms
write_attribute(:permissions, perms)
end
def <=>(role)
position <=> role.position
end
# Return true if the role is a builtin role
def builtin?
self.builtin != 0
end
# Return true if the role is a project member role
def member?
!self.builtin?
end
# Return true if role is allowed to do the specified action
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
def allowed_to?(action)
if action.is_a? Hash
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
else
allowed_permissions.include? action
end
end
# Return all the permissions that can be given to the role
def setable_permissions
setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
setable_permissions
end
# Find all the roles that can be given to a project member
def self.find_all_givable
find(:all, :conditions => {:builtin => 0}, :order => 'position')
end
# Return the builtin 'non member' role
def self.non_member
find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER}) || raise('Missing non-member builtin role.')
end
# Return the builtin 'anonymous' role
def self.anonymous
find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS}) || raise('Missing anonymous builtin role.')
end
private
def allowed_permissions
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
end
def allowed_actions
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
end
def check_deletable
raise "Can't delete role" if members.any?
raise "Can't delete builtin role" if builtin?
def check_integrity
raise "Can't delete role" if Member.find(:first, :conditions =>["role_id=?", self.id])
end
end

View File

@@ -17,68 +17,31 @@
class Setting < ActiveRecord::Base
DATE_FORMATS = [
'%Y-%m-%d',
'%d/%m/%Y',
'%d.%m.%Y',
'%d-%m-%Y',
'%m/%d/%Y',
'%d %b %Y',
'%d %B %Y',
'%b %d, %Y',
'%B %d, %Y'
]
TIME_FORMATS = [
'%H:%M',
'%I:%M %p'
]
cattr_accessor :available_settings
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
Redmine::Plugin.registered_plugins.each do |id, plugin|
next unless plugin.settings
@@available_settings["plugin_#{id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
end
validates_uniqueness_of :name
validates_inclusion_of :name, :in => @@available_settings.keys
validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
# Hash used to cache setting values
@cached_settings = {}
@cached_cleared_on = Time.now
def value
v = read_attribute(:value)
# Unserialize serialized settings
v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
v
def self.get(name)
name = name.to_s
setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
setting
end
def value=(v)
v = v.to_yaml if v && @@available_settings[name]['serialized']
write_attribute(:value, v.to_s)
end
# Returns the value of the setting named name
def self.[](name)
v = @cached_settings[name]
v ? v : (@cached_settings[name] = find_or_default(name).value)
get(name).value
end
def self.[]=(name, v)
setting = find_or_default(name)
setting.value = (v ? v : "")
@cached_settings[name] = nil
def self.[]=(name, value)
setting = get(name)
setting.value = (value ? value.to_s : "")
setting.save
setting.value
end
# Defines getter and setter for each setting
# Then setting values can be read using: Setting.some_setting_name
# or set using Setting.some_setting_name = "some value"
@@available_settings.each do |name, params|
src = <<-END_SRC
def self.#{name}
@@ -95,31 +58,4 @@ class Setting < ActiveRecord::Base
END_SRC
class_eval src, __FILE__, __LINE__
end
# Helper that returns an array based on per_page_options setting
def self.per_page_options_array
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
end
# Checks if settings have changed since the values were read
# and clears the cache hash if it's the case
# Called once per request
def self.check_cache
settings_updated_on = Setting.maximum(:updated_on)
if settings_updated_on && @cached_cleared_on <= settings_updated_on
@cached_settings.clear
@cached_cleared_on = Time.now
logger.info "Settings cache cleared." if logger
end
end
private
# Returns the Setting instance for the setting named name
# (record found in database or new record with default value)
def self.find_or_default(name)
name = name.to_s
raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
setting = find_by_name(name)
setting ||= new(:name => name, :value => @@available_settings[name]['default']) if @@available_settings.has_key? name
end
end

View File

@@ -1,5 +1,5 @@
# 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
# modify it under the terms of the GNU General Public License
@@ -29,14 +29,6 @@ class TimeEntry < ActiveRecord::Base
validates_numericality_of :hours, :allow_nil => true
validates_length_of :comments, :maximum => 255
def after_initialize
if new_record? && self.activity.nil?
if default_activity = Enumeration.default('ACTI')
self.activity_id = default_activity.id
end
end
end
def before_validation
self.project = issue.project if issue && project.nil?
end
@@ -47,10 +39,6 @@ class TimeEntry < ActiveRecord::Base
errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project)
end
def hours=(h)
write_attribute :hours, (h.is_a?(String) ? h.to_hours : h)
end
# tyear, tmonth, tweek assigned where setting spent_on attributes
# these attributes make time aggregations easier
def spent_on=(date)
@@ -58,16 +46,5 @@ class TimeEntry < ActiveRecord::Base
self.tyear = spent_on ? spent_on.year : nil
self.tmonth = spent_on ? spent_on.month : nil
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
end
# Returns true if the time entry can be edited by usr, otherwise false
def editable_by?(usr)
(usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
end
def self.visible_by(usr)
with_scope(:find => { :conditions => Project.allowed_to_condition(usr, :view_time_entries) }) do
yield
end
end
end
end

View File

@@ -18,37 +18,14 @@
class Tracker < ActiveRecord::Base
before_destroy :check_integrity
has_many :issues
has_many :workflows, :dependent => :delete_all do
def copy(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?
clear
connection.insert "INSERT INTO workflows (tracker_id, old_status_id, new_status_id, role_id)" +
" SELECT #{proxy_owner.id}, old_status_id, new_status_id, role_id" +
" FROM workflows" +
" WHERE tracker_id = #{tracker.id}"
end
end
has_and_belongs_to_many :projects
has_many :workflows, :dependent => :delete_all
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
acts_as_list
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
def to_s; name end
def <=>(tracker)
name <=> tracker.name
end
def self.all
find(:all, :order => 'position')
end
private
def check_integrity
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])

View File

@@ -18,29 +18,17 @@
require "digest/sha1"
class User < ActiveRecord::Base
class OnTheFlyCreationFailure < Exception; end
# Account statuses
STATUS_ANONYMOUS = 0
STATUS_ACTIVE = 1
STATUS_REGISTERED = 2
STATUS_LOCKED = 3
USER_FORMATS = {
:firstname_lastname => '#{firstname} #{lastname}',
:firstname => '#{firstname}',
:lastname_firstname => '#{lastname} #{firstname}',
:lastname_coma_firstname => '#{lastname}, #{firstname}',
:username => '#{login}'
}
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
has_many :projects, :through => :memberships
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
has_one :rss_key, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source
attr_accessor :password, :password_confirmation
@@ -48,25 +36,20 @@ class User < ActiveRecord::Base
# Prevents unauthorized assignments
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }
validates_presence_of :login, :firstname, :lastname, :mail
validates_uniqueness_of :login, :mail
# Login must contain lettres, numbers, underscores only
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
validates_format_of :login, :with => /^[a-z0-9_\-@\.]+$/i
validates_length_of :login, :maximum => 30
validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i
validates_length_of :firstname, :lastname, :maximum => 30
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
validates_length_of :mail, :maximum => 60, :allow_nil => true
validates_length_of :password, :minimum => 4, :allow_nil => true
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
validates_length_of :mail, :maximum => 60
# Password length between 4 and 12
validates_length_of :password, :in => 4..12, :allow_nil => true
validates_confirmation_of :password, :allow_nil => true
validates_associated :custom_values, :on => :update
def before_create
self.mail_notification = false
true
end
def before_save
# update hashed_password if password was set
self.hashed_password = User.hash_password(self.password) if self.password
@@ -86,8 +69,6 @@ class User < ActiveRecord::Base
# Returns the user that matches provided login and password, or nil
def self.try_to_login(login, password)
# Make sure no one can sign in with an empty password
return nil if password.to_s.empty?
user = find(:first, :conditions => ["login=?", login])
if user
# user is already in local database
@@ -108,23 +89,20 @@ class User < ActiveRecord::Base
onthefly.language = Setting.default_language
if onthefly.save
user = find(:first, :conditions => ["login=?", login])
logger.info("User '#{user.login}' created from the LDAP") if logger
else
logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger
raise OnTheFlyCreationFailure.new
logger.info("User '#{user.login}' created on the fly.") if logger
end
end
end
user.update_attribute(:last_login_on, Time.now) if user
user
rescue => text
raise text
rescue => text
raise text
end
# Return user's full name for display
def name(formatter = nil)
f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
eval '"' + f + '"'
def name
"#{firstname} #{lastname}"
end
def active?
@@ -143,34 +121,24 @@ class User < ActiveRecord::Base
User.hash_password(clear_password) == self.hashed_password
end
def role_for_project(project)
return nil unless project
member = memberships.detect {|m| m.project_id == project.id}
member ? member.role : nil
end
def authorized_to(project, action)
return true if self.admin?
role = role_for_project(project)
role && Permission.allowed_to_role(action, role)
end
def pref
self.preference ||= UserPreference.new(:user => self)
end
def time_zone
self.pref.time_zone.nil? ? nil : TimeZone[self.pref.time_zone]
end
def wants_comments_in_reverse_order?
self.pref[:comments_sorting] == 'desc'
end
# Return user's RSS key (a 40 chars long string), used to access feeds
def rss_key
token = self.rss_token || Token.create(:user => self, :action => 'feeds')
token.value
end
# Return an array of project ids for which the user has explicitly turned mail notifications on
def notified_projects_ids
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
end
def notified_project_ids=(ids)
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
@notified_projects_ids = nil
notified_projects_ids
def get_or_create_rss_key
self.rss_key || Token.create(:user => self, :action => 'feeds')
end
def self.find_by_rss_key(key)
@@ -184,86 +152,7 @@ class User < ActiveRecord::Base
end
def <=>(user)
if user.nil?
-1
elsif lastname.to_s.downcase == user.lastname.to_s.downcase
firstname.to_s.downcase <=> user.firstname.to_s.downcase
else
lastname.to_s.downcase <=> user.lastname.to_s.downcase
end
end
def to_s
name
end
def logged?
true
end
# Return user's role for project
def role_for_project(project)
# No role on archived projects
return nil unless project && project.active?
if logged?
# Find project membership
membership = memberships.detect {|m| m.project_id == project.id}
if membership
membership.role
else
@role_non_member ||= Role.non_member
end
else
@role_anonymous ||= Role.anonymous
end
end
# Return true if the user is a member of project
def member_of?(project)
role_for_project(project).member?
end
# Return true if the user is allowed to do the specified action on project
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
def allowed_to?(action, project, options={})
if project
# No action allowed on archived projects
return false unless project.active?
# No action allowed on disabled modules
return false unless project.allows_to?(action)
# Admin users are authorized for anything else
return true if admin?
role = role_for_project(project)
return false unless role
role.allowed_to?(action) && (project.is_public? || role.member?)
elsif options[:global]
# authorize if user has at least one role that has this permission
roles = memberships.collect {|m| m.role}.uniq
roles.detect {|r| r.allowed_to?(action)}
else
false
end
end
def self.current=(user)
@current_user = user
end
def self.current
@current_user ||= User.anonymous
end
def self.anonymous
anonymous_user = AnonymousUser.find(:first)
if anonymous_user.nil?
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
end
anonymous_user
lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname
end
private
@@ -272,19 +161,3 @@ private
Digest::SHA1.hexdigest(clear_password || "")
end
end
class AnonymousUser < User
def validate_on_create
# There should be only one AnonymousUser in the database
errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
end
# Overrides a few properties
def logged?; false end
def admin; false end
def name; 'Anonymous' end
def mail; nil end
def time_zone; nil end
def rss_key; nil end
end

View File

@@ -46,7 +46,4 @@ class UserPreference < ActiveRecord::Base
self.others.store attr_name, value
end
end
def comments_sorting; self[:comments_sorting] end
def comments_sorting=(order); self[:comments_sorting]=order end
end

View File

@@ -23,7 +23,6 @@ class Version < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :scope => [:project_id]
validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :activerecord_error_not_a_date, :allow_nil => true
def start_date
@@ -34,50 +33,8 @@ class Version < ActiveRecord::Base
effective_date
end
# Returns the total estimated time for this version
def estimated_hours
@estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
end
# Returns the total reported time for this version
def spent_hours
@spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
end
# Returns true if the version is completed: due date reached and no open issues
def completed?
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
end
def completed_pourcent
if fixed_issues.count == 0
0
elsif open_issues_count == 0
100
else
(closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
end
end
def closed_pourcent
if fixed_issues.count == 0
0
else
closed_issues_count * 100.0 / fixed_issues.count
end
end
# Returns true if the version is overdue: due date reached and some open issues
def overdue?
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
end
def open_issues_count
@open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
end
def closed_issues_count
@closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
effective_date && effective_date <= Date.today
end
def wiki_page
@@ -87,13 +44,11 @@ class Version < ActiveRecord::Base
@wiki_page
end
def to_s; name end
# Versions are sorted by effective_date and name
# Versions are sorted by effective_date
# Those with no effective_date are at the end, sorted by name
def <=>(version)
if self.effective_date
version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
version.effective_date ? (self.effective_date <=> version.effective_date) : -1
else
version.effective_date ? 1 : (self.name <=> version.name)
end

View File

@@ -18,37 +18,29 @@
class Wiki < ActiveRecord::Base
belongs_to :project
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
validates_presence_of :start_page
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|]*$/
# find the page with the given title
# if page doesn't exist, return a new page
def find_or_new_page(title)
title = start_page if title.blank?
find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
title = Wiki.titleize(title || start_page)
find_page(title) || WikiPage.new(:wiki => self, :title => title)
end
# find the page with the given title
def find_page(title, options = {})
def find_page(title)
title = start_page if title.blank?
title = Wiki.titleize(title)
page = pages.find_by_title(title)
if !page && !(options[:with_redirect] == false)
# search for a redirect
redirect = redirects.find_by_title(title)
page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
end
page
pages.find_by_title(Wiki.titleize(title))
end
# turn a string into a valid page title
def self.titleize(title)
# replace spaces with _ and remove unwanted caracters
title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
title = title.gsub(/\s+/, '_').delete(',./?;|') if title
# upcase the first letter
title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
title = title[0..0].upcase + title[1..-1] if title
title
end
end

View File

@@ -25,16 +25,10 @@ class WikiContent < ActiveRecord::Base
acts_as_versioned
class Version
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
attr_protected :data
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
:description => :comments,
:datetime => :updated_on,
:type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
def text=(plain)
case Setting.wiki_compression
when 'gzip'
@@ -61,18 +55,6 @@ class WikiContent < ActiveRecord::Base
data
end
end
def project
page.project
end
# Returns the previous version or nil
def previous
@previous ||= WikiContent::Version.find(:first,
:order => 'version DESC',
:include => :author,
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
end
end
end

View File

@@ -16,55 +16,19 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'diff'
require 'enumerator'
class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
:description => :text,
:datetime => :created_on,
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
acts_as_searchable :columns => ['title', 'text'],
:include => [:wiki, :content],
:project_key => "#{Wiki.table_name}.project_id"
attr_accessor :redirect_existing_links
validates_presence_of :title
validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
validates_associated :content
def title=(value)
value = Wiki.titleize(value)
@previous_title = read_attribute(:title) if @previous_title.blank?
write_attribute(:title, value)
end
def before_save
self.title = Wiki.titleize(title)
# Manage redirects if the title has changed
if !@previous_title.blank? && (@previous_title != title) && !new_record?
# Update redirects that point to the old title
wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
r.redirects_to = title
r.title == r.redirects_to ? r.destroy : r.save
end
# Remove redirects for the new title
wiki.redirects.find_all_by_title(title).each(&:destroy)
# Create a redirect to the new title
wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
@previous_title = nil
end
end
def before_destroy
# Remove redirects to this page
wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
def before_save
self.title = Wiki.titleize(title)
end
def pretty_title
@@ -88,12 +52,6 @@ class WikiPage < ActiveRecord::Base
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
end
def annotate(version=nil)
version = version ? version.to_i : self.content.version
c = content.versions.find_by_version(version)
c ? WikiAnnotate.new(c) : nil
end
def self.pretty_title(str)
(str && str.is_a?(String)) ? str.tr('_', ' ') : str
end
@@ -101,10 +59,6 @@ class WikiPage < ActiveRecord::Base
def project
wiki.project
end
def text
content.text if content
end
end
class WikiDiff
@@ -120,41 +74,3 @@ class WikiDiff
@diff = words_from.diff @words
end
end
class WikiAnnotate
attr_reader :lines, :content
def initialize(content)
@content = content
current = content
current_lines = current.text.split(/\r?\n/)
@lines = current_lines.collect {|t| [nil, nil, t]}
positions = []
current_lines.size.times {|i| positions << i}
while (current.previous)
d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
d.each_slice(3) do |s|
sign, line = s[0], s[1]
if sign == '+' && positions[line] && positions[line] != -1
if @lines[positions[line]][0].nil?
@lines[positions[line]][0] = current.version
@lines[positions[line]][1] = current.author
end
end
end
d.each_slice(3) do |s|
sign, line = s[0], s[1]
if sign == '-'
positions.insert(line, -1)
else
positions[line] = nil
end
end
positions.compact!
# Stop if every line is annotated
break unless @lines.detect { |line| line[0].nil? }
current = current.previous
end
@lines.each { |line| line[0] ||= current.version }
end
end

View File

@@ -15,25 +15,24 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.dirname(__FILE__) + '/../test_helper'
class IssueSweeper < ActionController::Caching::Sweeper
observe Issue
class JournalTest < Test::Unit::TestCase
fixtures :issues, :issue_statuses, :journals, :journal_details
def setup
@journal = Journal.find 1
def after_save(issue)
expire_cache_for(issue)
end
def test_journalized_is_an_issue
issue = @journal.issue
assert_kind_of Issue, issue
assert_equal 1, issue.id
def after_destroy(issue)
expire_cache_for(issue)
end
def test_new_status
status = @journal.new_status
assert_not_nil status
assert_kind_of IssueStatus, status
assert_equal 2, status.id
private
def expire_cache_for(issue)
# fragments of the main project
expire_fragment(Regexp.new("projects/(calendar|gantt)/#{issue.project_id}\\."))
# fragments of the root project that include subprojects issues
unless issue.project.parent_id.nil?
expire_fragment(Regexp.new("projects/(calendar|gantt)/#{issue.project.parent_id}\\..*subprojects"))
end
end
end

View File

@@ -1,45 +1,40 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# 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 WikisController < ApplicationController
layout 'base'
menu_item :settings
before_filter :find_project, :authorize
# Create or update a project's wiki
def edit
@wiki = @project.wiki || Wiki.new(:project => @project)
@wiki.attributes = params[:wiki]
@wiki.save if request.post?
render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
end
# Delete a project's wiki
def destroy
if request.post? && params[:confirm] && @project.wiki
@project.wiki.destroy
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
end
end
private
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# 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 ProjectSweeper < ActionController::Caching::Sweeper
observe Project
def before_save(project)
if project.new_record?
expire_cache_for(project.parent) if project.parent
else
project_before_update = Project.find(project.id)
return if project_before_update.parent_id == project.parent_id && project_before_update.status == project.status
expire_cache_for(project.parent) if project.parent
expire_cache_for(project_before_update.parent) if project_before_update.parent
end
end
def after_destroy(project)
expire_cache_for(project.parent) if project.parent
end
private
def expire_cache_for(project)
expire_fragment(Regexp.new("projects/(calendar|gantt)/#{project.id}\\."))
end
end

View File

@@ -1,33 +1,28 @@
<div id="login-form">
<% form_tag({:action=> "login"}) do %>
<table>
<tr>
<td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
<td align="left"><p><%= text_field_tag 'username', nil, :size => 40 %></p></td>
</tr>
<tr>
<td align="right"><label for="password"><%=l(:field_password)%>:</label></td>
<td align="left"><%= password_field_tag 'password', nil, :size => 40 %></td>
</tr>
<tr>
<td></td>
<td align="left">
<% if Setting.autologin? %>
<label for="autologin"><%= check_box_tag 'autologin' %> <%= l(:label_stay_logged_in) %></label>
<% end %>
</td>
</tr>
<tr>
<td align="left">
<% if Setting.lost_password? %>
<%= link_to l(:label_password_lost), :controller => 'account', :action => 'lost_password' %>
<% end %>
</td>
<td align="right">
<input type="submit" name="login" value="<%=l(:button_login)%> &#187;" />
</td>
</tr>
</table>
<%= javascript_tag "Form.Element.focus('username');" %>
<center>
<div class="box login">
<h2 class="icon22 icon22-authent"><%=l(:label_please_login)%></h2>
<% form_tag({:action=> "login"}, :class => "tabular") do %>
<p><label for="login"><%=l(:field_login)%>:</label>
<%= text_field_tag 'login', nil, :size => 25 %></p>
<p><label for="password"><%=l(:field_password)%>:</label>
<%= password_field_tag 'password', nil, :size => 25 %></p>
<% if Setting.autologin? %>
<p><label for="autologin"><%= check_box_tag 'autologin' %> <%= l(:label_stay_logged_in) %></label></p>
<% end %>
<p><input type="submit" name="login" value="<%=l(:button_login)%> &#187;" class="primary" /></p>
<% end %>
<%= javascript_tag "Form.Element.focus('login');" %>
<% links = []
links << link_to(l(:label_register), :action => 'register') if Setting.self_registration?
links << link_to(l(:label_password_lost), :action => 'lost_password') if Setting.lost_password?
%>
<%= links.join(" | ") %>
</div>
</center>

View File

@@ -1,11 +1,14 @@
<center>
<div class="box login">
<h2><%=l(:label_password_lost)%></h2>
<div class="box">
<% form_tag({:action=> "lost_password"}, :class => "tabular") do %>
<p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
<%= text_field_tag 'mail', nil, :size => 40 %>
<%= submit_tag l(:button_submit) %></p>
<%= text_field_tag 'mail', nil, :size => 40 %></p>
<p><center><%= submit_tag l(:button_submit) %></center></p>
<% end %>
</div>
</center>

View File

@@ -1,15 +1,22 @@
<center>
<div class="box login">
<h2><%=l(:label_password_lost)%></h2>
<p><%=l(:field_login)%>: <strong><%= @user.login %></strong><br />
<%= error_messages_for 'user' %>
<% form_tag({:token => @token.value}) do %>
<div class="box tabular">
<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
<%= password_field_tag 'new_password', nil, :size => 25 %><br />
<em><%= l(:text_caracters_minimum, 4) %></em></p>
<% form_tag({:token => @token.value}, :class => "tabular") do %>
<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
<p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
<%= password_field_tag 'new_password', nil, :size => 25 %><br />
<em><%= l(:text_length_between, 4, 12) %></em></p>
<p><label for="new_password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %></p>
<p><center><%= submit_tag l(:button_save) %></center></p>
<% end %>
</div>
<p><%= submit_tag l(:button_save) %></p>
<% end %>
</center>

View File

@@ -9,8 +9,7 @@
<%= text_field 'user', 'login', :size => 25 %></p>
<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
<%= password_field_tag 'password', nil, :size => 25 %><br />
<em><%= l(:text_caracters_minimum, 4) %></em></p>
<%= password_field_tag 'password', nil, :size => 25 %></p>
<p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
@@ -30,8 +29,18 @@
<% for @custom_value in @custom_values %>
<p><%= custom_field_tag_with_label @custom_value %></p>
<% end %>
<p><label for="user_mail_notification"><%=l(:field_mail_notification)%></label>
<%= check_box 'user', 'mail_notification' %></p>
<!--[eoform:user]-->
</div>
<%= submit_tag l(:button_submit) %>
<% end %>
<% content_for :header_tags do %>
<%= javascript_include_tag 'calendar/calendar' %>
<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
<%= javascript_include_tag 'calendar/calendar-setup' %>
<%= stylesheet_link_tag 'calendar' %>
<% end %>

View File

@@ -24,5 +24,5 @@
<h3><%=l(:label_activity)%></h3>
<p>
<%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %>
<%=l(:label_reported_issues)%>: <%= Issue.count(["author_id=?", @user.id]) %>
</p>

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