diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 37a810bdf..4b2ec8317 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AccountController < ApplicationController - layout 'base' helper :custom_fields include CustomFieldsHelper @@ -26,11 +25,11 @@ class AccountController < ApplicationController # Show user's account def show @user = User.find_active(params[:id]) - @custom_values = @user.custom_values.find(:all, :include => :custom_field) + @custom_values = @user.custom_values # 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.role_for_project(membership.project)) + membership.project.is_public? || (User.current.member_of?(membership.project)) end rescue ActiveRecord::RecordNotFound render_404 @@ -43,8 +42,17 @@ class AccountController < ApplicationController self.logged_user = nil else # Authenticate user - user = User.try_to_login(params[:login], params[:password]) - if user + user = User.try_to_login(params[:username], params[:password]) + if user.nil? + # Invalid credentials + flash.now[:error] = l(:notice_account_invalid_creditentials) + elsif user.new_record? + # Onthefly creation failed, display the registration form to fill/fix attributes + @user = user + session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id } + render :action => 'register' + else + # Valid user self.logged_user = user # generate a key and set cookie if autologin if params[:autologin] && Setting.autologin? @@ -52,8 +60,6 @@ class AccountController < ApplicationController cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now } end redirect_back_or_default :controller => 'my', :action => 'page' - else - flash.now[:error] = l(:notice_account_invalid_creditentials) end end end @@ -105,43 +111,52 @@ class AccountController < ApplicationController # User self-registration def register - redirect_to(home_url) && return unless Setting.self_registration? + redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration] if request.get? + session[:auth_source_registration] = nil @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] - if params["custom_fields"] - @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 - case Setting.self_registration - when '1' - # Email activation - 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 + if session[:auth_source_registration] @user.status = User::STATUS_ACTIVE + @user.login = session[:auth_source_registration][:login] + @user.auth_source_id = session[:auth_source_registration][:auth_source_id] if @user.save + session[:auth_source_registration] = nil + self.logged_user = @user flash[:notice] = l(:notice_account_activated) - redirect_to :action => 'login' + redirect_to :controller => 'my', :action => 'account' 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' + @user.login = params[:user][:login] + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + case Setting.self_registration + when '1' + # Email activation + 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 + self.logged_user = @user + flash[:notice] = l(:notice_account_activated) + redirect_to :controller => 'my', :action => 'account' + 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' + end end end end diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index b448affcc..a6df49dcd 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -16,13 +16,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AdminController < ApplicationController - layout 'base' before_filter :require_admin helper :sort include SortHelper - def index + def index + @no_configuration_data = Redmine::DefaultData::Loader::no_data? end def projects @@ -35,7 +35,7 @@ class AdminController < ApplicationController @project_count = Project.count(:conditions => conditions) @project_pages = Paginator.new self, @project_count, - 25, + per_page_option, params['page'] @projects = Project.find :all, :order => sort_clause, :conditions => conditions, @@ -44,15 +44,19 @@ class AdminController < ApplicationController render :action => "projects", :layout => false if request.xhr? end - - def mail_options - @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted) + + # Loads the default configuration + # (roles, trackers, statuses, workflow, enumerations) + def default_configuration if request.post? - Setting.notified_events = (params[:notified_events] || []) - Setting.emails_footer = params[:emails_footer] if params[:emails_footer] - flash[:notice] = l(:notice_successful_update) - redirect_to :controller => 'admin', :action => 'mail_options' + 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 end + redirect_to :action => 'index' end def test_email @@ -66,7 +70,7 @@ class AdminController < ApplicationController flash[:error] = l(:notice_email_error, e.message) end ActionMailer::Base.raise_delivery_errors = raise_delivery_errors - redirect_to :action => 'mail_options' + redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications' end def info diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 991b3fff7..d21d0bd8c 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -15,35 +15,43 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'uri' + class ApplicationController < ActionController::Base + layout 'base' + before_filter :user_setup, :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 logged_in_user - User.current.logged? ? User.current : nil - end - def current_role @current_role ||= User.current.role_for_project(@project) 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 if session[:user_id] # existing session - User.current = User.find(session[:user_id]) + (User.find_active(session[:user_id]) rescue nil) elsif cookies[:autologin] && Setting.autologin? # auto-login feature - User.current = User.find_by_autologin_key(cookies[:autologin]) + User.find_by_autologin_key(cookies[:autologin]) elsif params[:key] && accept_key_auth_actions.include?(params[:action]) # RSS key authentication - User.current = User.find_by_rss_key(params[:key]) - else - User.current = User.anonymous + User.find_by_rss_key(params[:key]) end end @@ -55,13 +63,14 @@ class ApplicationController < ActionController::Base end def set_localization + User.current.language = nil unless User.current.logged? lang = begin - if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym + if !User.current.language.blank? && GLoc.valid_language?(User.current.language) User.current.language elsif request.env['HTTP_ACCEPT_LANGUAGE'] - 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 + 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 end end rescue @@ -72,8 +81,7 @@ class ApplicationController < ActionController::Base def require_login if !User.current.logged? - store_location - redirect_to :controller => "account", :action => "login" + redirect_to :controller => "account", :action => "login", :back_url => request.request_uri return false end true @@ -87,39 +95,43 @@ class ApplicationController < ActionController::Base end true end + + def deny_access + User.current.logged? ? render_403 : require_login + end # Authorize the user for the requested action def authorize(ctrl = params[:controller], action = params[:action]) allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project) - allowed ? true : (User.current.logged? ? render_403 : require_login) + allowed ? true : deny_access 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 - unless @project.active? + 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 @project = nil render_404 - return false + false end - return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin? - User.current.logged? ? render_403 : require_login end - # store current uri in session. - # return to this location by calling redirect_back_or_default - def store_location - session[:return_to_params] = params - end - - # move to the last store_location call or to the passed default one def redirect_back_or_default(default) - if session[:return_to_params].nil? - redirect_to default - else - redirect_to session[:return_to_params] - session[:return_to_params] = nil + back_url = params[:back_url] + if !back_url.blank? + uri = URI.parse(back_url) + # do not redirect user to another host + if uri.relative? || (uri.host == request.host) + redirect_to(back_url) and return + end end + redirect_to default end def render_403 @@ -133,9 +145,15 @@ class ApplicationController < ActionController::Base 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 @@ -148,6 +166,38 @@ class ApplicationController < ActionController::Base 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 @@ -167,4 +217,9 @@ 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 diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index 0913de529..788bab94d 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -16,24 +16,40 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AttachmentsController < ApplicationController - layout 'base' - before_filter :find_project, :check_project_privacy + before_filter :find_project + def show + if @attachment.is_diff? + @diff = File.new(@attachment.diskfile, "rb").read + render :action => 'diff' + elsif @attachment.is_text? + @content = File.new(@attachment.diskfile, "rb").read + render :action => 'file' + elsif + download + end + end + def download + @attachment.increment_download if @attachment.container.is_a?(Version) + # images are sent inline - send_file @attachment.diskfile, :filename => @attachment.filename, + send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), :type => @attachment.content_type, :disposition => (@attachment.image? ? 'inline' : 'attachment') - rescue - # in case the disk file was deleted - render_404 end private def find_project @attachment = Attachment.find(params[:id]) + # Show 404 if the filename in the url is wrong + raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename + @project = @attachment.project - rescue + permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym + allowed = User.current.allowed_to?(permission, @project) + allowed ? true : (User.current.logged? ? render_403 : require_login) + rescue ActiveRecord::RecordNotFound render_404 end end diff --git a/app/controllers/auth_sources_controller.rb b/app/controllers/auth_sources_controller.rb index b830f1970..981f29f03 100644 --- a/app/controllers/auth_sources_controller.rb +++ b/app/controllers/auth_sources_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AuthSourcesController < ApplicationController - layout 'base' before_filter :require_admin def index diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 3a8b021a3..4532a88fe 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class BoardsController < ApplicationController - layout 'base' before_filter :find_project, :authorize helper :messages @@ -40,8 +39,8 @@ class BoardsController < ApplicationController sort_update @topic_count = @board.topics.count - @topic_pages = Paginator.new self, @topic_count, 25, params['page'] - @topics = @board.topics.find :all, :order => sort_clause, + @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}", :include => [:author, {:last_reply => :author}], :limit => @topic_pages.items_per_page, :offset => @topic_pages.current.offset diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index 1e1c988d9..4589996f1 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class CustomFieldsController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -39,6 +38,8 @@ class CustomFieldsController < ApplicationController @custom_field = UserCustomField.new(params[:custom_field]) when "ProjectCustomField" @custom_field = ProjectCustomField.new(params[:custom_field]) + when "TimeEntryCustomField" + @custom_field = TimeEntryCustomField.new(params[:custom_field]) else redirect_to :action => 'list' return diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 63ee96134..dbf9cd8e5 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -16,13 +16,42 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class DocumentsController < ApplicationController - layout 'base' - before_filter :find_project, :authorize - + 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 + 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]) @@ -33,26 +62,12 @@ class DocumentsController < ApplicationController def destroy @document.destroy - redirect_to :controller => 'projects', :action => 'list_documents', :id => @project + redirect_to :controller => 'documents', :action => 'index', :project_id => @project end - - def download - @attachment = @document.attachments.find(params[:attachment_id]) - @attachment.increment_download - send_file @attachment.diskfile, :filename => @attachment.filename, :type => @attachment.content_type - rescue - render_404 - end def add_attachment - # 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_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('document_added') + attachments = attach_files(@document, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added') redirect_to :action => 'show', :id => @document end @@ -63,9 +78,15 @@ 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 diff --git a/app/controllers/enumerations_controller.rb b/app/controllers/enumerations_controller.rb index 7a7f1685a..50521bab8 100644 --- a/app/controllers/enumerations_controller.rb +++ b/app/controllers/enumerations_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class EnumerationsController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -75,11 +74,20 @@ class EnumerationsController < ApplicationController 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" - redirect_to :action => 'list' + @enumeration = Enumeration.find(params[:id]) + if !@enumeration.in_use? + # No associated objects + @enumeration.destroy + redirect_to :action => 'index' + elsif params[:reassign_to_id] + if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id]) + @enumeration.destroy(reassign_to) + redirect_to :action => 'index' + end + end + @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration] + #rescue + # flash[:error] = 'Unable to delete enumeration' + # redirect_to :action => 'index' end end diff --git a/app/controllers/issue_categories_controller.rb b/app/controllers/issue_categories_controller.rb index 2c1c6657b..8315d6eb8 100644 --- a/app/controllers/issue_categories_controller.rb +++ b/app/controllers/issue_categories_controller.rb @@ -16,7 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueCategoriesController < ApplicationController - layout 'base' + menu_item :settings before_filter :find_project, :authorize verify :method => :post, :only => :destroy diff --git a/app/controllers/issue_relations_controller.rb b/app/controllers/issue_relations_controller.rb index cb0ad552a..2ca3f0d68 100644 --- a/app/controllers/issue_relations_controller.rb +++ b/app/controllers/issue_relations_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueRelationsController < ApplicationController - layout 'base' before_filter :find_project, :authorize def new diff --git a/app/controllers/issue_statuses_controller.rb b/app/controllers/issue_statuses_controller.rb index d0712e7c3..69d9db965 100644 --- a/app/controllers/issue_statuses_controller.rb +++ b/app/controllers/issue_statuses_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueStatusesController < ApplicationController - layout 'base' before_filter :require_admin verify :method => :post, :only => [ :destroy, :create, :update, :move ], diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 92443441c..7d572924f 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -16,13 +16,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssuesController < ApplicationController - layout 'base' - before_filter :find_project, :authorize, :except => [:index, :changes, :preview] + menu_item :new_issue, :only => :new + + before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment] + before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] + before_filter :find_project, :only => [:new, :update_form, :preview] + before_filter :authorize, :except => [:index, :changes, :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 @@ -39,13 +42,20 @@ class IssuesController < ApplicationController helper :sort include SortHelper include IssuesHelper + helper :timelog def index sort_init "#{Issue.table_name}.id", "desc" sort_update retrieve_query if @query.valid? - limit = %w(pdf csv).include?(params[:format]) ? Setting.issues_export_limit.to_i : 25 + 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, @@ -55,7 +65,7 @@ class IssuesController < ApplicationController :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.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') } end @@ -63,6 +73,8 @@ class IssuesController < ApplicationController # Send html if the query is not valid render(:template => 'issues/index.rhtml', :layout => !request.xhr?) end + rescue ActiveRecord::RecordNotFound + render_404 end def changes @@ -70,113 +82,239 @@ class IssuesController < ApplicationController sort_update retrieve_query if @query.valid? - @changes = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], + @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], :conditions => @query.statement, :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 = @issue.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") - @status_options = @issue.status.find_new_statuses_allowed_to(logged_in_user.role_for_project(@project), @issue.tracker) if logged_in_user + @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) + @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 end - def edit - @priorities = Enumeration::get_values('IPRI') - 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 - begin - @issue.init_journal(self.logged_in_user) - # Retrieve custom fields and values - if params["custom_fields"] - @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 - @issue.attributes = params[:issue] - if @issue.save - flash[:notice] = l(:notice_successful_update) - redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) - end - rescue ActiveRecord::StaleObjectError - # Optimistic locking exception - flash[:error] = l(:notice_locking_conflict) - end - end - end - - def add_note - journal = @issue.init_journal(User.current, params[:notes]) - 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 - if journal.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 + # Add a new issue + # The new issue will be created from an existing one if copy_from parameter is given + def new + @issue = Issue.new + @issue.copy_from(params[:copy_from]) if params[:copy_from] + @issue.project = @project + # Tracker must be set before custom field values + @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][: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 - show + @issue.attributes = params[:issue] + @issue.author = User.current + + 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 + else + requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) + # Check that the user is allowed to apply the requested status + @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status + 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) + @priorities = Enumeration::get_values('IPRI') + @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + @time_entry = TimeEntry.new + + @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 - 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 current_role.allowed_to?(:log_time) - @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 - + if request.post? + @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 (@time_entry.hours.nil? || @time_entry.valid?) && @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 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 - rescue ActiveRecord::StaleObjectError - # Optimistic locking exception - flash[:error] = l(:notice_locking_conflict) + redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) end - end - @assignable_to = @project.members.find(:all, :include => :user).collect{ |m| m.user } - @activities = Enumeration::get_values('ACTI') + end + rescue ActiveRecord::StaleObjectError + # Optimistic locking exception + flash.now[:error] = l(:notice_locking_conflict) end + def reply + journal = Journal.find(params[:journal_id]) if params[:journal_id] + if journal + user = journal.user + text = journal.notes + else + user = @issue.author + text = @issue.description + end + content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " + content << text.to_s.strip.gsub(%r{
((.|\s)*?)
}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" + render(:update) { |page| + page.<< "$('notes').value = \"#{content}\";" + page.show 'update' + page << "Form.Element.focus('notes');" + page << "Element.scrollTo('update');" + page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;" + } + 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? + call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) + # Don't save any change to the issue if the user is not authorized to apply the requested status + if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save + # Send notification for each issue (if changed) + 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(params[:back_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| + issue.init_journal(User.current) + 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 + def destroy - @issue.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 + return + end + end + @issues.each(&:destroy) redirect_to :action => 'index', :project_id => @project end def destroy_attachment a = @issue.attachments.find(params[:attachment_id]) a.destroy - journal = @issue.init_journal(self.logged_in_user) + journal = @issue.init_journal(User.current) journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :old_value => a.filename) @@ -185,34 +323,74 @@ class IssuesController < ApplicationController 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) + end + projects = @issues.collect(&:project).compact.uniq + @project = projects.first if projects.size == 1 + + @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), + :log_time => (@project && User.current.allowed_to?(:log_time, @project)), + :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), + :move => (@project && User.current.allowed_to?(:move_issues, @project)), + :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) + } + if @project + @assignables = @project.assignable_users + @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + end + @priorities = Enumeration.get_values('IPRI').reverse @statuses = IssueStatus.find(:all, :order => 'position') - @allowed_statuses = @issue.status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker) - @assignables = @issue.assignable_users - @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to) - @can = {:edit => User.current.allowed_to?(:edit_issues, @project), - :change_status => User.current.allowed_to?(:change_issue_status, @project), - :add => User.current.allowed_to?(:add_issues, @project), - :move => User.current.allowed_to?(:move_issues, @project), - :delete => User.current.allowed_to?(:delete_issues, @project)} + @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 = Issue.find_by_id(params[:id]) - @attachements = issue.attachments if issue - @text = params[:issue][:description] + @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_project + def find_issue @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) @project = @issue.project 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]) @@ -223,14 +401,16 @@ private # Retrieve query from session or build a new query def retrieve_query - if params[:query_id] - @query = Query.find(params[:query_id], :conditions => {:project_id => (@project ? @project.id : nil)}) - @query.executed_by = logged_in_user - session[:query] = @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] or !session[:query] or session[:query].project != @project + 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 => "_", :executed_by => logged_in_user) + @query = Query.new(:name => "_") @query.project = @project if params[:fields] and params[:fields].is_a? Array params[:fields].each do |field| @@ -241,9 +421,11 @@ private @query.add_short_filter(field, params[field]) if params[field] end end - session[:query] = @query + session[:query] = {:project_id => @query.project_id, :filters => @query.filters} else - @query = session[:query] + @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] + @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) + @query.project = @project end end end diff --git a/app/controllers/journals_controller.rb b/app/controllers/journals_controller.rb new file mode 100644 index 000000000..6df54f098 --- /dev/null +++ b/app/controllers/journals_controller.rb @@ -0,0 +1,40 @@ +# 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 + 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 diff --git a/app/controllers/mail_handler_controller.rb b/app/controllers/mail_handler_controller.rb new file mode 100644 index 000000000..8bcfce630 --- /dev/null +++ b/app/controllers/mail_handler_controller.rb @@ -0,0 +1,44 @@ +# 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 MailHandlerController < ActionController::Base + before_filter :check_credential + + verify :method => :post, + :only => :index, + :render => { :nothing => true, :status => 405 } + + # Submits an incoming email to MailHandler + def index + options = params.dup + email = options.delete(:email) + if MailHandler.receive(email, options) + render :nothing => true, :status => :created + else + render :nothing => true, :status => :unprocessable_entity + end + end + + private + + def check_credential + User.current = nil + unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key + render :nothing => true, :status => 403 + end + end +end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index a1706e601..1072090bc 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MembersController < ApplicationController - layout 'base' before_filter :find_member, :except => :new before_filter :find_project, :only => :new before_filter :authorize diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 74a957d6c..554279d21 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -16,45 +16,107 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MessagesController < ApplicationController - layout 'base' - before_filter :find_project, :authorize + menu_item :boards + before_filter :find_board, :only => [:new, :preview] + before_filter :find_message, :except => [:new, :preview] + before_filter :authorize, :except => :preview verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } + verify :xhr => true, :only => :quote + 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 = logged_in_user - @message.board = @board + @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 if request.post? && @message.save - 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 + attach_files(@message, params[:attachments]) redirect_to :action => 'show', :id => @message end end + # Reply to a topic def reply @reply = Message.new(params[:reply]) - @reply.author = logged_in_user + @reply.author = User.current @reply.board = @board - @message.children << @reply - redirect_to :action => 'show', :id => @message + @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 quote + user = @message.author + text = @message.content + content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " + content << text.to_s.strip.gsub(%r{
((.|\s)*?)
}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" + render(:update) { |page| + page.<< "$('message_content').value = \"#{content}\";" + page.show 'reply' + page << "Form.Element.focus('message_content');" + page << "Element.scrollTo('reply');" + page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;" + } + end + + def preview + message = @board.messages.find_by_id(params[:id]) + @attachements = message.attachments if message + @text = (params[:message] || params[:reply])[:content] + render :partial => 'common/preview' end private - def find_project + 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 @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 diff --git a/app/controllers/my_controller.rb b/app/controllers/my_controller.rb index 2fa5a9d9c..1cfa3e531 100644 --- a/app/controllers/my_controller.rb +++ b/app/controllers/my_controller.rb @@ -16,17 +16,17 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MyController < ApplicationController - helper :issues - - layout 'base' before_filter :require_login + helper :issues + BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, 'issuesreportedbyme' => :label_reported_issues, 'issueswatched' => :label_watched_issues, 'news' => :label_news_latest, 'calendar' => :label_calendar, - 'documents' => :label_document_plural + 'documents' => :label_document_plural, + 'timelog' => :label_spent_time }.freeze DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], @@ -44,7 +44,7 @@ class MyController < ApplicationController # Show user's page def page - @user = self.logged_in_user + @user = User.current @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT end @@ -76,7 +76,7 @@ class MyController < ApplicationController # Manage user's password def password - @user = self.logged_in_user + @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]) @@ -102,7 +102,7 @@ class MyController < ApplicationController # User's page layout configuration def page_layout - @user = self.logged_in_user + @user = User.current @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup session[:page_layout] = @blocks %w(top left right).each {|f| session[:page_layout][f] ||= [] } @@ -116,7 +116,7 @@ class MyController < ApplicationController def add_block block = params[:block] render(:nothing => true) and return unless block && (BLOCKS.keys.include? block) - @user = self.logged_in_user + @user = User.current # remove if already present in a group %w(top left right).each {|f| (session[:page_layout][f] ||= []).delete block } # add it on top @@ -151,7 +151,7 @@ class MyController < ApplicationController # Save user's page layout def page_layout_save - @user = self.logged_in_user + @user = User.current @user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout] @user.pref.save session[:page_layout] = nil diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index c41c5844e..b5f7ca1b2 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -16,8 +16,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class NewsController < ApplicationController - layout 'base' - before_filter :find_project, :authorize, :except => :index + before_filter :find_news, :except => [:new, :index, :preview] + before_filter :find_project, :only => [:new, :preview] + before_filter :authorize, :except => [:index, :preview] before_filter :find_optional_project, :only => :index accept_key_auth :index @@ -34,8 +35,22 @@ class NewsController < ApplicationController end 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) @@ -45,7 +60,7 @@ class NewsController < ApplicationController def add_comment @comment = Comment.new(params[:comment]) - @comment.author = logged_in_user + @comment.author = User.current if @news.comments << @comment flash[:notice] = l(:label_comment_added) redirect_to :action => 'show', :id => @news @@ -64,14 +79,25 @@ class NewsController < ApplicationController redirect_to :action => 'index', :project_id => @project end + def preview + @text = (params[:news] ? params[:news][:description] : nil) + render :partial => 'common/preview' + end + private - def find_project + def find_news @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]) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 289b34e24..0d83d81b3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -16,16 +16,19 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class ProjectsController < ApplicationController - layout 'base' - before_filter :find_project, :except => [ :index, :list, :add ] - before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ] + 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 :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 ] - cache_sweeper :version_sweeper, :only => [ :add_version ] - helper :sort include SortHelper helper :custom_fields @@ -40,34 +43,39 @@ class ProjectsController < ApplicationController include RepositoriesHelper include ProjectsHelper - def index - list - render :action => 'list' unless request.xhr? - end - # Lists visible projects - def list + def index projects = Project.find :all, - :conditions => Project.visible_by(logged_in_user), + :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]} + respond_to do |format| + format.html { + @project_tree = projects.group_by {|p| p.parent || p} + @project_tree.keys.each {|p| @project_tree[p] -= [p]} + } + format.atom { + render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), + :title => "#{Setting.app_title}: #{l(:label_project_latest)}") + } + end end # Add a new project def add - @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") - @root_projects = Project.find(:all, :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}") + @issue_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') @project = Project.new(params[:project]) - @project.enabled_module_names = Redmine::AccessControl.available_project_modules if request.get? - @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) } + @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? + @project.trackers = Tracker.all + @project.is_public = Setting.default_projects_public? + @project.enabled_module_names = Redmine::AccessControl.available_project_modules else - @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids] - @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } - @project.custom_values = @custom_values + @project.enabled_module_names = params[:enabled_modules] if @project.save - @project.enabled_module_names = params[:enabled_modules] flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'admin', :action => 'projects' end @@ -76,23 +84,36 @@ class ProjectsController < ApplicationController # Show @project def show - @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position") @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} - @subprojects = @project.active_children + @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current)) @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") - @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]) - @total_hours = @project.time_entries.sum(:hours) + @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 end def settings - @root_projects = Project::find(:all, :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id]) - @custom_fields = IssueCustomField.find(:all) + @root_projects = Project.find(:all, + :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], + :order => 'name') + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @issue_category ||= IssueCategory.new @member ||= @project.members.new - @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) } + @trackers = Tracker.all @repository ||= @project.repository @wiki ||= @project.wiki end @@ -100,11 +121,6 @@ class ProjectsController < ApplicationController # 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]) } - @project.custom_values = @custom_values - end @project.attributes = params[:project] if @project.save flash[:notice] = l(:notice_successful_update) @@ -170,288 +186,68 @@ class ProjectsController < ApplicationController end end - # Add a new document to @project - def add_document - @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_added(@document) if Setting.notified_events.include?('document_added') - redirect_to :action => 'list_documents', :id => @project - end - end - - # Show documents list of @project - def list_documents - @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 - - # Add a new issue to @project - # The new issue will be created from an existing one if copy_from parameter is given - def add_issue - @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue]) - @issue.project = @project - @issue.author = User.current - @issue.tracker ||= Tracker.find(params[:tracker_id]) - - 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(logged_in_user.role_for_project(@project), @issue.tracker))if logged_in_user - - if request.get? - @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 = IssueStatus.find_by_id(params[:issue][:status_id]) - # Check that the user is allowed to apply the requested status - @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status - @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) } - @issue.custom_values = @custom_values - if @issue.save - if params[:attachments] && params[:attachments].is_a?(Array) - # Save attachments - params[:attachments].each {|a| Attachment.create(:container => @issue, :file => a, :author => User.current) unless a.size == 0} - end - flash[:notice] = l(:notice_successful_create) - Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') - redirect_to :controller => 'issues', :action => 'index', :project_id => @project - return - end - end - @priorities = Enumeration::get_values('IPRI') - end - - # Bulk edit issues - def bulk_edit_issues - 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? ? nil : User.find_by_id(params[:assigned_to_id]) - category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id]) - fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id]) - issues = @project.issues.find_all_by_id(params[:issue_ids]) - 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 - issue.fixed_version = fixed_version if fixed_version - 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 - if current_role && User.current.allowed_to?(:change_issue_status, @project) - # 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 - render :update do |page| - page.hide 'query_form' - page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form' - end - end - - def move_issues - @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids] - redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues - @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 - @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') - else - User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)} - end - # issue can be moved to any tracker - @trackers = Tracker.find(:all) - if request.post? && params[:new_project_id] && @projects.collect(&:id).include?(params[:new_project_id].to_i) && params[:new_tracker_id] - new_project = Project.find_by_id(params[:new_project_id]) - new_tracker = params[:new_tracker_id].blank? ? nil : Tracker.find_by_id(params[:new_tracker_id]) - unsaved_issue_ids = [] - @issues.each do |issue| - unsaved_issue_ids << issue.id unless issue.move_to(new_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 - 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) - 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 add_file if request.post? @version = @project.versions.find_by_id(params[:version_id]) - # 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_added(@attachments) if !@attachments.empty? && Setting.notified_events.include?('file_added') + attachments = attach_files(@version, params[:attachments]) + Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added') redirect_to :controller => 'projects', :action => 'list_files', :id => @project end @versions = @project.versions.sort end def list_files - @versions = @project.versions.sort + sort_init "#{Attachment.table_name}.filename", "asc" + sort_update + @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse + render :layout => !request.xhr? end # Show changelog for @project def changelog - @trackers = Tracker.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') + @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position') retrieve_selected_tracker_ids(@trackers) @versions = @project.versions.sort end def roadmap - @trackers = Tracker.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position') + @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true]) retrieve_selected_tracker_ids(@trackers) @versions = @project.versions.sort @versions = @versions.select {|v| !v.completed? } unless params[:completed] end def activity - 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 - - case params[:format] - when 'atom' - # 30 last days - @date_from = Date.today - 30 - @date_to = Date.today + 1 - else - # current month - @date_from = Date.civil(@year, @month, 1) - @date_to = @date_from >> 1 - end + @days = Setting.activity_days_default.to_i - @event_types = %w(issues news files documents changesets wiki_pages messages) - @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)} - - @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 = [] - - if @scope.include?('issues') - @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] ) - @events += @project.issues_status_changes(@date_from, @date_to) - end - - if @scope.include?('news') - @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author ) - end - - if @scope.include?('files') - @events += 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 ) - end - - if @scope.include?('documents') - @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] ) - @events += 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 ) - end - - if @scope.include?('wiki_pages') - 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" - 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 " - conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", - @project.id, @date_from, @date_to] - - @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions) + if params[:from] + begin; @date_to = params[:from].to_date; rescue; end end - if @scope.include?('changesets') - @events += @project.repository.changesets.find(:all, :conditions => ["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]) - end + @date_to ||= Date.today + 1 + @date_from = @date_to - @days + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - if @scope.include?('messages') - @events += Message.find(:all, - :include => [:board, :author], - :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to]) - end - - @events_by_day = @events.group_by(&:event_date) + @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects) + @activity.scope_select {|t| !params["show_#{t}"].nil?} + @activity.default_scope! if @activity.scope.empty? + + events = @activity.events(@date_from, @date_to) respond_to do |format| - format.html { render :layout => false if request.xhr? } - format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") } + format.html { + @events_by_day = events.group_by(&:event_date) + render :layout => false if request.xhr? + } + format.atom { + title = (@activity.scope.size == 1) ? l("label_#{@activity.scope.first.singularize}_plural") : l(:label_activity) + render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") + } end end def calendar - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.rolled_up_trackers retrieve_selected_tracker_ids(@trackers) if params[:year] and params[:year].to_i > 1900 @@ -463,22 +259,23 @@ class ProjectsController < ApplicationController @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(params[:with_subprojects]) do + @project.issues_with_subprojects(@with_subprojects) do events += Issue.find(:all, :include => [:tracker, :status, :assigned_to, :priority, :project], :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] ) unless @selected_tracker_ids.empty? + events += Version.find(:all, :include => :project, + :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) end - events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) @calendar.events = events render :layout => false if request.xhr? end def gantt - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.rolled_up_trackers retrieve_selected_tracker_ids(@trackers) if params[:year] and params[:year].to_i >0 @@ -506,16 +303,25 @@ class ProjectsController < ApplicationController @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(params[:with_subprojects]) do + @project.issues_with_subprojects(@with_subprojects) do + # Issues that have start and due dates @events += Issue.find(:all, :order => "start_date, due_date", :include => [:tracker, :status, :assigned_to, :priority, :project], :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] ) unless @selected_tracker_ids.empty? + # Issues that don't have a due date but that are assigned to a version with a date + @events += Issue.find(:all, + :order => "start_date, effective_date", + :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], + :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date?)) and start_date is not null and due_date is null and effective_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to] + ) unless @selected_tracker_ids.empty? + @events += Version.find(:all, :include => :project, + :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to]) end - @events += @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' @@ -541,6 +347,14 @@ private 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 } diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb index 7feafd35b..8500e853a 100644 --- a/app/controllers/queries_controller.rb +++ b/app/controllers/queries_controller.rb @@ -16,21 +16,15 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class QueriesController < ApplicationController - layout 'base' - before_filter :find_project, :authorize - - 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 + menu_item :issues + before_filter :find_query, :except => :new + before_filter :find_optional_project, :only => :new def new @query = Query.new(params[:query]) - @query.project = @project - @query.user = logged_in_user - @query.executed_by = logged_in_user - @query.is_public = false unless current_role.allowed_to?(:manage_public_queries) + @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] params[:fields].each do |field| @@ -52,7 +46,8 @@ class QueriesController < ApplicationController @query.add_filter(field, params[:operators][field], params[:values][field]) end if params[:fields] @query.attributes = params[:query] - @query.is_public = false unless current_role.allowed_to?(:manage_public_queries) + @query.project = nil if params[:query_is_for_all] + @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin? @query.column_names = nil if params[:default_columns] if @query.save @@ -64,19 +59,21 @@ class QueriesController < ApplicationController def destroy @query.destroy if request.post? - redirect_to :controller => 'queries', :project_id => @project + redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 end private - def find_project - if params[:id] - @query = Query.find(params[:id]) - @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 + def find_query + @query = Query.find(params[:id]) + @project = @query.project + render_403 unless @query.editable_by?(User.current) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_optional_project + @project = Project.find(params[:project_id]) if params[:project_id] + User.current.allowed_to?(:save_queries, @project, :global => true) rescue ActiveRecord::RecordNotFound render_404 end diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index 6b95944a0..dd3ece930 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -16,7 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class ReportsController < ApplicationController - layout 'base' + menu_item :issues before_filter :find_project, :authorize def issue_report @@ -25,7 +25,7 @@ class ReportsController < ApplicationController case params[:detail] when "tracker" @field = "tracker_id" - @rows = Tracker.find :all, :order => 'position' + @rows = @project.trackers @data = issues_by_tracker @report_title = l(:field_tracker) render :template => "reports/issue_report_details" @@ -47,6 +47,12 @@ 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 } @@ -60,16 +66,18 @@ class ReportsController < ApplicationController @report_title = l(:field_subproject) render :template => "reports/issue_report_details" else - @trackers = Tracker.find(:all, :order => 'position') + @trackers = @project.trackers @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 @@ -180,7 +188,22 @@ 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, diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 8ff464c5b..2f96e2d66 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -19,20 +19,25 @@ 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 + rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed + def edit @repository = @project.repository if !@repository @repository = Repository.factory(params[:repository_scm]) - @repository.project = @project + @repository.project = @project if @repository end - if request.post? + if request.post? && @repository @repository.attributes = params[:repository] @repository.save end @@ -47,11 +52,11 @@ class RepositoriesController < ApplicationController def show # check if new revisions have been committed in the repository @repository.fetch_changesets if Setting.autofetch_changesets? - # get entries for the browse frame - @entries = @repository.entries('') + # root entries + @entries = @repository.entries('', @rev) # latest changesets @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") - show_error and return unless @entries || @changesets.any? + show_error_not_found unless @entries || @changesets.any? end def browse @@ -59,20 +64,23 @@ class RepositoriesController < ApplicationController if request.xhr? @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) else - show_error unless @entries + show_error_not_found and return unless @entries + @properties = @repository.properties(@path, @rev) + render :action => 'browse' end end def changes - @entry = @repository.scm.entry(@path, @rev) - show_error and return unless @entry + @entry = @repository.entry(@path, @rev) + show_error_not_found and return unless @entry @changesets = @repository.changesets_for_path(@path) + @properties = @repository.properties(@path, @rev) end def revisions @changeset_count = @repository.changesets.count @changeset_pages = Paginator.new self, @changeset_count, - 25, + per_page_option, params['page'] @changesets = @repository.changesets.find(:all, :limit => @changeset_pages.items_per_page, @@ -85,40 +93,69 @@ class RepositoriesController < ApplicationController end def entry - @content = @repository.scm.cat(@path, @rev) - show_error and return unless @content - if 'raw' == params[:format] + @entry = @repository.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.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 send_data @content, :filename => @path.split('/').last - end + else + # Prevent empty lines when displaying a file with Windows style eol + @content.gsub!("\r\n", "\n") + end + end + + def annotate + @annotate = @repository.scm.annotate(@path, @rev) + render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? end def revision @changeset = @repository.changesets.find_by_revision(@rev) - show_error and return unless @changeset + raise ChangesetNotFound unless @changeset @changes_count = @changeset.changes.size @changes_pages = Paginator.new self, @changes_count, 150, params['page'] @changes = @changeset.changes.find(:all, :limit => @changes_pages.items_per_page, :offset => @changes_pages.current.offset) - - render :action => "revision", :layout => false if request.xhr? + + respond_to do |format| + format.html + format.js {render :layout => false} + end + rescue ChangesetNotFound + show_error_not_found end def diff - @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1) - @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 - - @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 and return unless @diff + if params[:format] == 'diff' + @diff = @repository.diff(@path, @rev, @rev_to) + show_error_not_found and return unless @diff + filename = "changeset_r#{@rev}" + filename << "_r#{@rev_to}" if @rev_to + send_data @diff.join, :filename => "#{filename}.diff", + :type => 'text/x-patch', + :disposition => 'attachment' + else + @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 + + @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) + show_error_not_found unless @diff + end end end @@ -148,20 +185,30 @@ private 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 ||= '' - @rev = params[:rev].to_i if params[:rev] + @rev = params[:rev] + @rev_to = params[:rev_to] + raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE) rescue ActiveRecord::RecordNotFound render_404 + rescue InvalidRevisionParam + show_error_not_found end - def show_error - flash.now[:error] = l(:notice_scm_error) - render :nothing => true, :layout => true + def show_error_not_found + render_error l(:error_scm_not_found) + end + + # Handler for Redmine::Scm::Adapters::CommandFailed exception + def show_error_command_failed(exception) + render_error l(:error_scm_command_failed, exception.message) end def graph_commits_per_month(repository) @@ -182,7 +229,7 @@ private graph = SVG::Graph::Bar.new( :height => 300, - :width => 500, + :width => 800, :fields => fields.reverse, :stack => :side, :scale_integers => true, @@ -220,9 +267,12 @@ 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, + :height => 400, + :width => 800, :fields => fields, :stack => :side, :scale_integers => true, diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index a8f21ff65..72555e5b0 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class RolesController < ApplicationController - layout 'base' before_filter :require_admin verify :method => :post, :only => [ :destroy, :move ], @@ -33,12 +32,18 @@ class RolesController < ApplicationController end def new - @role = Role.new(params[:role]) + # 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) + end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'list' end @permissions = @role.setable_permissions + @roles = Role.find :all, :order => 'builtin, position' end def edit @@ -52,12 +57,11 @@ class RolesController < ApplicationController def destroy @role = Role.find(params[:id]) - #unless @role.members.empty? - # flash[:error] = 'Some members have this role. Can\'t delete it.' - #else - @role.destroy - #end + @role.destroy 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,7 +97,7 @@ class RolesController < ApplicationController end @roles = Role.find(:all, :order => 'builtin, position') @trackers = Tracker.find(:all, :order => 'position') - @statuses = IssueStatus.find(:all, :include => :workflows, :order => 'position') + @statuses = IssueStatus.find(:all, :order => 'position') end def report diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 2c00b3d74..e6e66f05c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -16,7 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class SearchController < ApplicationController - layout 'base' + before_filter :find_optional_project helper :messages include MessagesHelper @@ -27,73 +27,78 @@ class SearchController < ApplicationController @all_words = params[:all_words] || (params[:submit] ? false : true) @titles_only = !params[:titles_only].nil? + projects_to_search = + case params[:scope] + when 'all' + nil + when 'my_projects' + User.current.memberships.collect(&:project) + when 'subprojects' + @project ? ([ @project ] + @project.active_children) : nil + else + @project + end + offset = nil begin; offset = params[:offset].to_time if params[:offset]; rescue; end # quick jump to an issue - if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(logged_in_user)) + if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current)) redirect_to :controller => "issues", :action => "show", :id => $1 return end - if params[:id] - find_project - return unless check_project_privacy - end - - if @project + @object_types = %w(issues news documents changesets wiki_pages messages projects) + if projects_to_search.is_a? Project + # don't search projects + @object_types.delete('projects') # 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) + @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)} end + + @scope = @object_types.select {|t| params[t]} + @scope = @object_types if @scope.empty? + # 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 = @question.split.uniq.select {|w| w.length > 2 } + @tokens = @tokens.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 = [] + @results_by_type = Hash.new {|h,k| h[k] = 0} + 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 + @scope.each do |s| + r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search, + :all_words => @all_words, + :titles_only => @titles_only, + :limit => (limit+1), + :offset => offset, + :before => params[:previous].nil?) + @results += r + @results_by_type[s] += c + 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 - operator = @all_words ? ' AND ' : ' OR ' - 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' + @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 - # 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 @@ -101,8 +106,10 @@ class SearchController < ApplicationController end private - def find_project + def find_optional_project + return true unless params[:id] @project = Project.find(params[:id]) + check_project_privacy rescue ActiveRecord::RecordNotFound render_404 end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 09af63176..6482a3576 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class SettingsController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -25,10 +24,21 @@ class SettingsController < ApplicationController end def edit - 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 + @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 end + @options = {} + @options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] } + @deliveries = ActionMailer::Base.perform_deliveries end def plugin @@ -39,7 +49,7 @@ class SettingsController < ApplicationController flash[:notice] = l(:notice_successful_update) redirect_to :action => 'plugin', :id => params[:id] end - @partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial] + @partial = @plugin.settings[:partial] @settings = Setting["plugin_#{plugin_id}"] end end diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 68c0edefa..897a50fe5 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -16,55 +16,80 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class TimelogController < ApplicationController - layout 'base' - before_filter :find_project, :authorize + menu_item :issues + before_filter :find_project, :authorize, :only => [:edit, :destroy] + before_filter :find_optional_project, :only => [:report, :details] + verify :method => :post, :only => :destroy, :redirect_to => { :action => :details } + helper :sort include SortHelper helper :issues + include TimelogHelper + helper :custom_fields + include CustomFieldsHelper def report - @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", - :values => @project.versions, + @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id", + :klass => Project, + :label => :label_project}, + 'version' => {:sql => "#{Issue.table_name}.fixed_version_id", + :klass => Version, :label => :label_version}, 'category' => {:sql => "#{Issue.table_name}.category_id", - :values => @project.issue_categories, + :klass => IssueCategory, :label => :field_category}, 'member' => {:sql => "#{TimeEntry.table_name}.user_id", - :values => @project.users, + :klass => User, :label => :label_member}, 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", - :values => Tracker.find(:all), + :klass => Tracker, :label => :label_tracker}, 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", - :values => Enumeration::get_values('ACTI'), - :label => :label_activity} + :klass => Enumeration, + :label => :label_activity}, + 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id", + :klass => Issue, + :label => :label_issue} } + # Add list and boolean custom fields as available criterias + custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields) + custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf| + @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)", + :format => cf.field_format, + :label => cf.name} + end if @project + + # Add list and boolean time entry custom fields + TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf| + @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)", + :format => cf.field_format, + :label => cf.name} + end + @criterias = params[:criterias] || [] @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} @criterias.uniq! + @criterias = @criterias[0,3] - @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month' + @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' - if params[:date_from] - begin; @date_from = params[:date_from].to_date; rescue; end - end - if params[:date_to] - begin; @date_to = params[:date_to].to_date; rescue; end - end - @date_from ||= Date.civil(Date.today.year, 1, 1) - @date_to ||= Date.civil(Date.today.year, Date.today.month+1, 1) - 1 + retrieve_date_range unless @criterias.empty? sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ') sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ') - sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours" - sql << " 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" + 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" + sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project + sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries) + sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)] + sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" @hours = ActiveRecord::Base.connection.select_all(sql) @@ -76,53 +101,112 @@ 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 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 - render :layout => false if request.xhr? + respond_to do |format| + format.html { render :layout => !request.xhr? } + format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') } + end end def details sort_init 'spent_on', 'desc' sort_update - @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 + cond = ARCondition.new + if @project.nil? + cond << Project.allowed_to_condition(User.current, :view_time_entries) + elsif @issue.nil? + cond << @project.project_condition(Setting.display_subprojects_issues?) + else + cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id] + end - send_csv and return if 'csv' == params[:export] - render :action => 'details', :layout => false if request.xhr? + 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.atom { + entries = TimeEntry.find(:all, + :include => [:project, :activity, :user, {:issue => :tracker}], + :conditions => cond.conditions, + :order => "#{TimeEntry.table_name}.created_on DESC", + :limit => Setting.feeds_limit.to_i) + render_feed(entries, :title => l(:label_spent_time)) + } + 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 end def edit - 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) + render_403 and return if @time_entry && !@time_entry.editable_by?(User.current) + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) @time_entry.attributes = params[:time_entry] if request.post? and @time_entry.save flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue + redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url]) 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 ::ActionController::RedirectBackError + redirect_to :action => 'details', :project_id => @time_entry.project end private @@ -139,34 +223,63 @@ private render_404 return false end + rescue ActiveRecord::RecordNotFound + render_404 end - 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 + def find_optional_project + if !params[:issue_id].blank? + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + elsif !params[:project_id].blank? + @project = Project.find(params[:project_id]) end - export.rewind - send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv') + deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true) + end + + # Retrieves the date range based on predefined ranges or specific from/to param dates + def retrieve_date_range + @free_period = false + @from, @to = nil, nil + + if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?) + case params[:period].to_s + when 'today' + @from = @to = Date.today + when 'yesterday' + @from = @to = Date.today - 1 + when 'current_week' + @from = Date.today - (Date.today.cwday - 1)%7 + @to = @from + 6 + when 'last_week' + @from = Date.today - 7 - (Date.today.cwday - 1)%7 + @to = @from + 6 + when '7_days' + @from = Date.today - 7 + @to = Date.today + when 'current_month' + @from = Date.civil(Date.today.year, Date.today.month, 1) + @to = (@from >> 1) - 1 + when 'last_month' + @from = Date.civil(Date.today.year, Date.today.month, 1) << 1 + @to = (@from >> 1) - 1 + when '30_days' + @from = Date.today - 30 + @to = Date.today + when 'current_year' + @from = Date.civil(Date.today.year, 1, 1) + @to = Date.civil(Date.today.year, 12, 31) + end + elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?)) + begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end + begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end + @free_period = true + else + # default + end + + @from, @to = @to, @from if @from && @to && @from > @to + @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1 + @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) end end diff --git a/app/controllers/trackers_controller.rb b/app/controllers/trackers_controller.rb index 0fc91b527..8c02f9474 100644 --- a/app/controllers/trackers_controller.rb +++ b/app/controllers/trackers_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class TrackersController < ApplicationController - layout 'base' before_filter :require_admin def index @@ -37,14 +36,12 @@ class TrackersController < ApplicationController if request.post? and @tracker.save # workflow copy if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from])) - copy_from.workflows.each do |w| - @tracker.workflows << w.clone - end + @tracker.workflows.copy(copy_from) end flash[:notice] = l(:notice_successful_create) redirect_to :action => 'list' end - @trackers = Tracker.find :all + @trackers = Tracker.find :all, :order => 'position' end def edit diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index cf0128d7c..d2564c2cb 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -16,7 +16,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class UsersController < ApplicationController - layout 'base' before_filter :require_admin helper :sort @@ -33,13 +32,13 @@ class UsersController < ApplicationController sort_init 'login', 'asc' sort_update - @status = params[:status] ? params[:status].to_i : 1 - conditions = nil + @status = params[:status] ? params[:status].to_i : 1 + conditions = "status <> 0" conditions = ["status=?", @status] unless @status == 0 @user_count = User.count(:conditions => conditions) @user_pages = Paginator.new self, @user_count, - 15, + per_page_option, params['page'] @users = User.find :all,:order => sort_clause, :conditions => conditions, @@ -52,14 +51,11 @@ 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) } 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)) } - @user.custom_values = @custom_values if @user.save Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] flash[:notice] = l(:notice_successful_create) @@ -71,50 +67,34 @@ 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) } - else + if request.post? @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]) } - @user.custom_values = @custom_values - end if @user.update_attributes(params[:user]) flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'list' + # Give a string to redirect_to otherwise it would use status param as the response code + redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page])) end end @auth_sources = AuthSource.find(:all) @roles = Role.find_all_givable @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects @membership ||= Member.new + @memberships = @user.memberships end def edit_membership @user = User.find(params[:id]) @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user) @membership.attributes = params[:membership] - if request.post? and @membership.save - flash[:notice] = l(:notice_successful_update) - end - redirect_to :action => 'edit', :id => @user and return + @membership.save if request.post? + redirect_to :action => 'edit', :id => @user, :tab => 'memberships' end def destroy_membership @user = User.find(params[:id]) - if request.post? and Member.find(params[:membership_id]).destroy - flash[:notice] = l(:notice_successful_update) - end - redirect_to :action => 'edit', :id => @user and return + Member.find(params[:membership_id]).destroy if request.post? + redirect_to :action => 'edit', :id => @user, :tab => 'memberships' end - - def destroy - User.find(params[:id]).destroy - redirect_to :action => 'list' - rescue - flash[:error] = "Unable to delete user" - redirect_to :action => 'list' - end end diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index 4e9016ebf..ab2ccb773 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -16,10 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class VersionsController < ApplicationController - layout 'base' + menu_item :roadmap before_filter :find_project, :authorize - cache_sweeper :version_sweeper, :only => [ :edit, :destroy ] + def show + end def edit if request.post? and @version.update_attributes(params[:version]) @@ -35,20 +36,19 @@ class VersionsController < ApplicationController flash[:error] = "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 => @attachment.filename, :type => @attachment.content_type - rescue - render_404 - end def destroy_file @version.attachments.find(params[:attachment_id]).destroy flash[:notice] = l(:notice_successful_delete) redirect_to :controller => 'projects', :action => 'list_files', :id => @project end + + def status_by + respond_to do |format| + format.html { render :action => 'show' } + format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} } + end + end private def find_project diff --git a/app/controllers/watchers_controller.rb b/app/controllers/watchers_controller.rb index 206dc0843..8e6ee3a9e 100644 --- a/app/controllers/watchers_controller.rb +++ b/app/controllers/watchers_controller.rb @@ -16,27 +16,38 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class WatchersController < ApplicationController - layout 'base' - before_filter :require_login, :find_project, :check_project_privacy + before_filter :find_project + before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch] + before_filter :authorize, :only => :new - def add - user = User.current - @watched.add_watcher(user) - respond_to do |format| - format.html { render :text => 'Watcher added.', :layout => true } - format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } - end + verify :method => :post, + :only => [ :watch, :unwatch ], + :render => { :nothing => true, :status => :method_not_allowed } + + def watch + set_watcher(User.current, true) end - def remove - user = User.current - @watched.remove_watcher(user) - respond_to do |format| - format.html { render :text => 'Watcher removed.', :layout => true } - format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } - end + def unwatch + set_watcher(User.current, false) end - + + def new + @watcher = Watcher.new(params[:watcher]) + @watcher.watchable = @watched + @watcher.save if request.post? + respond_to do |format| + format.html { redirect_to :back } + format.js do + render :update do |page| + page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched} + end + end + end + rescue ::ActionController::RedirectBackError + render :text => 'Watcher added.', :layout => true + end + private def find_project klass = Object.const_get(params[:object_type].camelcase) @@ -46,4 +57,14 @@ private rescue render_404 end + + def set_watcher(user, watching) + @watched.set_watcher(user, watching) + respond_to do |format| + format.html { redirect_to :back } + format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} } + end + rescue ::ActionController::RedirectBackError + render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true + end end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index 2eac2268f..b8108e8ac 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -16,10 +16,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class WelcomeController < ApplicationController - layout 'base' def index - @news = News.latest logged_in_user - @projects = Project.latest logged_in_user + @news = News.latest User.current + @projects = Project.latest User.current end end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 7609323f4..46df2931e 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -18,10 +18,9 @@ require 'diff' class WikiController < ApplicationController - layout 'base' before_filter :find_wiki, :authorize - verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index } + verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index } helper :attachments include AttachmentsHelper @@ -48,12 +47,14 @@ class WikiController < ApplicationController send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") return end + @editable = editable? render :action => 'show' end # edit an existing page or a new one def edit @page = @wiki.find_or_new_page(params[:page]) + return render_403 unless editable? @page.content = WikiContent.new(:page => @page) if @page.new_record? @content = @page.content_for_version(params[:version]) @@ -69,7 +70,7 @@ class WikiController < ApplicationController #@content.text = params[:content][:text] #@content.comments = params[:content][:comments] @content.attributes = params[:content] - @content.author = logged_in_user + @content.author = User.current # if page is new @page.save will also save content, but not if page isn't a new record if (@page.new_record? ? @page.save : @content.save) redirect_to :action => 'index', :id => @project, :page => @page.title @@ -82,7 +83,8 @@ class WikiController < ApplicationController # rename a page def rename - @page = @wiki.find_page(params[:page]) + @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.redirect_existing_links = true # used to display the *original* title if some AR validation errors occur @original_title = @page.pretty_title @@ -92,12 +94,18 @@ class WikiController < ApplicationController end end + def protect + page = @wiki.find_page(params[:page]) + page.update_attribute :protected, params[:protected] + redirect_to :action => 'index', :id => @project, :page => page.title + end + # show page history def history @page = @wiki.find_page(params[:page]) @version_count = @page.content.versions.count - @version_pages = Paginator.new self, @version_count, 25, params['p'] + @version_pages = Paginator.new self, @version_count, per_page_option, params['p'] # don't load text @versions = @page.content.versions.find :all, :select => "id, author_id, comments, updated_on, version", @@ -114,9 +122,15 @@ 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]) + return render_403 unless editable? @page.destroy if @page redirect_to :action => 'special', :id => @project, :page => 'Page_index' end @@ -132,6 +146,7 @@ class WikiController < ApplicationController :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id", :order => 'title' @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} + @pages_by_parent_id = @pages.group_by(&:parent_id) # export wiki to a single html file when 'export' @pages = @wiki.pages.find :all, :order => 'title' @@ -147,23 +162,26 @@ class WikiController < ApplicationController def preview page = @wiki.find_page(params[:page]) - @attachements = page.attachments if page + # page is nil when previewing a new page + return render_403 unless page.nil? || editable?(page) + if page + @attachements = page.attachments + @previewed = page.content + end @text = params[:content][:text] render :partial => 'common/preview' end def add_attachment @page = @wiki.find_page(params[:page]) - # 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 + return render_403 unless editable? + attach_files(@page, params[:attachments]) redirect_to :action => 'index', :page => @page.title end def destroy_attachment @page = @wiki.find_page(params[:page]) + return render_403 unless editable? @page.attachments.find(params[:attachment_id]).destroy redirect_to :action => 'index', :page => @page.title end @@ -177,4 +195,9 @@ private rescue ActiveRecord::RecordNotFound render_404 end + + # Returns true if the current user is allowed to edit the page, otherwise false + def editable?(page = @page) + page.editable_by?(User.current) + end end diff --git a/app/controllers/wikis_controller.rb b/app/controllers/wikis_controller.rb index 146aaac8a..215d39f4b 100644 --- a/app/controllers/wikis_controller.rb +++ b/app/controllers/wikis_controller.rb @@ -16,14 +16,14 @@ # 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? + @wiki.save if request.post? render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'} end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 215945423..78e5bdc65 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,6 +15,9 @@ # 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' + module ApplicationHelper include Redmine::WikiFormatting::Macros::Definitions @@ -31,14 +34,34 @@ module ApplicationHelper 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]) end + + # Display a link to remote if user is authorized + def link_to_remote_if_authorized(name, options = {}, html_options = nil) + url = options[:url] || {} + link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action]) + end # Display a link to user's account page def link_to_user(user) - link_to user.name, :controller => 'account', :action => 'show', :id => user + user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous' end - def link_to_issue(issue) - link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue + def link_to_issue(issue, options={}) + options[:class] ||= '' + options[:class] << ' issue' + options[:class] << ' closed' if issue.closed? + link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options + end + + # Generates a link to an attachment. + # Options: + # * :text - Link text (default to attachment filename) + # * :download - Force download (default: false) + def link_to_attachment(attachment, options={}) + text = options.delete(:text) || attachment.filename + action = options.delete(:download) ? 'download' : 'show' + + link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options) end def toggle_link(name, id, options={}) @@ -48,14 +71,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 << "location.href='##{id}-anchor'; " - 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({ @@ -79,14 +94,30 @@ module ApplicationHelper def format_time(time, include_date = true) return nil unless time time = time.to_time if time.is_a?(String) + zone = User.current.time_zone + local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time) @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format) @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format) - include_date ? time.strftime("#{@date_format} #{@time_format}") : time.strftime(@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+)}, '\1.\2') 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.name, time_tag) + author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous') + l(:label_added_time_by, author_tag, time_tag) + end + + def l_or_humanize(s) + l_has_string?("label_#{s}".to_sym) ? l("label_#{s}".to_sym) : s.to_s.humanize end def day_name(day) @@ -97,46 +128,80 @@ module ApplicationHelper l(:actionview_datehelper_select_month_names).split(',')[month-1] end - def pagination_links_full(paginator, options={}, html_options={}) - page_param = options.delete(:page_param) || :page + def syntax_highlight(name, content) + type = CodeRay::FileType[name] + type ? CodeRay.scan(content, type).html : h(content) + end + def to_path_param(path) + path.to_s.split(%r{[/\\]}).select {|p| !p.blank?} + end + + def pagination_links_full(paginator, count=nil, 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(('« ' + l(:label_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 + {:update => 'content', + :url => url_param.merge(page_param => paginator.current.previous), + :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous html << (pagination_links_each(paginator, options) do |n| link_to_remote(n.to_s, - {:url => {:params => options.merge(page_param => n)}, :update => 'content'}, - {:href => url_for(:params => options.merge(page_param => n))}) + {:url => {:params => url_param.merge(page_param => n)}, + :update => 'content', + :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(:params => url_param.merge(page_param => n))}) end || '') html << ' ' + link_to_remote((l(:label_next) + ' »'), - {: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 + {:update => 'content', + :url => url_param.merge(page_param => paginator.current.next), + :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next + + unless count.nil? + html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ') + end + html end - def set_html_title(text) - @html_header_title = text + def per_page_links(selected=nil) + url_param = params.dup + url_param.clear if url_param.has_key?(:set_filter) + + links = Setting.per_page_options_array.collect do |n| + n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)}, + {:href => url_for(url_param.merge(:per_page => n))}) + end + links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil end - def html_title - title = [] - title << @project.name if @project - title << @html_header_title - title << Setting.app_title - title.compact.join(' - ') + def breadcrumb(*args) + elements = args.flatten + elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil end - ACCESSKEYS = {:edit => 'e', - :preview => 'r', - :quick_search => 'f', - :search => '4', - }.freeze unless const_defined?(:ACCESSKEYS) + 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) - ACCESSKEYS[s] + Redmine::AccessKeys.key_for s end # Formats text according to system settings. @@ -147,28 +212,34 @@ module ApplicationHelper options = args.last.is_a?(Hash) ? args.pop : {} case args.size when 1 - obj = nil - text = args.shift || '' + obj = options[:object] + text = args.shift when 2 obj = args.shift - text = obj.send(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] + attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) + if attachments - text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m| - align = $1 - filename = $2 + 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 :controller => 'attachments', :action => 'download', :id => found.id - "!#{align}#{image_url}!" + image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found + desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1") + alt = desc.blank? ? nil : "(#{desc})" + "!#{style}#{image_url}#{alt}!" else - "!#{align}#{filename}!" + "!#{style}#{filename}!" end end end @@ -181,18 +252,19 @@ module ApplicationHelper 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 {|project, title, anchor| "#{title}.html" } when :anchor # used for single-file wiki export - format_wiki_link = Proc.new {|project, title| "##{title}" } + format_wiki_link = Proc.new {|project, title, anchor| "##{title}" } else - format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title } + format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) } end - project = options[:project] || @project + project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) - # turn wiki links into html links - # example: + # Wiki links + # + # Examples: # [[mypage]] # [[mypage|mytext]] # wiki links can refer other project wikis, using project name or identifier: @@ -200,47 +272,142 @@ module ApplicationHelper # [[project:|mytext]] # [[project:mypage]] # [[project:mypage|mytext]] - text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m| + text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| link_project = project - page = $1 - title = $3 - if page =~ /^([^\:]+)\:(.*)$/ - link_project = Project.find_by_name($1) || Project.find_by_identifier($1) - page = title || $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'))) + 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 + # extract anchor + anchor = nil + if page =~ /^(.+?)\#(.+)$/ + page, anchor = $1, $2 + end + # 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), anchor), + :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + title || page + end else - # project or wiki doesn't exist - title || page + all end end - # turn issue and revision ids into links - # example: - # #52 -> #52 - # r52 -> r52 (project.id is 6) - text = text.gsub(%r{([\s,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m| - leading, otype, oid = $1, $2, $3 + # 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 + # Forum messages: + # message#1218 -> Link to message with id 1218 + text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m| + leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8 link = nil - if otype == 'r' - if project && (changeset = project.changesets.find_by_revision(oid)) - link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset', - :title => truncate(changeset.comments, 100)) - end - else - if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current)) - link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue', - :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})") - link = content_tag('del', link) if issue.closed? + 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 + when 'message' + if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current)) + link = link_to h(truncate(message.subject, 60)), {:only_path => only_path, + :controller => 'messages', + :action => 'show', + :board_id => message.board, + :id => message.root, + :anchor => (message.parent ? "message-#{message.id}" : nil)}, + :class => 'message' + end + end + elsif sep == ':' + # removes the double quotes if any + 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 => to_path_param(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 || "#{otype}#{oid}") + leading + (link || "#{prefix}#{sep}#{oid}") end text @@ -293,7 +460,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.first <=> y.first } + GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } end def label_tag_for(name, option_tags = nil, options = {}) @@ -303,23 +470,32 @@ module ApplicationHelper def labelled_tabular_form_for(name, object, options, &proc) options[:html] ||= {} - options[:html].store :class, "tabular" + options[:html][:class] = 'tabular' unless options[:html].has_key?(:class) form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc) end + def back_url_hidden_field_tag + back_url = params[:back_url] || request.env['HTTP_REFERER'] + hidden_field_tag('back_url', back_url) unless back_url.blank? + end + def check_all_links(form_name) link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + " | " + link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") end - def progress_bar(pct, options={}) + 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', - (pct > 0 ? content_tag('td', '', :width => "#{pct.floor}%;", :class => 'closed') : '') + - (pct < 100 ? content_tag('td', '', :width => "#{100-pct.floor}%;", :class => 'open') : '') + (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 @@ -341,13 +517,33 @@ module ApplicationHelper end def calendar_for(field_id) + include_calendar_headers_tags image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) + javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });") end + + def include_calendar_headers_tags + unless @calendar_headers_tags_included + @calendar_headers_tags_included = true + content_for :header_tags do + javascript_include_tag('calendar/calendar') + + javascript_include_tag("calendar/lang/calendar-#{current_language}.js") + + javascript_include_tag('calendar/calendar-setup') + + stylesheet_link_tag('calendar') + end + end + end def wikitoolbar_for(field_id) return '' unless Setting.text_formatting == 'textile' - javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();") + + 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) @@ -360,38 +556,3 @@ module ApplicationHelper (@has_content && @has_content[name]) || false 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 - diff --git a/app/helpers/attachments_helper.rb b/app/helpers/attachments_helper.rb index 989cd3e66..ebf417bab 100644 --- a/app/helpers/attachments_helper.rb +++ b/app/helpers/attachments_helper.rb @@ -22,4 +22,8 @@ module AttachmentsHelper render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options} end end + + def to_utf8(str) + str + end end diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index b1b176107..f61824e65 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -17,38 +17,49 @@ module CustomFieldsHelper + def custom_fields_tabs + tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural}, + {:name => 'TimeEntryCustomField', :label => :label_spent_time}, + {: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) + def custom_field_tag(name, custom_value) custom_field = custom_value.custom_field - field_name = "custom_fields[#{custom_field.id}]" - field_id = "custom_fields_#{custom_field.id}" + field_name = "#{name}[custom_field_values][#{custom_field.id}]" + field_id = "#{name}_custom_field_values_#{custom_field.id}" case custom_field.field_format when "date" - text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) + + text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) + calendar_for(field_id) when "text" - text_area 'custom_value', 'value', :name => field_name, :id => field_id, :cols => 60, :rows => 3 + text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%') when "bool" - check_box 'custom_value', 'value', :name => field_name, :id => field_id + check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0') when "list" - select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id + blank_option = custom_field.is_required? ? + (custom_field.default_value.blank? ? "" : '') : + '' + select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id) else - text_field 'custom_value', 'value', :name => field_name, :id => field_id + text_field_tag(field_name, custom_value.value, :id => field_id) end end # Return custom field label tag - def custom_field_label_tag(custom_value) + def custom_field_label_tag(name, custom_value) content_tag "label", custom_value.custom_field.name + (custom_value.custom_field.is_required? ? " *" : ""), - :for => "custom_fields_#{custom_value.custom_field.id}", + :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}", :class => (custom_value.errors.empty? ? nil : "error" ) end # Return custom field tag with its label tag - def custom_field_tag_with_label(custom_value) - custom_field_label_tag(custom_value) + custom_field_tag(custom_value) + def custom_field_tag_with_label(name, custom_value) + custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value) end # Return a string used to display a custom value @@ -62,7 +73,7 @@ module CustomFieldsHelper return "" unless value && !value.empty? case field_format when "date" - begin; l_date(value.to_date); rescue; value end + begin; format_date(value.to_date); rescue; value end when "bool" l_YesNo(value == "1") else diff --git a/app/helpers/ifpdf_helper.rb b/app/helpers/ifpdf_helper.rb index 585dbeeca..2cfca1929 100644 --- a/app/helpers/ifpdf_helper.rb +++ b/app/helpers/ifpdf_helper.rb @@ -27,13 +27,18 @@ module IfpdfHelper def initialize(lang) super() set_language_if_valid lang - case current_language - when :ja + case current_language.to_s + when 'ja' extend(PDF_Japanese) AddSJISFont() @font_for_content = 'SJIS' @font_for_footer = 'SJIS' - when :zh + when 'zh' + extend(PDF_Chinese) + AddGBFont() + @font_for_content = 'GB' + @font_for_footer = 'GB' + when 'zh-tw' extend(PDF_Chinese) AddBig5Font() @font_for_content = 'Big5' diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index f9a88f6dd..43acabd19 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -18,6 +18,7 @@ require 'csv' module IssuesHelper + include ApplicationHelper def render_issue_tooltip(issue) @cached_label_start_date ||= l(:field_start_date) @@ -31,6 +32,19 @@ module IssuesHelper "#{@cached_label_assigned_to}: #{issue.assigned_to}
" + "#{@cached_label_priority}: #{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 @@ -40,9 +54,15 @@ module IssuesHelper when 'due_date', 'start_date' value = format_date(detail.value.to_date) if detail.value old_value = format_date(detail.old_value.to_date) if detail.old_value + when 'project_id' + p = Project.find_by_id(detail.value) and value = p.name if detail.value + p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value when 'status_id' s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value + when 'tracker_id' + t = Tracker.find_by_id(detail.value) and value = t.name if detail.value + t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value when 'assigned_to_id' u = User.find_by_id(detail.value) and value = u.name if detail.value u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value @@ -55,6 +75,9 @@ module IssuesHelper when 'fixed_version_id' v = Version.find_by_id(detail.value) and value = v.name if detail.value v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value + when 'estimated_hours' + value = "%0.02f" % detail.value.to_f unless detail.value.blank? + old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank? end when 'cf' custom_field = CustomField.find_by_id(detail.prop_key) @@ -66,7 +89,8 @@ module IssuesHelper when 'attachment' label = l(:label_attachment) end - + call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value }) + label ||= detail.prop_key value ||= detail.value old_value ||= detail.old_value @@ -75,9 +99,9 @@ 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) + if detail.property == 'attachment' && !value.blank? && a = 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) + value = link_to_attachment(a) else value = content_tag("i", h(value)) if value end @@ -106,6 +130,7 @@ module IssuesHelper def issues_to_csv(issues, project = nil) ic = Iconv.new(l(:general_csv_encoding), 'UTF-8') + decimal_separator = l(:general_csv_decimal_separator) export = StringIO.new CSV::Writer.generate(export, l(:general_csv_separator)) do |csv| # csv header fields @@ -122,13 +147,16 @@ module IssuesHelper l(:field_start_date), l(:field_due_date), l(:field_done_ratio), + l(:field_estimated_hours), l(:field_created_on), l(:field_updated_on) ] - # only export custom fields if project is given - for custom_field in project.all_custom_fields - headers << custom_field.name - end if project + # 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_issue_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| @@ -142,15 +170,15 @@ module IssuesHelper issue.category, issue.fixed_version, issue.author.name, - issue.start_date ? l_date(issue.start_date) : nil, - issue.due_date ? l_date(issue.due_date) : nil, + format_date(issue.start_date), + format_date(issue.due_date), issue.done_ratio, - l_datetime(issue.created_on), - l_datetime(issue.updated_on) + issue.estimated_hours.to_s.gsub('.', decimal_separator), + format_time(issue.created_on), + format_time(issue.updated_on) ] - for custom_field in project.all_custom_fields - fields << (show_value issue.custom_value_for(custom_field)) - end if project + 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 diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb new file mode 100644 index 000000000..45579f771 --- /dev/null +++ b/app/helpers/journals_helper.rb @@ -0,0 +1,40 @@ +# 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) + links = [] + if !journal.notes.blank? + links << link_to_remote(image_tag('comment.png'), + { :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} }, + :title => l(:button_quote)) if options[:reply_links] + links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", + { :controller => 'journals', :action => 'edit', :id => journal }, + :title => l(:button_edit)) if editable + end + content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty? + 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 diff --git a/app/helpers/mail_handler_helper.rb b/app/helpers/mail_handler_helper.rb new file mode 100644 index 000000000..a29a6dd5a --- /dev/null +++ b/app/helpers/mail_handler_helper.rb @@ -0,0 +1,19 @@ +# 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 MailHandlerHelper +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5b78db71c..912482f1c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -18,12 +18,19 @@ module ProjectsHelper def link_to_version(version, options = {}) return '' unless version && version.is_a?(Version) - link_to version.name, {:controller => 'projects', - :action => 'roadmap', - :id => version.project_id, - :completed => (version.completed? ? 1 : nil), - :anchor => version.name - }, options + link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options + end + + def format_activity_title(text) + h(truncate_single_line(text, 100)) + end + + def format_activity_day(date) + date == Date.today ? l(:label_today).titleize : format_date(date) + end + + def format_activity_description(text) + h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...')) end def project_settings_tabs @@ -179,21 +186,13 @@ module ProjectsHelper end # today red line - if Date.today >= @date_from and Date.today <= @date_to + if Date.today >= date_from and Date.today <= date_to gc.stroke('red') - x = (Date.today-@date_from+1)*zoom + subject_width + 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) - - def new_issue_selector - trackers = Tracker.find(:all, :order => 'position') - # can't use form tag inside helper - content_tag('form', - select_tag('tracker_id', '' + options_from_collection_for_select(trackers, 'id', 'name'), :onchange => "if (this.value != '') {this.form.submit()}"), - :action => url_for(:controller => 'projects', :action => 'add_issue', :id => @project), :method => 'get') - end end diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index f92787278..a58c5d0ea 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -22,7 +22,9 @@ module QueriesHelper end def column_header(column) - column.sortable ? sort_header_tag(column.sortable, :caption => column.caption) : content_tag('th', column.caption) + 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) @@ -38,7 +40,7 @@ module QueriesHelper else case column.name when :subject - ((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + + h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) when :done_ratio progress_bar(value, :width => '80px') diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 41218fa79..852ed18d7 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -15,14 +15,21 @@ # 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) + def format_revision(txt) + txt.to_s[0,8] + end + + def render_properties(properties) + unless properties.nil? || properties.empty? + content = '' + properties.keys.sort.each do |property| + content << content_tag('li', "#{h property}: #{h properties[property]}") + end + content_tag('ul', content, :class => 'properties') + end end def to_utf8(str) @@ -44,25 +51,34 @@ module RepositoriesHelper end def scm_select_tag(repository) - container = [[]] - REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]} + scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']] + REDMINE_SUPPORTED_SCM.each do |scm| + scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm) + end + select_tag('repository_scm', - options_for_select(container, repository.class.name.demodulize), + options_for_select(scm_options, 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)") ) end def with_leading_slash(path) - path ||= '' - path.starts_with?("/") ? "/#{path}" : path + path.to_s.starts_with?('/') ? path : "/#{path}" + end + + def without_leading_slash(path) + path.gsub(%r{^/+}, '') end def subversion_field_tags(form, repository) content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) + '
(http://, https://, svn://, file:///)') + content_tag('p', form.text_field(:login, :size => 30)) + - content_tag('p', form.password_field(:password, :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]';")) end def darcs_field_tags(form, repository) @@ -73,8 +89,20 @@ 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 + + def filesystem_field_tags(form, repository) + content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ed2f40b69..cd96dbd3f 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -18,7 +18,8 @@ module SearchHelper def highlight_tokens(text, tokens) return text unless text && tokens && !tokens.empty? - regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE + re_tokens = tokens.collect {|t| Regexp.escape(t)} + regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE result = '' text.split(regexp).each_with_index do |words, i| if result.length > 1200 @@ -35,4 +36,28 @@ module SearchHelper end result end + + def type_label(t) + l("label_#{t.singularize}_plural") + end + + def project_select_tag + options = [[l(:label_project_all), 'all']] + options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? + options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty? + options << [@project.name, ''] unless @project.nil? + select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 + end + + def render_results_by_type(results_by_type) + links = [] + # Sorts types by results count + results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t| + c = results_by_type[t] + next if c == 0 + text = "#{type_label(t)} (#{c})" + links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1) + end + ('') unless links.empty? + end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f53314c40..47e691334 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -16,4 +16,14 @@ # 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 => 'projects', :partial => 'settings/projects', :label => :label_project_plural}, + {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking}, + {:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)}, + {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)}, + {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural} + ] + end end diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index dfd681fff..9ca5c11bd 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -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] + session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC') 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=nil) + def sort_link(column, caption, default_order) key, order = session[@sort_name][:key], session[@sort_name][:order] if key == column if order.downcase == 'asc' @@ -104,15 +104,17 @@ module SortHelper end else icon = nil - order = 'desc' # changed for desc order by default + order = default_order end caption = titleize(Inflector::humanize(column)) unless caption - url = {:sort_key => column, :sort_order => order, :issue_id => params[:issue_id], :project_id => params[:project_id]} + 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) link_to_remote(caption, - {:update => "content", :url => url}, - {:href => url_for(url)}) + + {:update => "content", :url => url_options}, + {:href => url_for(url_options)}) + (icon ? nbsp(2) + image_tag(icon) : '') end @@ -138,8 +140,9 @@ module SortHelper # 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), options) + content_tag('th', sort_link(column, caption, default_order), options) end private diff --git a/app/helpers/timelog_helper.rb b/app/helpers/timelog_helper.rb index 22e4eba0b..f55a8ffe7 100644 --- a/app/helpers/timelog_helper.rb +++ b/app/helpers/timelog_helper.rb @@ -16,8 +16,24 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module TimelogHelper + def render_timelog_breadcrumb + links = [] + links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil}) + links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project + links << link_to_issue(@issue) if @issue + breadcrumb links + end + + def activity_collection_for_select_options + activities = Enumeration::get_values('ACTI') + collection = [] + collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default) + activities.each { |a| collection << [a.name, a.id] } + collection + end + def select_hours(data, criteria, value) - data.select {|row| row[criteria] == value.to_s} + data.select {|row| row[criteria] == value} end def sum_hours(data) @@ -27,4 +43,116 @@ 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') + decimal_separator = l(:general_csv_decimal_separator) + custom_fields = TimeEntryCustomField.find(:all) + 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) + ] + # Export custom fields + headers += custom_fields.collect(&:name) + + 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.to_s.gsub('.', decimal_separator), + entry.comments + ] + fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) } + + 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 diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 9dc87c5cc..5b113e880 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -16,10 +16,43 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. module UsersHelper - def status_options_for_select(selected) - options_for_select([[l(:label_all), "*"], - [l(:status_active), 1], - [l(:status_registered), 2], - [l(:status_locked), 3]], selected) + def users_status_options_for_select(selected) + user_count_by_status = User.count(:group => 'status').to_hash + options_for_select([[l(:label_all), ''], + ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1], + ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2], + ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected) + end + + # Options for the new membership projects combo-box + def projects_options_for_select(projects) + options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") + projects_by_root = projects.group_by(&:root) + projects_by_root.keys.sort.each do |root| + options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root))) + projects_by_root[root].sort.each do |project| + next if project == root + options << content_tag('option', '» ' + h(project.name), :value => project.id) + end + end + options + 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 + + def user_settings_tabs + tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, + {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} + ] end end diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb index 452f4d7fb..0fcc6407c 100644 --- a/app/helpers/versions_helper.rb +++ b/app/helpers/versions_helper.rb @@ -16,4 +16,32 @@ # 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 diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb index c83c785fc..f4767ebed 100644 --- a/app/helpers/watchers_helper.rb +++ b/app/helpers/watchers_helper.rb @@ -24,7 +24,7 @@ module WatchersHelper return '' unless user && user.logged? && object.respond_to?('watched_by?') watched = object.watched_by?(user) url = {:controller => 'watchers', - :action => (watched ? 'remove' : 'add'), + :action => (watched ? 'unwatch' : 'watch'), :object_type => object.class.to_s.underscore, :object_id => object.id} link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)), @@ -33,4 +33,9 @@ module WatchersHelper :class => (watched ? 'icon icon-fav' : 'icon icon-fav-off')) end + + # Returns a comma separated list of users watching the given object + def watchers_list(object) + object.watcher_users.collect {|u| content_tag('span', link_to_user(u), :class => 'user') }.join(",\n") + end end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 980035bd4..0a6b810de 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -17,6 +17,22 @@ module WikiHelper + def render_page_hierarchy(pages, node=nil) + content = '' + if pages[node] + content << "\n" + end + content + end + def html_diff(wdiff) words = wdiff.words.collect{|word| h(word)} words_add = 0 diff --git a/app/models/attachment.rb b/app/models/attachment.rb index d2bcab33f..95ba8491f 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -21,13 +21,24 @@ class Attachment < ActiveRecord::Base belongs_to :container, :polymorphic => true belongs_to :author, :class_name => "User", :foreign_key => "author_id" - validates_presence_of :container, :filename + validates_presence_of :container, :filename, :author validates_length_of :filename, :maximum => 255 validates_length_of :disk_filename, :maximum => 255 acts_as_event :title => :filename, - :description => :filename, - :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}} + :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} + + acts_as_activity_provider :type => 'files', + :permission => :view_files, + :find_options => {:select => "#{Attachment.table_name}.*", + :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + + "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"} + + acts_as_activity_provider :type => 'documents', + :permission => :view_documents, + :find_options => {:select => "#{Attachment.table_name}.*", + :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + + "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} cattr_accessor :storage_path @@storage_path = "#{RAILS_ROOT}/files" @@ -36,75 +47,88 @@ class Attachment < ActiveRecord::Base errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes end - def file=(incomming_file) - unless incomming_file.nil? - @temp_file = incomming_file - if @temp_file.size > 0 - self.filename = sanitize_filename(@temp_file.original_filename) - self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename - self.content_type = @temp_file.content_type.chomp - self.filesize = @temp_file.size - end - end - end + def file=(incoming_file) + unless incoming_file.nil? + @temp_file = incoming_file + if @temp_file.size > 0 + self.filename = sanitize_filename(@temp_file.original_filename) + self.disk_filename = Attachment.disk_filename(filename) + self.content_type = @temp_file.content_type.to_s.chomp + self.filesize = @temp_file.size + end + end + end - def file - nil - end - - # Copy temp file to its final location - def before_save - if @temp_file && (@temp_file.size > 0) - logger.debug("saving '#{self.diskfile}'") - File.open(diskfile, "wb") do |f| - f.write(@temp_file.read) - end - self.digest = Digest::MD5.hexdigest(File.read(diskfile)) - end - # Don't save the content type if it's longer than the authorized length - if self.content_type && self.content_type.length > 255 - self.content_type = nil - end - end - - # Deletes file on the disk - def after_destroy - if self.filename? - File.delete(diskfile) if File.exist?(diskfile) - end - end - - # Returns file's location on disk - def diskfile - "#{@@storage_path}/#{self.disk_filename}" - end + def file + nil + end + + # Copy temp file to its final location + def before_save + if @temp_file && (@temp_file.size > 0) + logger.debug("saving '#{self.diskfile}'") + File.open(diskfile, "wb") do |f| + f.write(@temp_file.read) + end + self.digest = Digest::MD5.hexdigest(File.read(diskfile)) + end + # Don't save the content type if it's longer than the authorized length + if self.content_type && self.content_type.length > 255 + self.content_type = nil + end + end + + # Deletes file on the disk + def after_destroy + File.delete(diskfile) if !filename.blank? && File.exist?(diskfile) + 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.is_a?(Project) ? container : container.project + container.project end def image? - self.filename =~ /\.(jpeg|jpg|gif|png)$/i + self.filename =~ /\.(jpe?g|gif|png)$/i + end + + def is_text? + Redmine::MimeType.is_type?('text', filename) + end + + def is_diff? + self.filename =~ /\.(patch|diff)$/i end private def sanitize_filename(value) - # get only the filename, not the whole path - just_filename = value.gsub(/^.*(\\|\/)/, '') - # NOTE: File.basename doesn't work right with Windows paths on Unix - # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) + # get only the filename, not the whole path + just_filename = value.gsub(/^.*(\\|\/)/, '') + # NOTE: File.basename doesn't work right with Windows paths on Unix + # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/')) - # Finally, replace all non alphanumeric, underscore or periods with underscore - @filename = just_filename.gsub(/[^\w\.\-]/,'_') + # Finally, replace all non alphanumeric, hyphens or periods with underscore + @filename = just_filename.gsub(/[^\w\.\-]/,'_') + end + + # 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 end - end diff --git a/app/models/auth_source.rb b/app/models/auth_source.rb index 47eec106d..a0a2cdc5f 100644 --- a/app/models/auth_source.rb +++ b/app/models/auth_source.rb @@ -20,6 +20,7 @@ class AuthSource < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name + validates_length_of :name, :maximum => 60 def authenticate(login, password) end diff --git a/app/models/auth_source_ldap.rb b/app/models/auth_source_ldap.rb index b79b3ced0..655ffd6d5 100644 --- a/app/models/auth_source_ldap.rb +++ b/app/models/auth_source_ldap.rb @@ -20,13 +20,17 @@ 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? } + validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true + validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true + validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true + validates_numericality_of :port, :only_integer => true 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) @@ -69,11 +73,12 @@ class AuthSourceLdap < AuthSource private def initialize_ldap_con(ldap_user, ldap_password) - Net::LDAP.new( {:host => self.host, - :port => self.port, - :auth => { :method => :simple, :username => ldap_user, :password => ldap_password }, - :encryption => (self.tls ? :simple_tls : nil)} - ) + 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 end def self.get_attr(entry, attr_name) diff --git a/app/models/change.rb b/app/models/change.rb index d14f435a4..385fe5acb 100644 --- a/app/models/change.rb +++ b/app/models/change.rb @@ -19,4 +19,8 @@ class Change < ActiveRecord::Base belongs_to :changeset validates_presence_of :changeset_id, :action, :path + + def relative_path + changeset.repository.relative_path(path) + end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index e4e221732..c4258c88b 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -15,6 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'iconv' + class Changeset < ActiveRecord::Base belongs_to :repository has_many :changes, :dependent => :delete_all @@ -27,17 +29,23 @@ class Changeset < ActiveRecord::Base :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}} acts_as_searchable :columns => 'comments', - :include => :repository, + :include => {:repository => :project}, :project_key => "#{Repository.table_name}.project_id", :date_column => 'committed_on' + + acts_as_activity_provider :timestamp => "#{table_name}.committed_on", + :find_options => {:include => {:repository => :project}} 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) + write_attribute(:comments, Changeset.normalize_comments(comment)) end def committed_on=(date) @@ -45,9 +53,14 @@ class Changeset < ActiveRecord::Base 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? @@ -63,6 +76,14 @@ class Changeset < ActiveRecord::Base 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 + comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match| action = match[0] target_issue_ids = match[1].scan(/\d+/) @@ -71,15 +92,65 @@ 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| - # don't change the status is the issue is already closed + # 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 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 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 + + # Strips and reencodes a commit log before insertion into the database + def self.normalize_comments(str) + to_utf8(str.to_s.strip) + end + + private + + def self.to_utf8(str) + return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii + encoding = Setting.commit_logs_encoding.to_s.strip + unless encoding.blank? || encoding == 'UTF-8' + begin + return Iconv.conv('UTF-8', encoding, str) + rescue Iconv::Failure + # do nothing here + end + end + str + end end diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index c3b5f2a9f..4759b714b 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -30,9 +30,9 @@ class CustomField < ActiveRecord::Base }.freeze validates_presence_of :name, :field_format - validates_uniqueness_of :name + validates_uniqueness_of :name, :scope => :type validates_length_of :name, :maximum => 30 - validates_format_of :name, :with => /^[\w\s\'\-]*$/i + validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys def initialize(attributes = nil) @@ -43,6 +43,9 @@ 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 @@ -50,6 +53,11 @@ 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) @@ -58,7 +66,7 @@ class CustomField < ActiveRecord::Base # to move in project_custom_field def self.for_all - find(:all, :conditions => ["is_for_all=?", true]) + find(:all, :conditions => ["is_for_all=?", true], :order => 'position') end def type_name diff --git a/app/models/custom_value.rb b/app/models/custom_value.rb index afe4c1afb..1d453baf0 100644 --- a/app/models/custom_value.rb +++ b/app/models/custom_value.rb @@ -19,21 +19,37 @@ 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 + + # Returns true if the boolean custom value is true + def true? + self.value == '1' + end + protected def validate - errors.add(:value, :activerecord_error_blank) and return if custom_field.is_required? and value.blank? - 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.blank? || value =~ /^[+-]?\d+$/ - when 'float' - begin; !value.blank? && 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}$/ or value.empty? - when 'list' - errors.add(:value, :activerecord_error_inclusion) unless custom_field.possible_values.include? value or value.empty? + 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 end end end diff --git a/app/models/document.rb b/app/models/document.rb index 7a432b46b..627a2418f 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -20,11 +20,12 @@ 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_searchable :columns => ['title', "#{table_name}.description"], :include => :project 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}} - + acts_as_activity_provider :find_options => {:include => :project} + validates_presence_of :project, :title, :category validates_length_of :title, :maximum => 60 end diff --git a/app/models/enumeration.rb b/app/models/enumeration.rb index 400681a43..d32a0c049 100644 --- a/app/models/enumeration.rb +++ b/app/models/enumeration.rb @@ -23,12 +23,12 @@ class Enumeration < ActiveRecord::Base validates_presence_of :opt, :name validates_uniqueness_of :name, :scope => [:opt] validates_length_of :name, :maximum => 30 - validates_format_of :name, :with => /^[\w\s\'\-]*$/i + # Single table inheritance would be an option OPTIONS = { - "IPRI" => :enumeration_issue_priorities, - "DCAT" => :enumeration_doc_categories, - "ACTI" => :enumeration_activities + "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id}, + "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id}, + "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id} }.freeze def self.get_values(option) @@ -40,13 +40,32 @@ class Enumeration < ActiveRecord::Base end def option_name - OPTIONS[self.opt] + OPTIONS[self.opt][:label] end def before_save Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default? end + def objects_count + OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}") + end + + def in_use? + self.objects_count != 0 + end + + alias :destroy_without_reassign :destroy + + # Destroy the enumeration + # If a enumeration is specified, objects are reassigned + def destroy(reassign_to = nil) + if reassign_to && reassign_to.is_a?(Enumeration) + OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}") + end + destroy_without_reassign + end + def <=>(enumeration) position <=> enumeration.position end @@ -55,13 +74,6 @@ class Enumeration < ActiveRecord::Base private def check_integrity - case self.opt - when "IPRI" - raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id]) - when "DCAT" - raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id]) - when "ACTI" - raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id]) - end + raise "Can't delete enumeration" if self.in_use? end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 60cca4051..4701e41f1 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -27,24 +27,27 @@ class Issue < ActiveRecord::Base has_many :journals, :as => :journalized, :dependent => :destroy has_many :attachments, :as => :container, :dependent => :destroy - 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" + has_many :time_entries, :dependent => :delete_all + has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all + acts_as_customizable acts_as_watchable - acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue} + acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], + :include => [:project, :journals], + # sort by id so that limited eager loading doesn't break with postgresql + :order_column => "#{table_name}.id" 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, :tracker, :author, :status + acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]} + + validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 validates_numericality_of :estimated_hours, :allow_nil => true - validates_associated :custom_values, :on => :update def after_initialize if new_record? @@ -54,6 +57,11 @@ class Issue < ActiveRecord::Base end end + # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields + def available_custom_fields + (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : [] + end + def copy_from(arg) issue = arg.is_a?(Issue) ? arg : Issue.find(arg) self.attributes = issue.attributes.dup @@ -66,10 +74,14 @@ class Issue < ActiveRecord::Base transaction do if new_project && project_id != new_project.id # delete issue relations - self.relations_from.clear - self.relations_to.clear + unless Setting.cross_project_issue_relations? + self.relations_from.clear + self.relations_to.clear + end # issue is moved to another project - self.category = nil + # reassign to the category with same name if any + new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name) + self.category = new_category self.fixed_version = nil self.project = new_project end @@ -91,7 +103,11 @@ class Issue < ActiveRecord::Base self.priority = nil write_attribute(:priority_id, pid) end - + + def estimated_hours=(h) + write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) + end + def validate if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? errors.add :due_date, :activerecord_error_not_a_date @@ -106,6 +122,10 @@ 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 @@ -138,12 +158,17 @@ class Issue < ActiveRecord::Base 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 @@ -153,16 +178,14 @@ class Issue < ActiveRecord::Base end end - def custom_value_for(custom_field) - self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id } - return nil - end - 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 } + # Make sure updated_on is updated when adding a note. + updated_on_will_change! @current_journal end @@ -176,12 +199,19 @@ class Issue < ActiveRecord::Base 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 - recipients << author.mail if author - recipients << assigned_to.mail if assigned_to + # 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 @@ -202,9 +232,15 @@ class Issue < ActiveRecord::Base dependencies end - # Returns an array of the duplicate issues + # Returns an array of issues that duplicate this one def duplicates - relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)} + relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} + end + + # Returns the due date or the target due date if any + # Used on gantt chart + def due_before + due_date || (fixed_version ? fixed_version.effective_date : nil) end def duration @@ -214,4 +250,14 @@ 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 diff --git a/app/models/issue_category.rb b/app/models/issue_category.rb index 9478504f1..51baeb419 100644 --- a/app/models/issue_category.rb +++ b/app/models/issue_category.rb @@ -35,5 +35,9 @@ class IssueCategory < ActiveRecord::Base destroy_without_reassign end + def <=>(category) + name <=> category.name + end + def to_s; name end end diff --git a/app/models/issue_relation.rb b/app/models/issue_relation.rb index 07e940b85..49329e0bb 100644 --- a/app/models/issue_relation.rb +++ b/app/models/issue_relation.rb @@ -25,7 +25,7 @@ class IssueRelation < ActiveRecord::Base TYPE_PRECEDES = "precedes" TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, - TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 }, + TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, }.freeze diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index a5d228405..ddff9c005 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -56,6 +56,10 @@ class IssueStatus < ActiveRecord::Base false end + def <=>(status) + position <=> status.position + end + def to_s; name end private diff --git a/app/models/journal.rb b/app/models/journal.rb index 64483d21d..71a51290b 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -23,17 +23,20 @@ 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| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') }, + 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, - :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id}} + :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' }, + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}} + acts_as_activity_provider :type => 'issues', + :permission => :view_issues, + :find_options => {:include => [{:issue => :project}, :details, :user], + :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" + + " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"} + def save # Do not save an empty journal (details.empty? && notes.blank?) ? false : super @@ -44,4 +47,21 @@ class Journal < ActiveRecord::Base 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 diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb index 7a1d73244..2f1eba3e9 100644 --- a/app/models/mail_handler.rb +++ b/app/models/mail_handler.rb @@ -16,25 +16,138 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MailHandler < ActionMailer::Base + + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end + + attr_reader :email, :user + + def self.receive(email, options={}) + @@handler_options = options.dup + + @@handler_options[:issue] ||= {} + + @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) + @@handler_options[:allow_override] ||= [] + # Project needs to be overridable if not specified + @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) + # Status needs to be overridable if not specified + @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status) + super email + end # Processes incoming emails - # Currently, it only supports adding a note to an existing issue - # by replying to the initial notification message def receive(email) - # find related issue by parsing the subject - m = email.subject.match %r{\[.*#(\d+)\]} - return unless m - issue = Issue.find_by_id(m[1]) - return unless issue - - # find user - user = User.find_active(:first, :conditions => {:mail => email.from.first}) - return unless user + @email = email + @user = User.find_active(:first, :conditions => {:mail => email.from.first}) + unless @user + # Unknown user => the email is ignored + # TODO: ability to create the user's account + logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info + return false + end + User.current = @user + dispatch + end + + private + + ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]} + + def dispatch + if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE) + receive_issue_update(m[1].to_i) + else + receive_issue + end + rescue ActiveRecord::RecordInvalid => e + # TODO: send a email to the user + logger.error e.message if logger + false + rescue MissingInformation => e + logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "MailHandler: unauthorized attempt from #{user}" if logger + false + end + + # Creates a new issue + def receive_issue + project = target_project + tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) + category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) + priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority))) + status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) || IssueStatus.default + # check permission - return unless user.allowed_to?(:add_issue_notes, issue.project) + raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) + issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :status => status) + issue.subject = email.subject.chomp + issue.description = email.plain_text_body.chomp + issue.save! + add_attachments(issue) + logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info + Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added') + issue + end + + def target_project + # TODO: other ways to specify project: + # * parse the email To field + # * specific project (eg. Setting.mail_handler_target_project) + target = Project.find_by_identifier(get_keyword(:project)) + raise MissingInformation.new('Unable to determine target project') if target.nil? + target + end + + # Adds a note to an existing issue + def receive_issue_update(issue_id) + status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) + issue = Issue.find_by_id(issue_id) + return unless issue + # check permission + raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project) + raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project) + # add the note - issue.init_journal(user, email.body.chomp) - issue.save + journal = issue.init_journal(user, email.plain_text_body.chomp) + add_attachments(issue) + issue.status = status unless status.nil? + issue.save! + logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info + Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + journal + end + + def add_attachments(obj) + if email.has_attachments? + email.attachments.each do |attachment| + Attachment.create(:container => obj, + :file => attachment, + :author => user, + :content_type => attachment.content_type) + end + end + end + + def get_keyword(attr) + if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i + $1.strip + elsif !@@handler_options[:issue][attr].blank? + @@handler_options[:issue][attr] + end end end + +class TMail::Mail + # Returns body of the first plain text part found if any + def plain_text_body + return @plain_text_body unless @plain_text_body.nil? + p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten + plain = p.detect {|c| c.content_type == 'text/plain'} + @plain_text_body = plain.nil? ? self.body : plain.body + end +end + diff --git a/app/models/mailer.rb b/app/models/mailer.rb index fe432e9a6..61e5d596c 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.rb @@ -16,31 +16,52 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Mailer < ActionMailer::Base - helper ApplicationHelper - helper IssuesHelper - helper CustomFieldsHelper + helper :application + helper :issues + helper :custom_fields 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}" + 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) - subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] #{issue.status.name} - #{issue.subject}" + 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 reminder(user, issues, days) + set_language_if_valid user.language + recipients user.mail + subject l(:mail_subject_reminder, issues.size) + body :issues => issues, + :days => days, + :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc') + 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, @@ -59,6 +80,7 @@ class Mailer < ActionMailer::Base 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, @@ -67,6 +89,7 @@ class Mailer < ActionMailer::Base 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, @@ -74,6 +97,8 @@ class Mailer < ActionMailer::Base 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, @@ -83,7 +108,7 @@ class Mailer < ActionMailer::Base def account_information(user, password) set_language_if_valid user.language recipients user.mail - subject l(:mail_subject_register) + subject l(:mail_subject_register, Setting.app_title) body :user => user, :password => password, :login_url => url_for(:controller => 'account', :action => 'login') @@ -92,7 +117,7 @@ class Mailer < ActionMailer::Base 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) + subject l(:mail_subject_account_activation_request, Setting.app_title) body :user => user, :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') end @@ -100,7 +125,7 @@ class Mailer < ActionMailer::Base def lost_password(token) set_language_if_valid(token.user.language) recipients token.user.mail - subject l(:mail_subject_lost_password) + subject l(:mail_subject_lost_password, Setting.app_title) body :token => token, :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) end @@ -108,7 +133,7 @@ class Mailer < ActionMailer::Base def register(token) set_language_if_valid(token.user.language) recipients token.user.mail - subject l(:mail_subject_register) + subject l(:mail_subject_register, Setting.app_title) body :token => token, :url => url_for(:controller => 'account', :action => 'activate', :token => token.value) end @@ -119,7 +144,40 @@ class Mailer < ActionMailer::Base subject 'Redmine test' body :url => url_for(:controller => 'welcome') 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 + # Sends reminders to issue assignees + # Available options: + # * :days => how many days in the future to remind about (defaults to 7) + # * :tracker => id of tracker for filtering issues (defaults to all trackers) + # * :project => id or identifier of project to process (defaults to all projects) + def self.reminders(options={}) + days = options[:days] || 7 + project = options[:project] ? Project.find(options[:project]) : nil + tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil + + s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] + s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" + s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" + s << "#{Issue.table_name}.project_id = #{project.id}" if project + s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker + + issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker], + :conditions => s.conditions + ).group_by(&:assigned_to) + issues_by_assignee.each do |assignee, issues| + deliver_reminder(assignee, issues, days) unless assignee.nil? + end + end + private def initialize_defaults(method_name) super @@ -127,13 +185,31 @@ class Mailer < ActionMailer::Base 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 - # Overrides the create_mail method to remove the current user from the recipients and cc - # if he doesn't want to receive notifications about what he does + # 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 - recipients.delete(User.current.mail) if recipients && User.current.pref[:no_self_notified] - cc.delete(User.current.mail) if cc && User.current.pref[:no_self_notified] + # 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 @@ -141,6 +217,11 @@ class Mailer < ActionMailer::Base 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(File.join(template_root, 'mailer'), body, self).render(:file => layout) + 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 diff --git a/app/models/member.rb b/app/models/member.rb index 39703147d..b4617c229 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -31,8 +31,12 @@ class Member < ActiveRecord::Base 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 - project.issue_categories.update_all "assigned_to_id = NULL", ["assigned_to_id = ?", self.user.id] + IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] end end diff --git a/app/models/message.rb b/app/models/message.rb index 909c06a9e..80df7a33a 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -23,16 +23,26 @@ class Message < ActiveRecord::Base belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' acts_as_searchable :columns => ['subject', 'content'], - :include => :board, + :include => {:board, :project}, :project_key => 'project_id', - :date_column => 'created_on' + :date_column => "#{table_name}.created_on" acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, :description => :content, - :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}} - + :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, + :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : + {:id => o.parent_id, :anchor => "message-#{o.id}"})} + + acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} + + 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 @@ -43,6 +53,18 @@ 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 diff --git a/app/models/message_observer.rb b/app/models/message_observer.rb index 1c311e25f..043988172 100644 --- a/app/models/message_observer.rb +++ b/app/models/message_observer.rb @@ -18,9 +18,11 @@ 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} + recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?} # send notification to the board watchers recipients += message.board.watcher_recipients + # send notification to project members who want to be notified + recipients += message.board.project.recipients recipients = recipients.compact.uniq Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted') end diff --git a/app/models/news.rb b/app/models/news.rb index 3d8c4d661..4c4943b78 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -24,9 +24,10 @@ class News < ActiveRecord::Base validates_length_of :title, :maximum => 60 validates_length_of :summary, :maximum => 255 - acts_as_searchable :columns => ['title', 'description'] + acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}} - + acts_as_activity_provider :find_options => {:include => [:project, :author]} + # 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") diff --git a/app/models/project.rb b/app/models/project.rb index 1fbab2e4d..adc70c644 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -20,10 +20,10 @@ class Project < ActiveRecord::Base STATUS_ACTIVE = 1 STATUS_ARCHIVED = 9 - has_many :members, :dependent => :delete_all, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :members, :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" @@ -32,12 +32,12 @@ class Project < ActiveRecord::Base has_many :documents, :dependent => :destroy has_many :news, :dependent => :delete_all, :include => :author has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" - has_many :boards, :order => "position ASC" + has_many :boards, :dependent => :destroy, :order => "position ASC" has_one :repository, :dependent => :destroy has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy # Custom field for the project issues - has_and_belongs_to_many :custom_fields, + has_and_belongs_to_many :issue_custom_fields, :class_name => 'IssueCustomField', :order => "#{CustomField.table_name}.position", :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", @@ -45,23 +45,24 @@ class Project < ActiveRecord::Base acts_as_tree :order => "name", :counter_cache => true - acts_as_searchable :columns => ['name', 'description'], :project_key => 'id' + acts_as_customizable + acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"}, - :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} + :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}, + :author => nil attr_protected :status, :enabled_module_names - validates_presence_of :name, :description, :identifier + validates_presence_of :name, :identifier validates_uniqueness_of :name, :identifier - validates_associated :custom_values, :on => :update validates_associated :repository, :wiki validates_length_of :name, :maximum => 30 - validates_format_of :name, :with => /^[\w\s\'\-]*$/i - validates_length_of :description, :maximum => 255 - validates_length_of :homepage, :maximum => 60 - validates_length_of :identifier, :in => 3..12 + validates_length_of :homepage, :maximum => 255 + validates_length_of :identifier, :in => 3..20 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ + before_destroy :delete_all_members + def identifier=(identifier) super unless identifier_frozen? end @@ -72,26 +73,19 @@ class Project < ActiveRecord::Base def issues_with_subprojects(include_subprojects=false) conditions = nil - if include_subprojects && !active_children.empty? - ids = [id] + active_children.collect {|c| c.id} - conditions = ["#{Issue.table_name}.project_id IN (#{ids.join(',')})"] + if include_subprojects + ids = [id] + child_ids + conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] end - conditions ||= ["#{Issue.table_name}.project_id = ?", id] - Issue.with_scope :find => { :conditions => conditions } do - yield + 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 end end - # Return all issues status changes for the project between the 2 given dates - def issues_status_changes(from, to) - Journal.find(:all, :include => [:issue, :details, :user], - :conditions => ["#{Journal.table_name}.journalized_type = 'Issue'" + - " AND #{Issue.table_name}.project_id = ?" + - " AND #{JournalDetail.table_name}.prop_key = 'status_id'" + - " AND #{Journal.table_name}.created_on BETWEEN ? AND ?", - id, from, to+1]) - 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) @@ -99,6 +93,7 @@ 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}" elsif user && user.memberships.any? @@ -108,6 +103,53 @@ class Project < ActiveRecord::Base 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 + else + statements << "1=0" + if 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 + end + 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 @@ -129,9 +171,23 @@ 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} + 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 @@ -141,16 +197,25 @@ class Project < ActiveRecord::Base # Returns an array of all custom fields enabled for project issues # (explictly associated custom fields and custom fields enabled for all projects) - def custom_fields_for_issues(tracker) - all_custom_fields.select {|c| tracker.custom_fields.include? c } + def all_issue_custom_fields + @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort end - def all_custom_fields - @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq + def project + self end def <=>(project) - name <=> project.name + 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) @@ -173,11 +238,18 @@ class Project < ActiveRecord::Base enabled_modules << EnabledModule.new(:name => name.to_s) end end + + # Returns an auto-generated project identifier based on the last identifier used + def self.next_identifier + p = Project.find(:first, :order => 'created_on DESC') + p.nil? ? nil : p.identifier.to_s.succ + end protected def validate 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 diff --git a/app/models/query.rb b/app/models/query.rb index 30df55b96..0ce9a6a21 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -16,12 +16,13 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class QueryColumn - attr_accessor :name, :sortable + 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 @@ -53,8 +54,7 @@ class Query < ActiveRecord::Base serialize :filters serialize :column_names - attr_protected :project, :user - attr_accessor :executed_by + attr_protected :project_id, :user_id validates_presence_of :name, :on => :save validates_length_of :name, :maximum => 255 @@ -83,47 +83,49 @@ class Query < ActiveRecord::Base @@operators_by_filter_type = { :list => [ "=", "!" ], :list_status => [ "o", "=", "!", "c", "*" ], :list_optional => [ "=", "!", "!*", "*" ], - :list_one_or_more => [ "*", "=" ], + :list_subprojects => [ "*", "!*", "=" ], :date => [ "t+", "t+", "t", "w", ">t-", " [ ">t-", " [ "=", "~", "!", "!~" ], :text => [ "~", "!~" ], - :integer => [ "=", ">=", "<=" ] } + :integer => [ "=", ">=", "<=", "!*", "*" ] } 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"), - QueryColumn.new(:subject), + 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"), + 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), + 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"), + 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 executed_by=(user) - @executed_by = user - set_language_if_valid(user.language) if user + def after_initialize + # Store the fact that project is nil (used in #editable_by?) + @is_for_all = project.nil? end def validate filters.each_key do |field| errors.add label_for(field), :activerecord_error_blank unless # filter requires one or more values - (values_for(field) and !values_for(field).first.empty?) or + (values_for(field) and !values_for(field).first.blank?) or # filter doesn't require any value ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) end if filters @@ -131,57 +133,54 @@ class Query < ActiveRecord::Base def editable_by?(user) return false unless user - return true if !is_public && self.user_id == user.id - is_public && user.allowed_to?(:manage_public_queries, project) + # Admin can edit them all and regular users can edit their private queries + return true if user.admin? || (!is_public && self.user_id == user.id) + # Members can not edit public queries that are for all project (only admin is allowed to) + is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) end def available_filters 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 => 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] } }, + "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] } }, "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 }} + "estimated_hours" => { :type => :integer, :order => 13 }, + "done_ratio" => { :type => :integer, :order => 14 }} user_values = [] - user_values << ["<< #{l(:label_me)} >>", "me"] if executed_by + user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? if project - user_values += project.users.collect{|s| [s.name, s.id.to_s] } - elsif executed_by + user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } + else # members of the user's projects - user_values += executed_by.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] } + 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 - @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] } } + # project specific filters + unless @project.issue_categories.empty? + @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } } + end + unless @project.versions.empty? + @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } + end unless @project.active_children.empty? - @available_filters["subproject_id"] = { :type => :list_one_or_more, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } + @available_filters["subproject_id"] = { :type => :list_subprojects, :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" - options = { :type => :list_optional, :values => field.possible_values, :order => 20} - when "date" - options = { :type => :date, :order => 20 } - when "bool" - options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } - end - @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name }) - end - # remove category filter if no category defined - @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty? + add_custom_fields_filters(@project.all_issue_custom_fields) + else + # global filters for cross project issue list + add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) end @available_filters end @@ -220,7 +219,7 @@ 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 @@ -228,7 +227,7 @@ class Query < ActiveRecord::Base return @available_columns if @available_columns @available_columns = Query.available_columns @available_columns += (project ? - project.custom_fields : + project.all_issue_custom_fields : IssueCustomField.find(:all, :conditions => {:is_for_all => true}) ).collect {|cf| QueryCustomFieldColumn.new(cf) } end @@ -258,20 +257,28 @@ class Query < ActiveRecord::Base def statement # project/subprojects clause - clause = '' - if project && 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} + 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 end - clause << "#{Issue.table_name}.project_id IN (%d,%s)" % [project.id, subproject_ids.join(",")] if project + project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',') elsif project - clause << "#{Issue.table_name}.project_id=%d" % project.id - else - clause << Project.visible_by(executed_by) + project_clauses << "#{Issue.table_name}.project_id = %d" % project.id end + project_clauses << Project.visible_by(User.current) # filters clauses filters_clauses = [] @@ -280,12 +287,14 @@ class Query < ActiveRecord::Base v = values_for(field).clone next unless v and !v.empty? - sql = '' + sql = '' + is_custom_filter = false if field =~ /^cf_(\d+)$/ # custom field db_table = CustomValue.table_name db_field = 'value' - sql << "#{Issue.table_name}.id IN (SELECT #{db_table}.customized_id FROM #{db_table} where #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} AND " + 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 " else # regular field db_table = Issue.table_name @@ -295,18 +304,20 @@ class Query < ActiveRecord::Base # "me" value subsitution if %w(assigned_to_id author_id).include?(field) - v.push(executed_by ? executed_by.id.to_s : "0") if v.delete("me") + v.push(User.current.logged? ? User.current.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} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" + sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" when "!*" sql = sql + "#{db_table}.#{db_field} IS NULL" + sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter when "*" sql = sql + "#{db_table}.#{db_field} IS NOT NULL" + sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter when ">=" sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}" when "<=" @@ -330,7 +341,12 @@ class Query < ActiveRecord::Base 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" - sql = sql + "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(Time.now.at_beginning_of_week), connection.quoted_date(Time.now.next_week.yesterday)] + 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 "!~" @@ -340,8 +356,28 @@ class Query < ActiveRecord::Base filters_clauses << sql end if filters and valid? - clause << ' AND ' unless clause.empty? - clause << filters_clauses.join(' AND ') unless filters_clauses.empty? - clause + (project_clauses + filters_clauses).join(' AND ') + end + + private + + def add_custom_fields_filters(custom_fields) + @available_filters ||= {} + + custom_fields.select(&:is_filter?).each do |field| + case field.field_format + when "text" + options = { :type => :text, :order => 20 } + when "list" + options = { :type => :list_optional, :values => field.possible_values, :order => 20} + when "date" + 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 end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 35dd6803f..81e6647a2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -17,9 +17,26 @@ class Repository < ActiveRecord::Base belongs_to :project - has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC" + has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" has_many :changes, :through => :changesets - + + # Raw SQL to delete changesets and changes in the database + # has_many :changesets, :dependent => :destroy is too slow for big repositories + before_destroy :clear_changesets + + # Checks if the SCM is enabled when creating a repository + validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) } + + # 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? @@ -33,13 +50,29 @@ class Repository < ActiveRecord::Base def supports_cat? scm.supports_cat? end + + def supports_annotate? + scm.supports_annotate? + end + + def entry(path=nil, identifier=nil) + scm.entry(path, identifier) + end def entries(path=nil, identifier=nil) scm.entries(path, identifier) end - def diff(path, rev, rev_to, type) - scm.diff(path, rev, rev_to, type) + def properties(path, identifier=nil) + scm.properties(path, identifier) + end + + def cat(path, identifier=nil) + scm.cat(path, identifier) + end + + def diff(path, rev, rev_to) + scm.diff(path, rev, rev_to) end # Default behaviour: we search in cached changesets @@ -47,7 +80,12 @@ class Repository < ActiveRecord::Base 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}.revision DESC").collect(&:changeset) + :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 @@ -84,4 +122,19 @@ class Repository < ActiveRecord::Base rescue nil end + + private + + def before_save + # Strips url and root_url + url.strip! + root_url.strip! + true + end + + def clear_changesets + connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})") + connection.delete("DELETE FROM changesets_issues WHERE changesets_issues.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})") + connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}") + end end diff --git a/app/models/repository/bazaar.rb b/app/models/repository/bazaar.rb new file mode 100644 index 000000000..ec953bd45 --- /dev/null +++ b/app/models/repository/bazaar.rb @@ -0,0 +1,91 @@ +# 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? + # Set the filesize unless browsing a specific revision + if identifier.nil? && e.is_file? + full_path = File.join(root_url, e.path) + e.size = File.stat(full_path).size if File.file?(full_path) + end + 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 diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb index 16d906316..82082b3d6 100644 --- a/app/models/repository/cvs.rb +++ b/app/models/repository/cvs.rb @@ -29,13 +29,14 @@ class Repository::Cvs < Repository 'CVS' end - def entry(path, identifier) - e = entries(path, identifier) - e ? e.first : nil + def entry(path=nil, identifier=nil) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.entry(path, rev.nil? ? nil : rev.committed_on) end def entries(path=nil, identifier=nil) - entries=scm.entries(path, identifier) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + entries = scm.entries(path, rev.nil? ? nil : rev.committed_on) if entries entries.each() do |entry| unless entry.lastrev.nil? || entry.lastrev.identifier @@ -52,7 +53,12 @@ class Repository::Cvs < Repository entries end - def diff(path, rev, rev_to, type) + def cat(path, identifier=nil) + rev = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.cat(path, rev.nil? ? nil : rev.committed_on) + end + + def diff(path, rev, rev_to) #convert rev to revision. CVS can't handle changesets here diff=[] changeset_from=changesets.find_by_revision(rev) @@ -75,16 +81,14 @@ class Repository::Cvs < Repository unless revision_to revision_to=scm.get_previous_revision(revision_from) end - diff=diff+scm.diff(change_from.path, revision_from, revision_to, type) + file_diff = scm.diff(change_from.path, revision_from, revision_to) + diff = diff + file_diff unless file_diff.nil? 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. @@ -94,8 +98,10 @@ 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 - scm.revisions('', last_commit, nil, :with_paths => true) do |revision| + tmp_rev_num = 1 + scm.revisions('', fetch_since, 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]) @@ -103,22 +109,20 @@ class Repository::Cvs < Repository cs = changesets.find(:first, :conditions=>{ :committed_on=>revision.time-time_delta..revision.time+time_delta, :committer=>revision.author, - :comments=>revision.message + :comments=>Changeset.normalize_comments(revision.message) }) # create a new changeset.... - unless cs - # we use a negative changeset-number here (just for inserting) + unless cs + # we use a temporaray revision number here (just for inserting) # later on, we calculate a continous positive number - 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) + 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 end #convert CVS-File-States to internal Action-abbrevations @@ -139,12 +143,19 @@ class Repository::Cvs < Repository end end - 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! + # 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 end - end + end # transaction + end + + private + + # Returns the next revision number to assign to a CVS changeset + def next_revision_number + # Need to retrieve existing revision numbers to sort them as integers + @current_revision_number ||= (connection.select_values("SELECT revision FROM #{Changeset.table_name} WHERE repository_id = #{id} AND revision NOT LIKE '_%'").collect(&:to_i).max || 0) + @current_revision_number += 1 end end diff --git a/app/models/repository/darcs.rb b/app/models/repository/darcs.rb index 48cc246fb..855a403fc 100644 --- a/app/models/repository/darcs.rb +++ b/app/models/repository/darcs.rb @@ -28,8 +28,14 @@ class Repository::Darcs < Repository 'Darcs' end + def entry(path=nil, identifier=nil) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.entry(path, patch.nil? ? nil : patch.scmid) + end + def entries(path=nil, identifier=nil) - entries=scm.entries(path, identifier) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + entries = scm.entries(path, patch.nil? ? nil : patch.scmid) if entries entries.each do |entry| # Search the DB for the entry's last change @@ -45,20 +51,26 @@ class Repository::Darcs < Repository entries end - def diff(path, rev, rev_to, type) + def cat(path, identifier=nil) + patch = identifier.nil? ? nil : changesets.find_by_revision(identifier) + scm.cat(path, patch.nil? ? nil : patch.scmid) + end + + def diff(path, rev, rev_to) patch_from = changesets.find_by_revision(rev) + return nil if patch_from.nil? patch_to = changesets.find_by_revision(rev_to) if rev_to if path.blank? path = patch_from.changes.collect{|change| change.path}.join(' ') end - scm.diff(path, patch_from.scmid, patch_to.scmid, type) + patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil 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 + 1 : 1 + next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1 # latest revision in the repository scm_revision = scm_info.lastrev.scmid unless changesets.find_by_scmid(scm_revision) @@ -71,9 +83,7 @@ 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], diff --git a/app/sweepers/version_sweeper.rb b/app/models/repository/filesystem.rb similarity index 65% rename from app/sweepers/version_sweeper.rb rename to app/models/repository/filesystem.rb index e1323e261..da096cc09 100644 --- a/app/sweepers/version_sweeper.rb +++ b/app/models/repository/filesystem.rb @@ -1,6 +1,9 @@ # redMine - project management software # Copyright (C) 2006-2007 Jean-Philippe Lang # +# FileSystem adapter +# File written by Paul Rivier, at Demotera. +# # 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 @@ -15,20 +18,26 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -class VersionSweeper < ActionController::Caching::Sweeper - observe Version +require 'redmine/scm/adapters/filesystem_adapter' - def after_save(version) - expire_cache_for(version) +class Repository::Filesystem < Repository + attr_protected :root_url + validates_presence_of :url + + def scm_adapter + Redmine::Scm::Adapters::FilesystemAdapter end - def after_destroy(version) - expire_cache_for(version) + def self.scm_name + 'Filesystem' end - -private - def expire_cache_for(version) - # calendar and gantt fragments of the project - expire_fragment(Regexp.new("projects/(calendar|gantt)/#{version.project_id}\\.")) + + def entries(path=nil, identifier=nil) + scm.entries(path, identifier) end + + def fetch_changesets + nil + end + end diff --git a/app/models/repository/git.rb b/app/models/repository/git.rb new file mode 100644 index 000000000..2f440fe29 --- /dev/null +++ b/app/models/repository/git.rb @@ -0,0 +1,68 @@ +# 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) + scm.revisions('', db_revision, nil, :reverse => true) do |revision| + transaction do + changeset = Changeset.create(:repository => self, + :revision => revision.identifier, + :scmid => revision.scmid, + :committer => revision.author, + :committed_on => revision.time, + :comments => revision.message) + + revision.paths.each do |change| + Change.create(:changeset => changeset, + :action => change[:action], + :path => change[:path], + :from_path => change[:from_path], + :from_revision => change[:from_revision]) + end + end + end + end + end + end +end diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb index 5d9ea9cd4..18cbc9495 100644 --- a/app/models/repository/mercurial.rb +++ b/app/models/repository/mercurial.rb @@ -34,6 +34,11 @@ class Repository::Mercurial < Repository if entries entries.each do |entry| next unless entry.is_file? + # Set the filesize unless browsing a specific revision + if identifier.nil? + full_path = File.join(root_url, entry.path) + entry.size = File.stat(full_path).size if File.file?(full_path) + end # Search the DB for the entry's last change change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC") if change @@ -51,29 +56,37 @@ class Repository::Mercurial < Repository scm_info = scm.info if scm_info # latest revision found in database - db_revision = latest_changeset ? latest_changeset.revision : nil + db_revision = latest_changeset ? latest_changeset.revision.to_i : -1 # latest revision in the repository - 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]) + 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 end - end + end unless revisions.nil? + identifier_from = identifier_to + 1 end end end diff --git a/app/models/repository/subversion.rb b/app/models/repository/subversion.rb index a0485608d..3981d6f4c 100644 --- a/app/models/repository/subversion.rb +++ b/app/models/repository/subversion.rb @@ -35,11 +35,16 @@ class Repository::Subversion < Repository 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 : 0 + 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 @@ -71,4 +76,14 @@ 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 diff --git a/app/models/role.rb b/app/models/role.rb index 015146dc4..6f1fb4768 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -21,7 +21,18 @@ class Role < ActiveRecord::Base BUILTIN_ANONYMOUS = 2 before_destroy :check_deletable - has_many :workflows, :dependent => :delete_all + 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 + has_many :members acts_as_list diff --git a/app/models/setting.rb b/app/models/setting.rb index 4d4cf0045..072afa0db 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -33,6 +33,45 @@ class Setting < ActiveRecord::Base '%H:%M', '%I:%M %p' ] + + ENCODINGS = %w(US-ASCII + windows-1250 + windows-1251 + windows-1252 + windows-1253 + windows-1254 + windows-1255 + windows-1256 + windows-1257 + windows-1258 + windows-31j + ISO-2022-JP + ISO-2022-KR + ISO-8859-1 + ISO-8859-2 + ISO-8859-3 + ISO-8859-4 + ISO-8859-5 + ISO-8859-6 + ISO-8859-7 + ISO-8859-8 + ISO-8859-9 + ISO-8859-13 + ISO-8859-15 + KOI8-R + UTF-8 + UTF-16 + UTF-16BE + UTF-16LE + EUC-JP + Shift_JIS + GB18030 + GBK + ISCII91 + EUC-KR + Big5 + Big5-HKSCS + TIS-620) cattr_accessor :available_settings @@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) @@ -53,12 +92,13 @@ class Setting < ActiveRecord::Base 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 end def value=(v) v = v.to_yaml if v && @@available_settings[name]['serialized'] - write_attribute(:value, v) + write_attribute(:value, v.to_s) end # Returns the value of the setting named name @@ -95,6 +135,11 @@ class Setting < ActiveRecord::Base 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 diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index 905857073..57a75604d 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -1,5 +1,5 @@ # redMine - project management software -# Copyright (C) 2006-2007 Jean-Philippe Lang +# Copyright (C) 2006-2008 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -24,11 +24,25 @@ class TimeEntry < ActiveRecord::Base belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek + + acts_as_customizable + acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"}, + :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}}, + :author => :user, + :description => :comments validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on validates_numericality_of :hours, :allow_nil => true - validates_length_of :comments, :maximum => 255 + validates_length_of :comments, :maximum => 255, :allow_nil => true + 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 @@ -39,6 +53,10 @@ class TimeEntry < ActiveRecord::Base errors.add :issue_id, :activerecord_error_invalid if (issue_id && !issue) || (issue && project!=issue.project) end + def hours=(h) + write_attribute :hours, (h.is_a?(String) ? h.to_hours : h) + end + # tyear, tmonth, tweek assigned where setting spent_on attributes # these attributes make time aggregations easier def spent_on=(date) @@ -46,5 +64,16 @@ 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 + 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 diff --git a/app/models/time_entry_custom_field.rb b/app/models/time_entry_custom_field.rb new file mode 100644 index 000000000..2ec3d27be --- /dev/null +++ b/app/models/time_entry_custom_field.rb @@ -0,0 +1,23 @@ +# redMine - project management software +# Copyright (C) 2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class TimeEntryCustomField < CustomField + def type_name + :label_spent_time + end +end + diff --git a/app/models/tracker.rb b/app/models/tracker.rb index 90ef31912..ecee908eb 100644 --- a/app/models/tracker.rb +++ b/app/models/tracker.rb @@ -18,7 +18,19 @@ class Tracker < ActiveRecord::Base before_destroy :check_integrity has_many :issues - has_many :workflows, :dependent => :delete_all + 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_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 @@ -29,6 +41,14 @@ class Tracker < ActiveRecord::Base 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]) diff --git a/app/models/user.rb b/app/models/user.rb index 9c8d1d9a3..4f82f61b2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,37 +18,48 @@ require "digest/sha1" class User < ActiveRecord::Base + # 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 :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name" + has_many :members, :dependent => :delete_all has_many :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'" belongs_to :auth_source + acts_as_customizable + attr_accessor :password, :password_confirmation attr_accessor :last_before_login_on # Prevents unauthorized assignments attr_protected :login, :admin, :password, :password_confirmation, :hashed_password - validates_presence_of :login, :firstname, :lastname, :mail - validates_uniqueness_of :login, :mail + 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? } # 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_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 - validates_length_of :mail, :maximum => 60 - # Password length between 4 and 12 - validates_length_of :password, :in => 4..12, :allow_nil => true + 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_confirmation_of :password, :allow_nil => true - validates_associated :custom_values, :on => :update def before_create self.mail_notification = false @@ -74,6 +85,8 @@ 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 @@ -89,25 +102,25 @@ class User < ActiveRecord::Base # user is not yet registered, try to authenticate with available sources attrs = AuthSource.authenticate(login, password) if attrs - onthefly = new(*attrs) - onthefly.login = login - onthefly.language = Setting.default_language - if onthefly.save - user = find(:first, :conditions => ["login=?", login]) - logger.info("User '#{user.login}' created on the fly.") if logger + user = new(*attrs) + user.login = login + user.language = Setting.default_language + if user.save + user.reload + logger.info("User '#{user.login}' created from the LDAP") if logger end end end - user.update_attribute(:last_login_on, Time.now) if user + user.update_attribute(:last_login_on, Time.now) if user && !user.new_record? user - - rescue => text - raise text + rescue => text + raise text end # Return user's full name for display - def name - "#{firstname} #{lastname}" + def name(formatter = nil) + f = USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname] + eval '"' + f + '"' end def active? @@ -130,6 +143,14 @@ class User < ActiveRecord::Base 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') @@ -159,7 +180,13 @@ class User < ActiveRecord::Base end def <=>(user) - lastname == user.lastname ? firstname <=> user.firstname : lastname <=> user.lastname + 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 @@ -170,18 +197,24 @@ class User < ActiveRecord::Base true end + def anonymous? + !logged? + end + # Return user's role for project def role_for_project(project) # No role on archived projects return nil unless project && project.active? - # Find project membership - membership = memberships.detect {|m| m.project_id == project.id} - if membership - membership.role - elsif logged? - Role.non_member + 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 ||= Role.anonymous end end @@ -194,17 +227,26 @@ class User < ActiveRecord::Base # action can be: # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') # * a permission Symbol (eg. :edit_project) - def allowed_to?(action, project) - # No action allowed on archived projects - return false unless project.active? - # No action allowed on disabled modules - return false unless project.allows_to?(action) - # Admin users are authorized for anything else - return true if admin? - - role = role_for_project(project) - return false unless role - role.allowed_to?(action) && (project.is_public? || role.member?) + def allowed_to?(action, project, options={}) + if project + # No action allowed on archived projects + return false unless project.active? + # No action allowed on disabled modules + return false unless project.allows_to?(action) + # Admin users are authorized for anything else + return true if admin? + + role = role_for_project(project) + return false unless role + role.allowed_to?(action) && (project.is_public? || role.member?) + + elsif options[:global] + # authorize if user has at least one role that has this permission + roles = memberships.collect {|m| m.role}.uniq + roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action)) + else + false + end end def self.current=(user) @@ -212,11 +254,16 @@ class User < ActiveRecord::Base end def self.current - @current_user ||= AnonymousUser.new + @current_user ||= User.anonymous end def self.anonymous - AnonymousUser.new + 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 end private @@ -227,12 +274,21 @@ private end class AnonymousUser < User - def logged? - false + + 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 - # Anonymous user has no RSS key - def rss_key - nil + def available_custom_fields + [] 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 diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 1ed9e0fd9..3daa7a740 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -42,8 +42,13 @@ class UserPreference < ActiveRecord::Base if attribute_present? attr_name super else - self.others ||= {} - self.others.store attr_name, value + h = read_attribute(:others).dup || {} + h.update(attr_name => value) + write_attribute(:others, h) + value end end + + def comments_sorting; self[:comments_sorting] end + def comments_sorting=(order); self[:comments_sorting]=order end end diff --git a/app/models/version.rb b/app/models/version.rb index 0547dbabc..dc618af0f 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -23,7 +23,7 @@ class Version < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name, :scope => [:project_id] - validates_length_of :name, :maximum => 30 + 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,6 +34,16 @@ 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) @@ -49,6 +59,14 @@ class Version < ActiveRecord::Base 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) @@ -71,11 +89,11 @@ class Version < ActiveRecord::Base def to_s; name end - # Versions are sorted by effective_date + # Versions are sorted by effective_date and name # 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) : -1 + version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1 else version.effective_date ? 1 : (self.name <=> version.name) end diff --git a/app/models/watcher.rb b/app/models/watcher.rb index cb6ff52ea..38110c584 100644 --- a/app/models/watcher.rb +++ b/app/models/watcher.rb @@ -19,5 +19,12 @@ class Watcher < ActiveRecord::Base belongs_to :watchable, :polymorphic => true belongs_to :user + validates_presence_of :user validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id] + + protected + + def validate + errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active? + end end diff --git a/app/models/wiki.rb b/app/models/wiki.rb index b6d6a9b50..3432a2bc7 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -17,7 +17,7 @@ class Wiki < ActiveRecord::Base belongs_to :project - has_many :pages, :class_name => 'WikiPage', :dependent => :destroy + has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title' has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all validates_presence_of :start_page diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index 4b60a4373..f2ee39c4d 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -25,15 +25,27 @@ 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}} + acts_as_activity_provider :type => 'wiki_pages', + :timestamp => "#{WikiContent.versioned_table_name}.updated_on", + :permission => :view_wiki_pages, + :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " + + "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " + + "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " + + "#{WikiContent.versioned_table_name}.id", + :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"} + def text=(plain) case Setting.wiki_compression when 'gzip' @@ -60,6 +72,18 @@ 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 diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index cbca4fd68..2416fab74 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -16,19 +16,21 @@ # 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_tree :order => 'title' + 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], + :include => [{:wiki => :project}, :content], :project_key => "#{Wiki.table_name}.project_id" attr_accessor :redirect_existing_links @@ -87,6 +89,12 @@ 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 @@ -98,6 +106,29 @@ class WikiPage < ActiveRecord::Base def text content.text if content end + + # Returns true if usr is allowed to edit the page, otherwise false + def editable_by?(usr) + !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) + end + + def parent_title + @parent_title || (self.parent && self.parent.pretty_title) + end + + def parent_title=(t) + @parent_title = t + parent_page = t.blank? ? nil : self.wiki.find_page(t) + self.parent = parent_page + end + + protected + + def validate + errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil? + errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self)) + errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id) + end end class WikiDiff @@ -113,3 +144,41 @@ 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 diff --git a/app/views/account/login.rhtml b/app/views/account/login.rhtml index 5bfbfb8d6..d8c1f313f 100644 --- a/app/views/account/login.rhtml +++ b/app/views/account/login.rhtml @@ -1,9 +1,10 @@
<% form_tag({:action=> "login"}) do %> +<%= back_url_hidden_field_tag %> - - + + @@ -28,6 +29,6 @@

<%= text_field_tag 'login', nil, :size => 40 %>

<%= text_field_tag 'username', nil, :size => 40 %>

-<%= javascript_tag "Form.Element.focus('login');" %> +<%= javascript_tag "Form.Element.focus('username');" %> <% end %>
diff --git a/app/views/account/password_recovery.rhtml b/app/views/account/password_recovery.rhtml index 439ab11b5..7fdd2b2fd 100644 --- a/app/views/account/password_recovery.rhtml +++ b/app/views/account/password_recovery.rhtml @@ -6,7 +6,7 @@

<%= password_field_tag 'new_password', nil, :size => 25 %>
-<%= l(:text_length_between, 4, 12) %>

+<%= l(:text_caracters_minimum, 4) %>

<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>

diff --git a/app/views/account/register.rhtml b/app/views/account/register.rhtml index f04bfbb0e..755a7ad4b 100644 --- a/app/views/account/register.rhtml +++ b/app/views/account/register.rhtml @@ -5,14 +5,17 @@
+<% if @user.auth_source_id.nil? %>

-<%= text_field 'user', 'login', :size => 25 %>

+<%= text_field 'user', 'login', :size => 25 %>

-<%= password_field_tag 'password', nil, :size => 25 %>

+<%= password_field_tag 'password', nil, :size => 25 %>
+<%= l(:text_caracters_minimum, 4) %>

<%= password_field_tag 'password_confirmation', nil, :size => 25 %>

+<% end %>

<%= text_field 'user', 'firstname' %>

@@ -26,18 +29,11 @@

<%= select("user", "language", lang_options_for_select) %>

-<% for @custom_value in @custom_values %> -

<%= custom_field_tag_with_label @custom_value %>

+<% @user.custom_field_values.each do |value| %> +

<%= custom_field_tag_with_label :user, value %>

<% end %>
<%= submit_tag l(:button_submit) %> <% end %> - -<% content_for :header_tags do %> -<%= javascript_include_tag 'calendar/calendar' %> -<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> -<%= javascript_include_tag 'calendar/calendar-setup' %> -<%= stylesheet_link_tag 'calendar' %> -<% end %> diff --git a/app/views/account/show.rhtml b/app/views/account/show.rhtml index 97212b377..1160a5d8c 100644 --- a/app/views/account/show.rhtml +++ b/app/views/account/show.rhtml @@ -1,7 +1,11 @@ +
+<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %> +
+

<%=h @user.name %>

-<%= mail_to @user.mail unless @user.pref.hide_mail %> +<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>

  • <%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %>
  • <% for custom_value in @custom_values %> @@ -16,8 +20,8 @@

    <%=l(:label_project_plural)%>

      <% for membership in @memberships %> -
    • <%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %> - (<%= membership.role.name %>, <%= format_date(membership.created_on) %>)
    • +
    • <%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %> + (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)
    • <% end %>
    <% end %> diff --git a/app/views/admin/_no_data.rhtml b/app/views/admin/_no_data.rhtml new file mode 100644 index 000000000..5d52dc059 --- /dev/null +++ b/app/views/admin/_no_data.rhtml @@ -0,0 +1,8 @@ +
    +<% form_tag({:action => 'default_configuration'}) do %> + <%= simple_format(l(:text_no_configuration_data)) %> +

    <%= l(:field_language) %>: + <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %> + <%= submit_tag l(:text_load_default_configuration) %>

    +<% end %> +
    diff --git a/app/views/admin/index.rhtml b/app/views/admin/index.rhtml index 02eb5ae89..18bee34cb 100644 --- a/app/views/admin/index.rhtml +++ b/app/views/admin/index.rhtml @@ -1,5 +1,7 @@

    <%=l(:label_administration)%>

    +<%= render :partial => 'no_data' if @no_configuration_data %> +

    <%= link_to l(:label_project_plural), :controller => 'admin', :action => 'projects' %> | <%= link_to l(:label_new), :controller => 'projects', :action => 'add' %> @@ -28,18 +30,12 @@ <%= link_to l(:label_enumerations), :controller => 'enumerations' %>

    -

    -<%= link_to l(:field_mail_notification), :controller => 'admin', :action => 'mail_options' %> -

    - -

    -<%= link_to l(:label_authentication), :controller => 'auth_sources' %> -

    -

    <%= link_to l(:label_settings), :controller => 'settings' %>

    <%= link_to l(:label_information_plural), :controller => 'admin', :action => 'info' %> -

    \ No newline at end of file +

    + +<% html_title(l(:label_administration)) -%> diff --git a/app/views/admin/info.rhtml b/app/views/admin/info.rhtml index d84d2ad32..05c27f5ac 100644 --- a/app/views/admin/info.rhtml +++ b/app/views/admin/info.rhtml @@ -1,25 +1,27 @@

    <%=l(:label_information_plural)%>

    -

    <%=l(:field_version)%>: <%= Redmine::Info.versioned_name %> (<%= @db_adapter_name %>)

    +

    <%= Redmine::Info.versioned_name %> (<%= @db_adapter_name %>)

    - - - + + +
    Default administrator account changed<%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
    File repository writable<%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
    RMagick available<%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
    <%= l(:text_default_administrator_account_changed) %><%= image_tag (@flags[:default_admin_changed] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
    <%= l(:text_file_repository_writable) %><%= image_tag (@flags[:file_repository_writable] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
    <%= l(:text_rmagick_available) %><%= image_tag (@flags[:rmagick_available] ? 'true.png' : 'false.png'), :style => "vertical-align:bottom;" %>
    <% if @plugins.any? %>   -

    Plugins

    +

    <%= l(:label_plugins) %>

    - <% @plugins.keys.sort.each do |plugin| %> + <% @plugins.keys.sort {|x,y| x.to_s <=> y.to_s}.each do |plugin| %> - + <% end %>
    <%=h @plugins[plugin].name %> <%=h @plugins[plugin].description %> <%=h @plugins[plugin].author %> <%=h @plugins[plugin].version %><%= link_to('Configure', :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %><%= link_to(l(:button_configure), :controller => 'settings', :action => 'plugin', :id => plugin.to_s) if @plugins[plugin].configurable? %>
    <% end %> + +<% html_title(l(:label_information_plural)) -%> diff --git a/app/views/admin/mail_options.rhtml b/app/views/admin/mail_options.rhtml deleted file mode 100644 index 3c95ebd71..000000000 --- a/app/views/admin/mail_options.rhtml +++ /dev/null @@ -1,22 +0,0 @@ -
    -<%= link_to l(:label_send_test_email), :action => 'test_email' %> -
    - -

    <%=l(:field_mail_notification)%>

    - -<% form_tag({:action => 'mail_options'}, :id => 'mail-options-form') do %> - -
    <%=l(:text_select_mail_notifications)%> -<% @notifiables.each do |notifiable| %> -
    -<% end %> -

    <%= check_all_links('mail-options-form') %>

    -
    - -
    <%= l(:setting_emails_footer) %> -<%= text_area_tag 'emails_footer', Setting.emails_footer, :class => 'wiki-edit', :rows => 5 %> -
    - -<%= submit_tag l(:button_save) %> -<% end %> diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index d231be102..c42845622 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -17,19 +17,19 @@ <%= sort_header_tag('name', :caption => l(:label_project)) %> <%=l(:field_description)%> - <%=l(:field_is_public)%> <%=l(:label_subproject_plural)%> - <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %> + <%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> + <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> <% for project in @projects %> "> - <%= project.active? ? link_to(project.name, :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> - <%= textilizable project.description, :project => project %> - <%= image_tag 'true.png' if project.is_public? %> + <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> + <%= textilizable project.short_description, :project => project %> <%= project.children.size %> + <%= image_tag 'true.png' if project.is_public? %> <%= format_date(project.created_on) %> @@ -45,5 +45,6 @@ -

    <%= pagination_links_full @project_pages, :status => @status %> -[ <%= @project_pages.current.first_item %> - <%= @project_pages.current.last_item %> / <%= @project_count %> ]

    +

    <%= pagination_links_full @project_pages, @project_count %>

    + +<% html_title(l(:label_project_plural)) -%> diff --git a/app/views/attachments/_form.rhtml b/app/views/attachments/_form.rhtml index 18f08c6be..c98528b85 100644 --- a/app/views/attachments/_form.rhtml +++ b/app/views/attachments/_form.rhtml @@ -1,4 +1,9 @@ -

    - -<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

    + +<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%> +<%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %> +<%= l(:label_optional_description) %> + +
    +<%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;' %> +(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) + diff --git a/app/views/attachments/_links.rhtml b/app/views/attachments/_links.rhtml index cce11292e..9aae909fe 100644 --- a/app/views/attachments/_links.rhtml +++ b/app/views/attachments/_links.rhtml @@ -1,12 +1,17 @@
    <% for attachment in attachments %> -

    <%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' %> - (<%= number_to_human_size attachment.filesize %>) - <% unless options[:no_author] %> - <%= attachment.author.name %>, <%= format_date(attachment.created_on) %> - <% end %> +

    <%= link_to_attachment attachment, :class => 'icon icon-attachment' -%> +<%= h(" - #{attachment.description}") unless attachment.description.blank? %> + (<%= number_to_human_size attachment.filesize %>) <% if options[:delete_url] %> - <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}), :confirm => l(:text_are_you_sure), :method => :post %> + <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}), + :confirm => l(:text_are_you_sure), + :method => :post, + :class => 'delete', + :title => l(:button_delete) %> + <% end %> + <% unless options[:no_author] %> + <%= attachment.author %>, <%= format_time(attachment.created_on) %> <% end %>

    <% end %> diff --git a/app/views/attachments/diff.rhtml b/app/views/attachments/diff.rhtml new file mode 100644 index 000000000..7b64dca17 --- /dev/null +++ b/app/views/attachments/diff.rhtml @@ -0,0 +1,15 @@ +

    <%=h @attachment.filename %>

    + +
    +

    <%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <%= @attachment.author %>, <%= format_time(@attachment.created_on) %>

    +

    <%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> + (<%= number_to_human_size @attachment.filesize %>)

    + +
    +  +<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/app/views/attachments/file.rhtml b/app/views/attachments/file.rhtml new file mode 100644 index 000000000..468c6b666 --- /dev/null +++ b/app/views/attachments/file.rhtml @@ -0,0 +1,15 @@ +

    <%=h @attachment.filename %>

    + +
    +

    <%= h("#{@attachment.description} - ") unless @attachment.description.blank? %> + <%= @attachment.author %>, <%= format_time(@attachment.created_on) %>

    +

    <%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%> + (<%= number_to_human_size @attachment.filesize %>)

    + +
    +  +<%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %> + +<% content_for :header_tags do -%> + <%= stylesheet_link_tag "scm" -%> +<% end -%> diff --git a/app/views/auth_sources/_form.rhtml b/app/views/auth_sources/_form.rhtml index 24d2913e3..9ffffafc7 100644 --- a/app/views/auth_sources/_form.rhtml +++ b/app/views/auth_sources/_form.rhtml @@ -15,18 +15,19 @@ <%= text_field 'auth_source', 'account' %>

    -<%= password_field 'auth_source', 'account_password' %>

    +<%= password_field 'auth_source', 'account_password', :name => 'ignore', + :value => ((@auth_source.new_record? || @auth_source.account_password.blank?) ? '' : ('x'*15)), + :onfocus => "this.value=''; this.name='auth_source[account_password]';", + :onchange => "this.name='auth_source[account_password]';" %>

    <%= text_field 'auth_source', 'base_dn', :size => 60 %>

    -
    -

    <%= check_box 'auth_source', 'onthefly_register' %>

    +
    -

    -

    <%=l(:label_attribute_plural)%> +
    <%=l(:label_attribute_plural)%>

    <%= text_field 'auth_source', 'attr_login', :size => 20 %>

    @@ -39,7 +40,5 @@

    <%= text_field 'auth_source', 'attr_mail', :size => 20 %>

    -

    -
diff --git a/app/views/auth_sources/list.rhtml b/app/views/auth_sources/list.rhtml index f486f45b7..6836e6c67 100644 --- a/app/views/auth_sources/list.rhtml +++ b/app/views/auth_sources/list.rhtml @@ -25,4 +25,4 @@ -<%= pagination_links_full @auth_source_pages %> +

<%= pagination_links_full @auth_source_pages %>

diff --git a/app/views/boards/index.rhtml b/app/views/boards/index.rhtml index 3291d0194..655352a96 100644 --- a/app/views/boards/index.rhtml +++ b/app/views/boards/index.rhtml @@ -1,6 +1,6 @@

<%= l(:label_board_plural) %>

- +
@@ -19,7 +19,7 @@
<%= l(:label_board) %> <%= l(:label_topic_plural) %> <% if board.last_message %> - <%= board.last_message.author.name %>, <%= format_time(board.last_message.created_on) %>
+ <%= authoring board.last_message.created_on, board.last_message.author %>
<%= link_to_message board.last_message %> <% end %>
@@ -28,3 +28,15 @@ <% end %>
+ +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}, + :class => 'feed' %> +

+ +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> +<% end %> + +<% html_title l(:label_board_plural) %> diff --git a/app/views/boards/show.rhtml b/app/views/boards/show.rhtml index 0af89fdb7..96818df34 100644 --- a/app/views/boards/show.rhtml +++ b/app/views/boards/show.rhtml @@ -1,3 +1,5 @@ +<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %> +
<%= link_to_if_authorized l(:label_message_new), {:controller => 'messages', :action => 'new', :board_id => @board}, @@ -8,45 +10,52 @@

<%=h @board.name %>

<% if @topics.any? %> - +
<%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %> - + <%= sort_header_tag("#{Message.table_name}.replies_count", :caption => l(:label_reply_plural)) %> <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %> <% @topics.each do |topic| %> - - - - - - + + + + + <% end %>
<%= l(:field_subject) %> <%= l(:field_author) %><%= l(:label_reply_plural) %>
<%= link_to h(topic.subject), :controller => 'messages', :action => 'show', :board_id => @board, :id => topic %><%= link_to_user topic.author %><%= format_time(topic.created_on) %><%= topic.replies_count %> - +
<%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %><%= topic.author %><%= format_time(topic.created_on) %><%= topic.replies_count %> <% if topic.last_reply %> - <%= topic.last_reply.author.name %>, <%= format_time(topic.last_reply.created_on) %>
+ <%= authoring topic.last_reply.created_on, topic.last_reply.author %>
<%= link_to_message topic.last_reply %> <% end %> -
-

<%= pagination_links_full @topic_pages %> -[ <%= @topic_pages.current.first_item %> - <%= @topic_pages.current.last_item %> / <%= @topic_count %> ]

+

<%= pagination_links_full @topic_pages, @topic_count %>

<% else %>

<%= l(:label_no_data) %>

<% end %> + +<% html_title h(@board.name) %> diff --git a/app/views/common/403.rhtml b/app/views/common/403.rhtml index d2d03f811..d1173a186 100644 --- a/app/views/common/403.rhtml +++ b/app/views/common/403.rhtml @@ -3,4 +3,4 @@

<%= l(:notice_not_authorized) %>

Back

-<% set_html_title '403' %> +<% html_title '403' %> diff --git a/app/views/common/404.rhtml b/app/views/common/404.rhtml index 080b04842..753e716c6 100644 --- a/app/views/common/404.rhtml +++ b/app/views/common/404.rhtml @@ -3,4 +3,4 @@

<%= l(:notice_file_not_found) %>

Back

-<% set_html_title '404' %> +<% html_title '404' %> diff --git a/app/views/common/_attachments_form.rhtml b/app/views/common/_attachments_form.rhtml deleted file mode 100644 index 673f4a52e..000000000 --- a/app/views/common/_attachments_form.rhtml +++ /dev/null @@ -1,6 +0,0 @@ -

- -<%= file_field_tag 'attachments[]', :size => 30 %> -(<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) -

diff --git a/app/views/common/_calendar.rhtml b/app/views/common/_calendar.rhtml index 7534a1223..1095cd501 100644 --- a/app/views/common/_calendar.rhtml +++ b/app/views/common/_calendar.rhtml @@ -19,12 +19,15 @@ while day <= calendar.enddt %> elsif day == i.due_date image_tag('arrow_to.png') end %> - <%= h("#{i.project.name} -") unless @project && @project == i.project %> + <%= h("#{i.project} -") unless @project && @project == i.project %> <%= link_to_issue i %>: <%= h(truncate(i.subject, 30)) %> <%= render_issue_tooltip i %>
<% else %> - <%= link_to_version i, :class => "icon icon-package" %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i%> + <% end %> <% end %> diff --git a/app/views/common/_diff.rhtml b/app/views/common/_diff.rhtml new file mode 100644 index 000000000..0b28101b7 --- /dev/null +++ b/app/views/common/_diff.rhtml @@ -0,0 +1,64 @@ +<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%> +
+<% if diff_type == 'sbs' -%> + + + + + +<% prev_line_left, prev_line_right = nil, nil -%> +<% table_file.keys.sort.each do |key| -%> +<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> + + +<% end -%> + + + + + + +<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> +<% end -%> + +
<%= table_file.file_name %>
......
<%= table_file[key].nb_line_left %> +
<%=to_utf8 table_file[key].line_left %>
+
<%= table_file[key].nb_line_right %> +
<%=to_utf8 table_file[key].line_right %>
+
+ +<% else -%> + + + + + +<% prev_line_left, prev_line_right = nil, nil -%> +<% table_file.keys.sort.each do |key, line| %> +<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> + + + +<% end -%> + + + + <% if table_file[key].line_left.empty? -%> + + <% else -%> + + <% end -%> + +<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%> +<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%> +<% end -%> + +
<%= table_file.file_name %>
......
<%= table_file[key].nb_line_left %><%= table_file[key].nb_line_right %> +
<%=to_utf8 table_file[key].line_right %>
+
+
<%=to_utf8 table_file[key].line_left %>
+
+<% end -%> + +
+<% end -%> diff --git a/app/views/common/_file.rhtml b/app/views/common/_file.rhtml new file mode 100644 index 000000000..43f5c6c4b --- /dev/null +++ b/app/views/common/_file.rhtml @@ -0,0 +1,11 @@ +
+ + +<% line_num = 1 %> +<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %> + +<% line_num += 1 %> +<% end %> + +
<%= line_num %>
<%= line %>
+
diff --git a/app/views/common/_preview.rhtml b/app/views/common/_preview.rhtml index e3bfc3a25..fd95f1188 100644 --- a/app/views/common/_preview.rhtml +++ b/app/views/common/_preview.rhtml @@ -1,3 +1,3 @@
<%= l(:label_preview) %> -<%= textilizable @text, :attachments => @attachements %> +<%= textilizable @text, :attachments => @attachements, :object => @previewed %>
diff --git a/app/views/common/feed.atom.rxml b/app/views/common/feed.atom.rxml index 59b3163f4..c1b88a28e 100644 --- a/app/views/common/feed.atom.rxml +++ b/app/views/common/feed.atom.rxml @@ -1,6 +1,6 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do - xml.title @title + xml.title truncate_single_line(@title, 100) xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false})) xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) xml.id url_for(:controller => 'welcome', :only_path => false) @@ -9,14 +9,19 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do xml.generator(:uri => Redmine::Info.url, :version => Redmine::VERSION) { xml.text! Redmine::Info.versioned_name; } @items.each do |item| xml.entry do - xml.title truncate(item.event_title, 100) - xml.link "rel" => "alternate", "href" => url_for(item.event_url(:only_path => false)) - xml.id url_for(item.event_url(:only_path => false)) + url = url_for(item.event_url(:only_path => false)) + if @project + xml.title truncate_single_line(item.event_title, 100) + else + xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100) + end + xml.link "rel" => "alternate", "href" => url + xml.id url xml.updated item.event_datetime.xmlschema - author = item.event_author if item.respond_to?(:author) + author = item.event_author if item.respond_to?(:event_author) xml.author do - xml.name(author.is_a?(User) ? author.name : author) - xml.email(author.mail) if author.is_a?(User) + xml.name(author) + xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank? end if author xml.content "type" => "html" do xml.text! textilizable(item.event_description) diff --git a/app/views/custom_fields/_form.rhtml b/app/views/custom_fields/_form.rhtml index 013be9b70..f4aee6870 100644 --- a/app/views/custom_fields/_form.rhtml +++ b/app/views/custom_fields/_form.rhtml @@ -7,21 +7,43 @@ function toggle_custom_field_format() { p_length = $("custom_field_min_length"); p_regexp = $("custom_field_regexp"); p_values = $("custom_field_possible_values"); + p_searchable = $("custom_field_searchable"); + p_default = $("custom_field_default_value"); + + p_default.setAttribute('type','text'); + Element.show(p_default.parentNode); + switch (format.value) { case "list": Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); + if (p_searchable) Element.show(p_searchable.parentNode); Element.show(p_values); break; - case "date": case "bool": + p_default.setAttribute('type','checkbox'); Element.hide(p_length.parentNode); Element.hide(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "date": + Element.hide(p_length.parentNode); + Element.hide(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); + Element.hide(p_values); + break; + case "float": + case "int": + Element.show(p_length.parentNode); + Element.show(p_regexp.parentNode); + if (p_searchable) Element.hide(p_searchable.parentNode); Element.hide(p_values); break; default: Element.show(p_length.parentNode); Element.show(p_regexp.parentNode); + if (p_searchable) Element.show(p_searchable.parentNode); Element.hide(p_values); break; } @@ -47,7 +69,6 @@ function deleteValueField(e) { //]]> -

<%= f.text_field :name, :required => true %>

<%= f.select :field_format, custom_field_formats_for_select, {}, :onchange => "toggle_custom_field_format();" %>

@@ -59,11 +80,9 @@ function deleteValueField(e) { <% (@custom_field.possible_values.to_a + [""]).each do |value| %> <%= text_field_tag 'custom_field[possible_values][]', value, :size => 30 %> <%= image_to_function "delete.png", "deleteValueField(this);return false" %>
<% end %> -

+

<%= @custom_field.field_format == 'bool' ? f.check_box(:default_value) : f.text_field(:default_value) %>

-<%= javascript_tag "toggle_custom_field_format();" %> -
<% case @custom_field.type.to_s @@ -78,6 +97,7 @@ when "IssueCustomField" %>

<%= f.check_box :is_required %>

<%= f.check_box :is_for_all %>

<%= f.check_box :is_filter %>

+

<%= f.check_box :searchable %>

<% when "UserCustomField" %>

<%= f.check_box :is_required %>

@@ -85,5 +105,9 @@ when "IssueCustomField" %> <% when "ProjectCustomField" %>

<%= f.check_box :is_required %>

+<% when "TimeEntryCustomField" %> +

<%= f.check_box :is_required %>

+ <% end %>
+<%= javascript_tag "toggle_custom_field_format();" %> diff --git a/app/views/custom_fields/list.rhtml b/app/views/custom_fields/list.rhtml index 8862b3de1..43ddd99c8 100644 --- a/app/views/custom_fields/list.rhtml +++ b/app/views/custom_fields/list.rhtml @@ -1,21 +1,26 @@

<%=l(:label_custom_field_plural)%>

+<% selected_tab = params[:tab] ? params[:tab].to_s : custom_fields_tabs.first[:name] %> +
    -
  • <%= link_to l(:label_issue_plural), {}, :id=> "tab-IssueCustomField", :onclick => "showTab('IssueCustomField'); this.blur(); return false;" %>
  • -
  • <%= link_to l(:label_project_plural), {}, :id=> "tab-ProjectCustomField", :onclick => "showTab('ProjectCustomField'); this.blur(); return false;" %>
  • -
  • <%= link_to l(:label_user_plural), {}, :id=> "tab-UserCustomField", :onclick => "showTab('UserCustomField'); this.blur(); return false;" %>
  • +<% custom_fields_tabs.each do |tab| -%> +
  • <%= link_to l(tab[:label]), { :tab => tab[:name] }, + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? nil : 'selected'), + :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %>
  • +<% end -%>
-<% %w(IssueCustomField ProjectCustomField UserCustomField).each do |type| %> -
+<% custom_fields_tabs.each do |tab| %> +
- <% if type == 'IssueCustomField' %> + <% if tab[:name] == 'IssueCustomField' %> <% end %> @@ -23,12 +28,12 @@ -<% for custom_field in (@custom_fields_by_type[type] || []).sort %> +<% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> "> - <% if type == 'IssueCustomField' %> + <% if tab[:name] == 'IssueCustomField' %> <% end %> @@ -45,9 +50,9 @@ <% end; reset_cycle %>
<%=l(:field_name)%> <%=l(:field_field_format)%> <%=l(:field_is_required)%><%=l(:field_is_for_all)%> <%=l(:label_used_by)%>
<%= link_to custom_field.name, :action => 'edit', :id => custom_field %> <%= l(CustomField::FIELD_FORMATS[custom_field.field_format][:name]) %> <%= image_tag 'true.png' if custom_field.is_required? %><%= image_tag 'true.png' if custom_field.is_for_all? %> <%= custom_field.projects.count.to_s + ' ' + lwr(:label_project, custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %>
-
-<%= link_to l(:label_custom_field_new), {:action => 'new', :type => type}, :class => 'icon icon-add' %> + +

<%= link_to l(:label_custom_field_new), {:action => 'new', :type => tab[:name]}, :class => 'icon icon-add' %>

<% end %> -<%= javascript_tag "showTab('#{@tab}');" %> \ No newline at end of file +<% html_title(l(:label_custom_field_plural)) -%> diff --git a/app/views/projects/list_documents.rhtml b/app/views/documents/index.rhtml similarity index 80% rename from app/views/projects/list_documents.rhtml rename to app/views/documents/index.rhtml index bb272ee86..14d997360 100644 --- a/app/views/projects/list_documents.rhtml +++ b/app/views/documents/index.rhtml @@ -1,16 +1,16 @@
<%= link_to_if_authorized l(:label_document_new), - {:controller => 'projects', :action => 'add_document', :id => @project}, + {:controller => 'documents', :action => 'new', :project_id => @project}, :class => 'icon icon-add', :onclick => 'Element.show("add-document"); return false;' %>
-

<%= @document.title %>

+

<%=h @document.title %>

-

<%= @document.category.name %>
+

<%=h @document.category.name %>
<%= format_date @document.created_on %>

+
<%= textilizable @document.description, :attachments => @document.attachments %> -
+

<%= l(:label_attachment_plural) %>

-
    -<% for attachment in @attachments %> -
  • -
    - <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy_attachment', :id => @document, :attachment_id => attachment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> -
    - <%= link_to attachment.filename, :action => 'download', :id => @document, :attachment_id => attachment %> - (<%= number_to_human_size attachment.filesize %>)
    - <%= authoring attachment.created_on, attachment.author %>
    - <%= lwr(:label_download, attachment.downloads) %> -
  • -<% end %> -
-
- +<%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %> <% if authorize_for('documents', 'add_attachment') %> -

<%= toggle_link l(:label_attachment_new), "add_attachment_form" %>

- <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :class => "tabular", :id => "add_attachment_form", :style => "display:none;") do %> - <%= render :partial => 'attachments/form' %> +

<%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;", + :id => 'attach_files_link' %>

+ <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %> +
+

<%= render :partial => 'attachments/form' %>

+
<%= submit_tag l(:button_add) %> <% end %> <% end %> + +<% html_title @document.title -%> diff --git a/app/views/enumerations/destroy.rhtml b/app/views/enumerations/destroy.rhtml new file mode 100644 index 000000000..657df8322 --- /dev/null +++ b/app/views/enumerations/destroy.rhtml @@ -0,0 +1,12 @@ +

<%= l(@enumeration.option_name) %>: <%=h @enumeration %>

+ +<% form_tag({}) do %> +
+

<%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %>

+

<%= l(:text_enumeration_category_reassign_to) %> +<%= select_tag 'reassign_to_id', ("" + options_from_collection_for_select(@enumerations, 'id', 'name')) %>

+
+ +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'enumerations', :action => 'index' %> +<% end %> diff --git a/app/views/enumerations/list.rhtml b/app/views/enumerations/list.rhtml index bab4df830..7f3886b44 100644 --- a/app/views/enumerations/list.rhtml +++ b/app/views/enumerations/list.rhtml @@ -1,14 +1,14 @@

<%=l(:label_enumerations)%>

-<% Enumeration::OPTIONS.each do |option, name| %> -

<%= l(name) %>

+<% Enumeration::OPTIONS.each do |option, params| %> +

<%= l(params[:label]) %>

<% enumerations = Enumeration.get_values(option) %> <% if enumerations.any? %> <% enumerations.each do |enumeration| %> - + + <% end %>
<%= link_to enumeration.name, :action => 'edit', :id => enumeration %><%= link_to h(enumeration), :action => 'edit', :id => enumeration %> <%= image_tag('true.png') if enumeration.is_default? %> <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %> @@ -16,6 +16,9 @@ <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %> <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %> + <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %> +
@@ -24,3 +27,5 @@

<%= link_to l(:label_enumeration_new), { :action => 'new', :opt => option } %>

<% end %> + +<% html_title(l(:label_enumerations)) -%> diff --git a/app/views/issue_categories/destroy.rhtml b/app/views/issue_categories/destroy.rhtml index a563736e2..2b61810e7 100644 --- a/app/views/issue_categories/destroy.rhtml +++ b/app/views/issue_categories/destroy.rhtml @@ -3,9 +3,9 @@ <% form_tag({}) do %>

<%= l(:text_issue_category_destroy_question, @issue_count) %>

-

<%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %>
+


<% if @categories.size > 0 %> -<%= radio_button_tag 'todo', 'reassign', false %> <%= l(:text_issue_category_reassign_to) %>: +: <%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %>

<% end %>
diff --git a/app/views/issue_statuses/list.rhtml b/app/views/issue_statuses/list.rhtml index aaeec559a..e35911813 100644 --- a/app/views/issue_statuses/list.rhtml +++ b/app/views/issue_statuses/list.rhtml @@ -32,4 +32,6 @@ -<%= pagination_links_full @issue_status_pages %> \ No newline at end of file +

<%= pagination_links_full @issue_status_pages %>

+ +<% html_title(l(:label_issue_status_plural)) -%> diff --git a/app/views/issues/_changesets.rhtml b/app/views/issues/_changesets.rhtml new file mode 100644 index 000000000..caa983cbf --- /dev/null +++ b/app/views/issues/_changesets.rhtml @@ -0,0 +1,8 @@ +<% changesets.each do |changeset| %> +
+

<%= link_to("#{l(:label_revision)} #{changeset.revision}", + :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision) %>
+ <%= authoring(changeset.committed_on, changeset.committer) %>

+ <%= textilizable(changeset, :comments) %> +
+<% end %> diff --git a/app/views/issues/_edit.rhtml b/app/views/issues/_edit.rhtml new file mode 100644 index 000000000..2c7a4286e --- /dev/null +++ b/app/views/issues/_edit.rhtml @@ -0,0 +1,54 @@ +<% labelled_tabular_form_for :issue, @issue, + :url => {:action => 'edit', :id => @issue}, + :html => {:id => 'issue-form', + :class => nil, + :multipart => true} do |f| %> + <%= error_messages_for 'issue' %> + <%= error_messages_for 'time_entry' %> +
+ <% if @edit_allowed || !@allowed_statuses.empty? %> +
<%= l(:label_change_properties) %> + <% if !@issue.new_record? && !@issue.errors.any? && @edit_allowed %> + (<%= link_to l(:label_more), {}, :onclick => 'Effect.toggle("issue_descr_fields", "appear", {duration:0.3}); return false;' %>) + <% end %> + + <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %> +
+ <% end %> + <% if authorize_for('timelog', 'edit') %> +
<%= l(:button_log_time) %> + <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> +
+

<%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %>

+
+
+

<%= time_entry.select :activity_id, activity_collection_for_select_options %>

+
+

<%= time_entry.text_field :comments, :size => 60 %>

+ <% @time_entry.custom_field_values.each do |value| %> +

<%= custom_field_tag_with_label :time_entry, value %>

+ <% end %> + <% end %> +
+ <% end %> + +
<%= l(:field_notes) %> + <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> + <%= wikitoolbar_for 'notes' %> + +

<%=l(:label_attachment_plural)%>
<%= render :partial => 'attachments/form' %>

+
+
+ + <%= f.hidden_field :lock_version %> + <%= submit_tag l(:button_submit) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'issues', :action => 'preview', :project_id => @project, :id => @issue }, + :method => 'post', + :update => 'preview', + :with => 'Form.serialize("issue-form")', + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> +<% end %> + +
diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index 203d1cca3..419536fee 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -1,8 +1,22 @@ -<%= error_messages_for 'issue' %> -
+<% if @issue.new_record? %> +

<%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %>

+<%= observe_field :issue_tracker_id, :url => { :action => :new }, + :update => :content, + :with => "Form.serialize('issue-form')" %> +
+<% end %> + +
> +

<%= f.text_field :subject, :size => 80, :required => true %>

+

<%= f.text_area :description, :required => true, + :cols => 60, + :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), + :accesskey => accesskey(:edit), + :class => 'wiki-edit' %>

+
-<% if @issue.new_record? %> +<% if @issue.new_record? || @allowed_statuses.any? %>

<%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %>

<% else %>

<%= @issue.status.name %>

@@ -14,7 +28,10 @@ <%= prompt_to_remote(l(:label_issue_category_new), l(:label_issue_category_new), 'category[name]', {:controller => 'projects', :action => 'add_issue_category', :id => @project}, - :class => 'small') if authorize_for('projects', 'add_issue_category') %>

+ :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %>

+<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %>
@@ -24,29 +41,13 @@

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

-

<%= f.text_field :subject, :size => 80, :required => true %>

-

<%= f.text_area :description, :required => true, - :cols => 60, - :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), - :accesskey => accesskey(:edit), - :class => 'wiki-edit' %>

-

<%= f.select :fixed_version_id, (@project.versions.sort.collect {|v| [v.name, v.id]}), { :include_blank => true } %>

-<% for @custom_value in @custom_values %> -

<%= custom_field_tag_with_label @custom_value %>

-<% end %> +
+<%= render :partial => 'form_custom_fields' %> <% if @issue.new_record? %> -

-<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

+

<%= render :partial => 'attachments/form' %>

<% end %> -
+ +<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> <%= wikitoolbar_for 'issue_description' %> - -<% content_for :header_tags do %> - <%= javascript_include_tag 'calendar/calendar' %> - <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> - <%= javascript_include_tag 'calendar/calendar-setup' %> - <%= stylesheet_link_tag 'calendar' %> -<% end %> diff --git a/app/views/issues/_form_custom_fields.rhtml b/app/views/issues/_form_custom_fields.rhtml new file mode 100644 index 000000000..752fb4d37 --- /dev/null +++ b/app/views/issues/_form_custom_fields.rhtml @@ -0,0 +1,12 @@ +
+<% i = 0 %> +<% split_on = @issue.custom_field_values.size / 2 %> +<% @issue.custom_field_values.each do |value| %> +

<%= custom_field_tag_with_label :issue, value %>

+<% if i == split_on -%> +
+<% end -%> +<% i += 1 -%> +<% end -%> +
+
diff --git a/app/views/issues/_form_update.rhtml b/app/views/issues/_form_update.rhtml new file mode 100644 index 000000000..25e81a7fd --- /dev/null +++ b/app/views/issues/_form_update.rhtml @@ -0,0 +1,10 @@ +
+

<%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %>

+

<%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %>

+
+
+

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

+<%= content_tag('p', f.select(:fixed_version_id, + (@project.versions.sort.collect {|v| [v.name, v.id]}), + { :include_blank => true })) unless @project.versions.empty? %> +
diff --git a/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml index bab37b4fd..b8efdb400 100644 --- a/app/views/issues/_history.rhtml +++ b/app/views/issues/_history.rhtml @@ -1,13 +1,14 @@ -<% note_id = 1 %> +<% reply_links = authorize_for('issues', 'edit') -%> <% for journal in journals %> -

<%= link_to "##{note_id}", :anchor => "note-#{note_id}" %>
- <%= content_tag('a', '', :name => "note-#{note_id}")%> +
+

<%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %>
+ <%= content_tag('a', '', :name => "note-#{journal.indice}")%> <%= format_time(journal.created_on) %> - <%= journal.user.name %>

    <% for detail in journal.details %>
  • <%= show_detail(detail) %>
  • <% end %>
- <%= textilizable(journal.notes) unless journal.notes.blank? %> - <% note_id += 1 %> + <%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %> +
<% end %> diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index d8e3102df..b42357894 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -1,25 +1,22 @@ -
- +<% form_tag({}) do -%> +
- - <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#') %> + <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %> <% query.columns.each do |column| %> <%= column_header(column) %> <% end %> - <% issues.each do |issue| %> + <% issues.each do |issue| -%> "> - + - <% query.columns.each do |column| %> - <%= content_tag 'td', column_content(column, issue), :class => column.name %> - <% end %> + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> - <% end %> + <% end -%>
<%= link_to_remote(image_tag('edit.png'), - {:url => { :controller => 'projects', :action => 'bulk_edit_issues', :id => @project }, - :method => :get}, - {:title => l(:label_bulk_edit_selected_issues)}) if @project && User.current.allowed_to?(:edit_issues, @project) %> + <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
<%= check_box_tag("issue_ids[]", issue.id, false, :id => "issue_#{issue.id}", :disabled => (!@project || @project != issue.project)) %><%= check_box_tag("ids[]", issue.id, false, :id => nil) %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
+<% end -%> diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml index 9051e46b8..8900b7359 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -1,5 +1,6 @@ -<% if issues.length > 0 %> - +<% if issues && issues.any? %> +<% form_tag({}) do %> +
@@ -7,19 +8,21 @@ <% for issue in issues %> - <%= "status-#{issue.status.position} priority-#{issue.priority.position}" %>"> + "> - - + <% end %>
# <%=l(:field_tracker)%>
+ <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>

<%= issue.project.name %> - <%= issue.tracker.name %>
- <%= issue.status.name %> - <%= format_time(issue.updated_on) %>

-

<%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %>

+
<%=h issue.project.name %> - <%= issue.tracker.name %>
+ <%= issue.status.name %> - <%= format_time(issue.updated_on) %>
+ <%= link_to h(issue.subject), :controller => 'issues', :action => 'show', :id => issue %>
+<% end %> <% else %> - <%=l(:label_no_data)%> -<% end %> \ No newline at end of file +

<%= l(:label_no_data) %>

+<% end %> diff --git a/app/views/issues/_pdf.rfpdf b/app/views/issues/_pdf.rfpdf deleted file mode 100644 index 558399abb..000000000 --- a/app/views/issues/_pdf.rfpdf +++ /dev/null @@ -1,102 +0,0 @@ -<% pdf.SetFontStyle('B',11) - pdf.Cell(190,10, "#{issue.project.name} - #{issue.tracker.name} # #{issue.id}: #{issue.subject}") - pdf.Ln - - y0 = pdf.GetY - - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_status) + ":","LT") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, issue.status.name,"RT") - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_priority) + ":","LT") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, issue.priority.name,"RT") - pdf.Ln - - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_author) + ":","L") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, issue.author.name,"R") - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_category) + ":","L") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, (issue.category ? issue.category.name : "-"),"R") - pdf.Ln - - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_created_on) + ":","L") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, format_date(issue.created_on),"R") - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_assigned_to) + ":","L") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, (issue.assigned_to ? issue.assigned_to.name : "-"),"R") - pdf.Ln - - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_updated_on) + ":","LB") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, format_date(issue.updated_on),"RB") - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_due_date) + ":","LB") - pdf.SetFontStyle('',9) - pdf.Cell(60,5, format_date(issue.due_date),"RB") - pdf.Ln - - for custom_value in issue.custom_values - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, custom_value.custom_field.name + ":","L") - pdf.SetFontStyle('',9) - pdf.MultiCell(155,5, (show_value custom_value),"R") - end - - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_subject) + ":","LTB") - pdf.SetFontStyle('',9) - pdf.Cell(155,5, issue.subject,"RTB") - pdf.Ln - - pdf.SetFontStyle('B',9) - pdf.Cell(35,5, l(:field_description) + ":") - pdf.SetFontStyle('',9) - pdf.MultiCell(155,5, issue.description,"BR") - - pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY) - pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY) - - pdf.Ln - - pdf.SetFontStyle('B',9) - pdf.Cell(190,5, l(:label_history), "B") - pdf.Ln - for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") - pdf.SetFontStyle('B',8) - pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name) - pdf.Ln - pdf.SetFontStyle('I',8) - for detail in journal.details - pdf.Cell(190,5, "- " + show_detail(detail, true)) - pdf.Ln - end - if journal.notes? - pdf.SetFontStyle('',8) - pdf.MultiCell(190,5, journal.notes) - end - pdf.Ln - end - - if issue.attachments.any? - pdf.SetFontStyle('B',9) - pdf.Cell(190,5, l(:label_attachment_plural), "B") - pdf.Ln - for attachment in issue.attachments - pdf.SetFontStyle('',8) - pdf.Cell(80,5, attachment.filename) - pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R") - pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R") - pdf.Cell(65,5, attachment.author.name,0,0,"R") - pdf.Ln - end - end -%> diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml index 3b42ce465..e94d4180b 100644 --- a/app/views/issues/_sidebar.rhtml +++ b/app/views/issues/_sidebar.rhtml @@ -1,18 +1,14 @@ -<% if authorize_for('projects', 'add_issue') %> -

<%= l(:label_issue_new) %>

-<%= l(:label_tracker) %>: <%= new_issue_selector %> -<% end %> -

<%= l(:label_issue_plural) %>

-<%= link_to l(:label_issue_view_all), { :set_filter => 1 } %>
+<%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %>
+<% if @project %> <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %>
<%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %> +<% end %> +<% unless sidebar_queries.empty? -%>

<%= l(:label_query_plural) %>

-<% queries = @project.queries.find(:all, - :order => "name ASC", - :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)]) - queries.each do |query| %> +<% sidebar_queries.each do |query| -%> <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %>
-<% end %> +<% end -%> +<% end -%> diff --git a/app/views/issues/_bulk_edit_form.rhtml b/app/views/issues/bulk_edit.rhtml similarity index 50% rename from app/views/issues/_bulk_edit_form.rhtml rename to app/views/issues/bulk_edit.rhtml index bc3f62e6d..b916cf092 100644 --- a/app/views/issues/_bulk_edit_form.rhtml +++ b/app/views/issues/bulk_edit.rhtml @@ -1,15 +1,23 @@ -
-
<%= l(:label_bulk_edit_selected_issues) %> +

<%= l(:label_bulk_edit_selected_issues) %>

+
    <%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %>
+ +<% form_tag() do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> +
+
+<%= l(:label_change_properties) %>

-<% if @available_statuses %> +<% if @available_statuses.any? %> <% end %> +<%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none') + + options_from_collection_for_select(@project.issue_categories, :id, :name)) %>

+<%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none') + + options_from_collection_for_select(@project.versions, :id, :name)) %>

@@ -28,11 +38,13 @@

- -
-<%= text_area_tag 'notes', '', :cols => 80, :rows => 5 %> - +<%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
-

<%= submit_tag l(:button_apply) %> -<%= link_to l(:button_cancel), {}, :onclick => 'Element.hide("bulk-edit-fields"); if ($("query_form")) {Element.show("query_form")}; return false;' %>

+ +
<%= l(:field_notes) %> +<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'notes' %>
+ +

<%= submit_tag l(:button_submit) %> +<% end %> diff --git a/app/views/issues/change_status.rhtml b/app/views/issues/change_status.rhtml deleted file mode 100644 index a1e294556..000000000 --- a/app/views/issues/change_status.rhtml +++ /dev/null @@ -1,38 +0,0 @@ -

<%=l(:label_issue)%> #<%= @issue.id %>: <%=h @issue.subject %>

- -<%= error_messages_for 'issue' %> -<% labelled_tabular_form_for(:issue, @issue, :url => {:action => 'change_status', :id => @issue}, :html => {:multipart => true}) do |f| %> - -<%= hidden_field_tag 'confirm', 1 %> -<%= hidden_field_tag 'new_status_id', @new_status.id %> -<%= f.hidden_field :lock_version %> - -
-
-

<%= @new_status.name %>

-

<%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %>

-

<%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %>

-

<%= f.select :fixed_version_id, (@project.versions.sort.collect {|v| [v.name, v.id]}), { :include_blank => true } %>

-
-
-<% if authorize_for('timelog', 'edit') %> -<% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %> -

<%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %>

-

<%= time_entry.text_field :comments, :size => 40 %>

-

<%= time_entry.select :activity_id, (@activities.collect {|p| [p.name, p.id]}) %>

-<% end %> -<% end %> -
- -
- -

-<%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>

- -

-<%= file_field_tag 'attachments[]', :size => 30 %> (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)

-
- -<%= submit_tag l(:button_save) %> -<% end %> diff --git a/app/views/issues/changes.rxml b/app/views/issues/changes.rxml index f1aa5d206..239d2d6a3 100644 --- a/app/views/issues/changes.rxml +++ b/app/views/issues/changes.rxml @@ -1,12 +1,12 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do xml.title @title - xml.link "rel" => "self", "href" => url_for(:controller => 'feeds', :action => 'history', :format => 'atom', :only_path => false) - xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false) + xml.link "rel" => "self", "href" => url_for(:format => 'atom', :key => User.current.rss_key, :only_path => false) + xml.link "rel" => "alternate", "href" => home_url(:only_path => false) xml.id url_for(:controller => 'welcome', :only_path => false) - xml.updated((@changes.first ? @changes.first.event_datetime : Time.now).xmlschema) + xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema) xml.author { xml.name "#{Setting.app_title}" } - @changes.each do |change| + @journals.each do |change| issue = change.issue xml.entry do xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}" diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index e44911daf..671655db7 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -1,40 +1,90 @@ -<% back_to = url_for(:controller => 'issues', :action => 'index', :project_id => @project) %>
    +<% if !@issue.nil? -%>
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon-edit', :disabled => !@can[:edit] %>
  • <%= l(:field_status) %>
      - <% @statuses.each do |s| %> -
    • <%= context_menu_link s.name, {:controller => 'issues', :action => 'change_status', :id => @issue, :new_status_id => s}, - :selected => (s == @issue.status), :disabled => !(@can[:change_status] && @allowed_statuses.include?(s)) %>
    • - <% end %> + <% @statuses.each do |s| -%> +
    • <%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post, + :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %>
    • + <% end -%>
  • +<% else %> +
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
  • +<% end %> +
  • <%= l(:field_priority) %>
      - <% @priorities.each do |p| %> -
    • <%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => back_to}, :method => :post, - :selected => (p == @issue.priority), :disabled => !@can[:edit] %>
    • - <% end %> + <% @priorities.each do |p| -%> +
    • <%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post, + :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %>
    • + <% end -%>
  • + <% unless @project.nil? || @project.versions.empty? -%> +
  • + <%= l(:field_fixed_version) %> +
      + <% @project.versions.sort.each do |v| -%> +
    • <%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post, + :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %>
    • + <% end -%> +
    • <%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %>
    • +
    +
  • + <% end %> + <% unless @assignables.nil? || @assignables.empty? -%>
  • <%= l(:field_assigned_to) %>
      - <% @assignables.each do |u| %> -
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => back_to}, :method => :post, - :selected => (u == @issue.assigned_to), :disabled => !(@can[:edit] || @can[:change_status]) %>
    • - <% end %> -
    • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => back_to}, :method => :post, - :selected => @issue.assigned_to.nil?, :disabled => !(@can[:edit] || @can[:change_status]) %>
    • + <% @assignables.each do |u| -%> +
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post, + :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %>
    • + <% end -%> +
    • <%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %>
  • -
  • <%= context_menu_link l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue}, - :class => 'icon-copy', :disabled => !@can[:add] %>
  • -
  • <%= context_menu_link l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, - :class => 'icon-move', :disabled => !@can[:move] %> -
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, - :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon-del', :disabled => !@can[:delete] %>
  • + <% end %> + <% unless @project.nil? || @project.issue_categories.empty? -%> +
  • + <%= l(:field_category) %> +
      + <% @project.issue_categories.each do |u| -%> +
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post, + :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %>
    • + <% end -%> +
    • <%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post, + :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %>
    • +
    +
  • + <% end -%> +
  • + <%= l(:field_done_ratio) %> +
      + <% (0..10).map{|x|x*10}.each do |p| -%> +
    • <%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post, + :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %>
    • + <% end -%> +
    +
  • + +<% if !@issue.nil? %> +
  • <%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, + :class => 'icon-copy', :disabled => !@can[:copy] %>
  • + <% if @can[:log_time] -%> +
  • <%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, + :class => 'icon-time' %>
  • + <% end %> +<% end %> + +
  • <%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, + :class => 'icon-move', :disabled => !@can[:move] %>
  • +
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)}, + :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %>
diff --git a/app/views/issues/destroy.rhtml b/app/views/issues/destroy.rhtml new file mode 100644 index 000000000..2f3c036b6 --- /dev/null +++ b/app/views/issues/destroy.rhtml @@ -0,0 +1,15 @@ +

<%= l(:confirmation) %>

+ +<% form_tag do %> +<%= @issues.collect {|i| hidden_field_tag 'ids[]', i.id } %> +
+

<%= l(:text_destroy_time_entries_question, @hours) %>

+

+
+
+ +<%= text_field_tag 'reassign_to_id', params[:reassign_to_id], :size => 6, :onfocus => '$("todo_reassign").checked=true;' %> +

+
+<%= submit_tag l(:button_apply) %> +<% end %> diff --git a/app/views/issues/edit.rhtml b/app/views/issues/edit.rhtml index 1577216ed..97f26a205 100644 --- a/app/views/issues/edit.rhtml +++ b/app/views/issues/edit.rhtml @@ -1,19 +1,3 @@

<%=h "#{@issue.tracker.name} ##{@issue.id}" %>

-<% labelled_tabular_form_for :issue, @issue, - :url => {:action => 'edit'}, - :html => {:id => 'issue-form'} do |f| %> - <%= render :partial => 'form', :locals => {:f => f} %> - <%= f.hidden_field :lock_version %> - <%= submit_tag l(:button_save) %> - <%= link_to_remote l(:label_preview), - { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, - :method => 'post', - :update => 'preview', - :with => "Form.serialize('issue-form')", - :complete => "location.href='#preview-top'" - }, :accesskey => accesskey(:preview) %> -<% end %> - - -
+<%= render :partial => 'edit' %> diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index de0fd4add..973f3eb25 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -1,11 +1,12 @@ <% if @query.new_record? %>

<%=l(:label_issue_plural)%>

- <% set_html_title l(:label_issue_plural) %> + <% html_title(l(:label_issue_plural)) %> <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> <%= hidden_field_tag('project_id', @project.id) if @project %> +
<%= l(:label_filter_plural) %> <%= render :partial => 'queries/filters', :locals => {:query => @query} %> -
+

<%= link_to_remote l(:button_apply), { :url => { :set_filter => 1 }, :update => "content", @@ -17,12 +18,11 @@ :update => "content", }, :class => 'icon icon-reload' %> - <% if current_role && current_role.allowed_to?(:save_queries) %> + <% if User.current.allowed_to?(:save_queries, @project, :global => true) %> <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %> <% end %> -

-
-   +

+
<% end %> <% else %>
@@ -31,43 +31,37 @@ <%= link_to l(:button_delete), {:controller => 'queries', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> <% end %>
- -

<%= @query.name %>

+

<%=h @query.name %>

- <% set_html_title @query.name %> + <% html_title @query.name %> <% end %> <%= error_messages_for 'query' %> <% if @query.valid? %> <% if @issues.empty? %>

<%= l(:label_no_data) %>

<% else %> -<% form_tag({:controller => 'projects', :action => 'bulk_edit_issues', :id => @project}, :id => 'issues_form', :onsubmit => "if (!checkBulkEdit(this)) {alert('#{l(:notice_no_issue_selected)}'); return false;}" ) do %> <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %> -
+

<%= pagination_links_full @issue_pages, @issue_count %>

+ +

<%= l(:label_export_to) %> -<%= link_to 'CSV', {:format => 'csv'}, :class => 'icon icon-csv' %>, -<%= link_to 'PDF', {:format => 'pdf'}, :class => 'icon icon-pdf' %> -

-

<%= pagination_links_full @issue_pages %> -[ <%= @issue_pages.current.first_item %> - <%= @issue_pages.current.last_item %> / <%= @issue_count %> ]

-<% end %> +<%= link_to 'Atom', {:query_id => @query, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +<%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %> +<%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %> +

<% end %> <% end %> <% content_for :sidebar do %> <%= render :partial => 'issues/sidebar' %> -<% end if @project%> +<% end %> <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_issue_plural)) %> <%= auto_discovery_link_tag(:atom, {:action => 'changes', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> - <%= javascript_include_tag 'calendar/calendar' %> - <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> - <%= javascript_include_tag 'calendar/calendar-setup' %> - <%= stylesheet_link_tag 'calendar' %> <%= javascript_include_tag 'context_menu' %> <%= stylesheet_link_tag 'context_menu' %> <% end %> -<%= javascript_tag 'new ContextMenu({})' %> +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> diff --git a/app/views/issues/move.rhtml b/app/views/issues/move.rhtml new file mode 100644 index 000000000..35761e160 --- /dev/null +++ b/app/views/issues/move.rhtml @@ -0,0 +1,22 @@ +

<%= l(:button_move) %>

+ +
    <%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %>
+ +<% form_tag({}, :id => 'move_form') do %> +<%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %> + +
+

+<%= select_tag "new_project_id", + options_from_collection_for_select(@allowed_projects, 'id', 'name', @target_project.id), + :onchange => remote_function(:url => { :action => 'move' }, + :method => :get, + :update => 'content', + :with => "Form.serialize('move_form')") %>

+ +

+<%= select_tag "new_tracker_id", "" + options_from_collection_for_select(@trackers, "id", "name") %>

+
+ +<%= submit_tag l(:button_move) %> +<% end %> diff --git a/app/views/projects/add_issue.rhtml b/app/views/issues/new.rhtml similarity index 68% rename from app/views/projects/add_issue.rhtml rename to app/views/issues/new.rhtml index a68922906..280e2009b 100644 --- a/app/views/projects/add_issue.rhtml +++ b/app/views/issues/new.rhtml @@ -1,19 +1,19 @@ -

<%=l(:label_issue_new)%>: <%= @issue.tracker %>

+

<%=l(:label_issue_new)%>

<% labelled_tabular_form_for :issue, @issue, - :url => {:action => 'add_issue'}, :html => {:multipart => true, :id => 'issue-form'} do |f| %> - <%= f.hidden_field :tracker_id %> + <%= error_messages_for 'issue' %> +
<%= render :partial => 'issues/form', :locals => {:f => f} %> +
<%= submit_tag l(:button_create) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'issues', :action => 'preview', :id => @issue }, + { :url => { :controller => 'issues', :action => 'preview', :project_id => @project }, :method => 'post', :update => 'preview', :with => "Form.serialize('issue-form')", - :complete => "location.href='#preview-top'" + :complete => "Element.scrollTo('preview')" }, :accesskey => accesskey(:preview) %> <% end %> -
diff --git a/app/views/issues/show.rfpdf b/app/views/issues/show.rfpdf index 08f2cb92d..73d9d66b5 100644 --- a/app/views/issues/show.rfpdf +++ b/app/views/issues/show.rfpdf @@ -4,7 +4,123 @@ pdf.footer_date = format_date(Date.today) pdf.AddPage - render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => @issue } + pdf.SetFontStyle('B',11) + pdf.Cell(190,10, "#{@issue.project} - #{@issue.tracker} # #{@issue.id}: #{@issue.subject}") + pdf.Ln + + y0 = pdf.GetY + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_status) + ":","LT") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, @issue.status.name,"RT") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_priority) + ":","LT") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, @issue.priority.name,"RT") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_author) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, @issue.author.name,"R") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_category) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, (@issue.category ? @issue.category.name : "-"),"R") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_created_on) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(@issue.created_on),"R") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_assigned_to) + ":","L") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, (@issue.assigned_to ? @issue.assigned_to.name : "-"),"R") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_updated_on) + ":","LB") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(@issue.updated_on),"RB") + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_due_date) + ":","LB") + pdf.SetFontStyle('',9) + pdf.Cell(60,5, format_date(@issue.due_date),"RB") + pdf.Ln + + for custom_value in @issue.custom_values + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, custom_value.custom_field.name + ":","L") + pdf.SetFontStyle('',9) + pdf.MultiCell(155,5, (show_value custom_value),"R") + end + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_subject) + ":","LTB") + pdf.SetFontStyle('',9) + pdf.Cell(155,5, @issue.subject,"RTB") + pdf.Ln + + pdf.SetFontStyle('B',9) + pdf.Cell(35,5, l(:field_description) + ":") + pdf.SetFontStyle('',9) + pdf.MultiCell(155,5, @issue.description,"BR") + + pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY) + pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY) + + pdf.Ln + + if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @issue.project) + pdf.SetFontStyle('B',9) + pdf.Cell(190,5, l(:label_associated_revisions), "B") + pdf.Ln + for changeset in @issue.changesets + pdf.SetFontStyle('B',8) + pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.committer) + pdf.Ln + unless changeset.comments.blank? + pdf.SetFontStyle('',8) + pdf.MultiCell(190,5, changeset.comments) + end + pdf.Ln + end + end + + pdf.SetFontStyle('B',9) + pdf.Cell(190,5, l(:label_history), "B") + pdf.Ln + for journal in @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") + pdf.SetFontStyle('B',8) + pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name) + pdf.Ln + pdf.SetFontStyle('I',8) + for detail in journal.details + pdf.Cell(190,5, "- " + show_detail(detail, true)) + pdf.Ln + end + if journal.notes? + pdf.SetFontStyle('',8) + pdf.MultiCell(190,5, journal.notes) + end + pdf.Ln + end + + if @issue.attachments.any? + pdf.SetFontStyle('B',9) + pdf.Cell(190,5, l(:label_attachment_plural), "B") + pdf.Ln + for attachment in @issue.attachments + pdf.SetFontStyle('',8) + pdf.Cell(80,5, attachment.filename) + pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R") + pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R") + pdf.Cell(65,5, attachment.author.name,0,0,"R") + pdf.Ln + end + end %> <%= pdf.Output %> diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index 6e4f22574..463fa6960 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -1,10 +1,9 @@
-<%= show_and_goto_link(l(:label_add_note), 'add-note', :class => 'icon icon-note') if authorize_for('issues', 'add_note') %> -<%= link_to_if_authorized l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %> +<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %> <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %> <%= watcher_tag(@issue, User.current) %> -<%= link_to_if_authorized l(:button_copy), {:controller => 'projects', :action => 'add_issue', :id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> -<%= link_to_if_authorized l(:button_move), {:controller => 'projects', :action => 'move_issues', :id => @project, "issue_ids[]" => @issue.id }, :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %> +<%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %> <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
@@ -14,39 +13,39 @@

<%=h @issue.subject %>

<%= authoring @issue.created_on, @issue.author %>. - <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) if @issue.created_on != @issue.updated_on %>. + <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>

- - + + - - + + - - + + - + <% if User.current.allowed_to?(:view_time_entries, @project) %> - - + + <% end %> - + <% if @issue.estimated_hours %> - + <% end %> -<% n = 0 -for custom_value in @custom_values %> - +<% n = 0 -%> +<% @issue.custom_values.each do |value| -%> + <% n = n + 1 if (n > 1) n = 0 %> @@ -54,17 +53,18 @@ for custom_value in @custom_values %> <%end end %> +<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
<%=l(:field_status)%> :<%= @issue.status.name %><%=l(:field_start_date)%> :<%= format_date(@issue.start_date) %><%=l(:field_status)%>:<%= @issue.status.name %><%=l(:field_start_date)%>:<%= format_date(@issue.start_date) %>
<%=l(:field_priority)%> :<%= @issue.priority.name %><%=l(:field_due_date)%> :<%= format_date(@issue.due_date) %><%=l(:field_priority)%>:<%= @issue.priority.name %><%=l(:field_due_date)%>:<%= format_date(@issue.due_date) %>
<%=l(:field_assigned_to)%> :<%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %><%=l(:field_done_ratio)%> :<%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %><%=l(:field_assigned_to)%>:<%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %><%=l(:field_done_ratio)%>:<%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %>
<%=l(:field_category)%> :<%=h @issue.category ? @issue.category.name : "-" %><%=l(:field_category)%>:<%=h @issue.category ? @issue.category.name : "-" %><%=l(:label_spent_time)%> :<%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :issue_id => @issue}, :class => 'icon icon-time') : "-" %><%=l(:label_spent_time)%>:<%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %>
<%=l(:field_fixed_version)%> :<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %><%=l(:field_fixed_version)%>:<%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %><%=l(:field_estimated_hours)%> :<%= lwr(:label_f_hour, @issue.estimated_hours) %><%=l(:field_estimated_hours)%>:<%= lwr(:label_f_hour, @issue.estimated_hours) %>
<%= custom_value.custom_field.name %> :<%= simple_format(h(show_value(custom_value))) %><%=h value.custom_field.name %>:<%= simple_format(h(show_value(value))) %>

-<% if @issue.changesets.any? %> -
- <%= l(:label_revision_plural) %>: <%= @issue.changesets.collect{|changeset| link_to(changeset.revision, :controller => 'repositories', :action => 'revision', :id => @project, :rev => changeset.revision)}.join(", ") %> +
+<%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
-<% end %> - +

<%=l(:field_description)%>

+
<%= textilizable @issue, :description, :attachments => @issue.attachments %> +
<% if @issue.attachments.any? %> <%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %> @@ -77,16 +77,21 @@ end %>
<% end %> +<% if User.current.allowed_to?(:add_issue_watchers, @project) || + (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %> +
+
+<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %> +
+<% end %> +
-<% if authorize_for('issues', 'change_status') and @status_options and !@status_options.empty? %> - <% form_tag({:controller => 'issues', :action => 'change_status', :id => @issue}) do %> -

<%=l(:label_change_status)%> : - - <%= submit_tag l(:button_change) %>

- <% end %> +<% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %> +
+

<%=l(:label_associated_revisions)%>

+<%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %> +
<% end %> <% if @journals.any? %> @@ -95,29 +100,28 @@ end %> <%= render :partial => 'history', :locals => { :journals => @journals } %>
<% end %> +
-<% if authorize_for('issues', 'add_note') %> - - diff --git a/app/views/mailer/_issue_text_html.rhtml b/app/views/mailer/_issue_text_html.rhtml index a3eb05b01..d0f247812 100644 --- a/app/views/mailer/_issue_text_html.rhtml +++ b/app/views/mailer/_issue_text_html.rhtml @@ -12,4 +12,4 @@ <% end %> -<%= textilizable(issue.description) %> +<%= textilizable(issue, :description, :only_path => false) %> diff --git a/app/views/mailer/document_added.text.html.rhtml b/app/views/mailer/document_added.text.html.rhtml index 2ef63012b..dc1f659a0 100644 --- a/app/views/mailer/document_added.text.html.rhtml +++ b/app/views/mailer/document_added.text.html.rhtml @@ -1,3 +1,3 @@ <%= link_to @document.title, @document_url %> (<%= @document.category.name %>)

-<%= textilizable(@document.description) %> +<%= textilizable(@document, :description, :only_path => false) %> diff --git a/app/views/mailer/issue_add.text.html.rhtml b/app/views/mailer/issue_add.text.html.rhtml index adcb4c9fe..b1c4605e6 100644 --- a/app/views/mailer/issue_add.text.html.rhtml +++ b/app/views/mailer/issue_add.text.html.rhtml @@ -1,3 +1,3 @@ -<%= l(:text_issue_added, "##{@issue.id}") %> +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %>
-<%= render :file => "_issue_text_html", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_add.text.plain.rhtml b/app/views/mailer/issue_add.text.plain.rhtml index 797fb11b6..c6cb0837f 100644 --- a/app/views/mailer/issue_add.text.plain.rhtml +++ b/app/views/mailer/issue_add.text.plain.rhtml @@ -1,3 +1,4 @@ -<%= l(:text_issue_added, "##{@issue.id}") %> +<%= l(:text_issue_added, "##{@issue.id}", @issue.author) %> + ---------------------------------------- -<%= render :file => "_issue_text_plain", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.html.rhtml b/app/views/mailer/issue_edit.text.html.rhtml index 40d34968e..48affaf77 100644 --- a/app/views/mailer/issue_edit.text.html.rhtml +++ b/app/views/mailer/issue_edit.text.html.rhtml @@ -1,10 +1,11 @@ -<%= l(:text_issue_updated, "##{@issue.id}") %>
-<%= @journal.user.name %> +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> +
    <% for detail in @journal.details %>
  • <%= show_detail(detail, true) %>
  • <% end %>
-<%= textilizable(@journal.notes) %> + +<%= textilizable(@journal, :notes, :only_path => false) %>
-<%= render :file => "_issue_text_html", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_html", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/issue_edit.text.plain.rhtml b/app/views/mailer/issue_edit.text.plain.rhtml index 32019eaea..b5a5ec978 100644 --- a/app/views/mailer/issue_edit.text.plain.rhtml +++ b/app/views/mailer/issue_edit.text.plain.rhtml @@ -1,8 +1,9 @@ -<%= l(:text_issue_updated, "##{@issue.id}") %> -<%= @journal.user.name %> -<% for detail in @journal.details %> +<%= l(:text_issue_updated, "##{@issue.id}", @journal.user) %> + +<% for detail in @journal.details -%> <%= show_detail(detail, true) %> -<% end %> +<% end -%> + <%= @journal.notes if @journal.notes? %> ---------------------------------------- -<%= render :file => "_issue_text_plain", :use_full_path => true, :locals => { :issue => @issue, :issue_url => @issue_url } %> +<%= render :partial => "issue_text_plain", :locals => { :issue => @issue, :issue_url => @issue_url } %> diff --git a/app/views/mailer/layout.text.html.rhtml b/app/views/mailer/layout.text.html.rhtml index b78e92bdd..c95c94501 100644 --- a/app/views/mailer/layout.text.html.rhtml +++ b/app/views/mailer/layout.text.html.rhtml @@ -1,12 +1,32 @@ diff --git a/app/views/mailer/lost_password.text.html.rhtml b/app/views/mailer/lost_password.text.html.rhtml index 26eacfa92..4dd570c94 100644 --- a/app/views/mailer/lost_password.text.html.rhtml +++ b/app/views/mailer/lost_password.text.html.rhtml @@ -1,2 +1,4 @@

<%= l(:mail_body_lost_password) %>
<%= auto_link(@url) %>

+ +

<%= l(:field_login) %>: <%= @token.user.login %>

diff --git a/app/views/mailer/lost_password.text.plain.rhtml b/app/views/mailer/lost_password.text.plain.rhtml index aec1b5b86..f5000ed7e 100644 --- a/app/views/mailer/lost_password.text.plain.rhtml +++ b/app/views/mailer/lost_password.text.plain.rhtml @@ -1,2 +1,4 @@ <%= l(:mail_body_lost_password) %> <%= @url %> + +<%= l(:field_login) %>: <%= @token.user.login %> diff --git a/app/views/mailer/message_posted.text.html.rhtml b/app/views/mailer/message_posted.text.html.rhtml index 558a6e52a..d91ce5a04 100644 --- a/app/views/mailer/message_posted.text.html.rhtml +++ b/app/views/mailer/message_posted.text.html.rhtml @@ -1,4 +1,4 @@

<%=h @message.board.project.name %> - <%=h @message.board.name %>: <%= link_to @message.subject, @message_url %>

-<%= @message.author.name %> +<%= @message.author %> -<%= textilizable @message.content %> +<%= textilizable(@message, :content, :only_path => false) %> diff --git a/app/views/mailer/message_posted.text.plain.rhtml b/app/views/mailer/message_posted.text.plain.rhtml index cc1120567..ef6a3b3ae 100644 --- a/app/views/mailer/message_posted.text.plain.rhtml +++ b/app/views/mailer/message_posted.text.plain.rhtml @@ -1,4 +1,4 @@ <%= @message_url %> -<%= @message.author.name %> +<%= @message.author %> <%= @message.content %> diff --git a/app/views/mailer/news_added.text.html.rhtml b/app/views/mailer/news_added.text.html.rhtml index 010ef8ee1..15bc89fac 100644 --- a/app/views/mailer/news_added.text.html.rhtml +++ b/app/views/mailer/news_added.text.html.rhtml @@ -1,4 +1,4 @@

<%= link_to @news.title, @news_url %>

<%= @news.author.name %> -<%= textilizable(@news.description) %> +<%= textilizable(@news, :description, :only_path => false) %> diff --git a/app/views/mailer/reminder.text.html.rhtml b/app/views/mailer/reminder.text.html.rhtml new file mode 100644 index 000000000..1e33fbe43 --- /dev/null +++ b/app/views/mailer/reminder.text.html.rhtml @@ -0,0 +1,9 @@ +

<%= l(:mail_body_reminder, @issues.size, @days) %>

+ +
    +<% @issues.each do |issue| -%> +
  • <%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %>
  • +<% end -%> +
+ +

<%= link_to l(:label_issue_view_all), @issues_url %>

diff --git a/app/views/mailer/reminder.text.plain.rhtml b/app/views/mailer/reminder.text.plain.rhtml new file mode 100644 index 000000000..7e6a2e585 --- /dev/null +++ b/app/views/mailer/reminder.text.plain.rhtml @@ -0,0 +1,7 @@ +<%= l(:mail_body_reminder, @issues.size, @days) %>: + +<% @issues.each do |issue| -%> +* <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %> +<% end -%> + +<%= @issues_url %> diff --git a/app/views/messages/_form.rhtml b/app/views/messages/_form.rhtml index e484baf2f..540811ec3 100644 --- a/app/views/messages/_form.rhtml +++ b/app/views/messages/_form.rhtml @@ -1,14 +1,21 @@ <%= error_messages_for 'message' %> +<% replying ||= false %>


-<%= f.text_field :subject, :required => true, :size => 120 %>

+<%= f.text_field :subject, :size => 120 %> -

<%= f.text_area :content, :required => true, :cols => 80, :rows => 15, :class => 'wiki-edit' %>

+<% if !replying && User.current.allowed_to?(:edit_messages, @project) %> + + +<% end %> +

+ +

<%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %>

<%= wikitoolbar_for 'message_content' %> - -<%= render :partial => 'attachments/form' %> +

<%= l(:label_attachment_plural) %>
+<%= render :partial => 'attachments/form' %>

diff --git a/app/views/messages/edit.rhtml b/app/views/messages/edit.rhtml new file mode 100644 index 000000000..56e708620 --- /dev/null +++ b/app/views/messages/edit.rhtml @@ -0,0 +1,14 @@ +

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%=h @message.subject %>

+ +<% form_for :message, @message, :url => {:action => 'edit'}, :html => {:multipart => true, :id => 'message-form'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f, :replying => !@message.parent.nil?} %> + <%= submit_tag l(:button_save) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('message-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
diff --git a/app/views/messages/new.rhtml b/app/views/messages/new.rhtml index 5c688f465..050c13284 100644 --- a/app/views/messages/new.rhtml +++ b/app/views/messages/new.rhtml @@ -1,6 +1,15 @@

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%= l(:label_message_new) %>

-<% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true} do |f| %> +<% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %> <%= render :partial => 'form', :locals => {:f => f} %> <%= submit_tag l(:button_create) %> + <%= link_to_remote l(:label_preview), + { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('message-form')", + :complete => "Element.scrollTo('preview')" + }, :accesskey => accesskey(:preview) %> <% end %> + +
diff --git a/app/views/messages/show.rhtml b/app/views/messages/show.rhtml index 772f0653e..c24be7a21 100644 --- a/app/views/messages/show.rhtml +++ b/app/views/messages/show.rhtml @@ -1,27 +1,60 @@ -

<%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> » <%=h @message.subject %>

+<%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}), + link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %> -

<%= authoring @message.created_on, @message.author %>

-
-<%= textilizable(@message.content, :attachments => @message.attachments) %> +
+ <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %> + <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %> + <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> +
+ +

<%=h @topic.subject %>

+ +
+

<%= authoring @topic.created_on, @topic.author %>

+
+<%= textilizable(@topic.content, :attachments => @topic.attachments) %> +
+<%= link_to_attachments @topic.attachments, :no_author => true %>
-<%= link_to_attachments @message.attachments, :no_author => true %>
+<% unless @replies.empty? %>

<%= l(:label_reply_plural) %>

-<% @message.children.each do |message| %> +<% @replies.each do |message| %> "> -

<%=h message.subject %> - <%= message.author.name %>, <%= format_time(message.created_on) %>

-
<%= textilizable message.content %>
+
+ <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %> + <%= link_to_if_authorized image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit) %> + <%= link_to_if_authorized image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete) %> +
+
+

<%=h message.subject %> - <%= authoring message.created_on, message.author %>

+
<%= textilizable message, :content, :attachments => message.attachments %>
+ <%= link_to_attachments message.attachments, :no_author => true %> +
+<% end %> <% end %> -<% if authorize_for('messages', 'reply') %> -

<%= toggle_link l(:button_reply), "reply", :focus => "reply_content" %>

+<% if !@topic.locked? && authorize_for('messages', 'reply') %> +

<%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %>

<% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> + +<% html_title h(@topic.subject) %> diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index 11bba9c8d..20210c99a 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -4,7 +4,10 @@

<%=l(:label_my_account)%>

<%= error_messages_for 'user' %> -<% form_for :user, @user, :url => { :action => "account" }, :builder => TabularFormBuilder, :lang => current_language do |f| %> +<% form_for :user, @user, :url => { :action => "account" }, + :builder => TabularFormBuilder, + :lang => current_language, + :html => { :id => 'my_account_form' } do |f| %>

<%=l(:label_information_plural)%>

@@ -12,10 +15,6 @@

<%= f.text_field :lastname, :required => true %>

<%= f.text_field :mail, :required => true %>

<%= f.select :language, lang_options_for_select %>

- -<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %> -

<%= pref_fields.check_box :hide_mail %>

-<% end %>
<%= submit_tag l(:button_save) %> @@ -28,15 +27,26 @@ :onchange => 'if ($("notification_option").value == "selected") {Element.show("notified-projects")} else {Element.hide("notified-projects")}' %> <% content_tag 'div', :id => 'notified-projects', :style => (@notification_option == 'selected' ? '' : 'display:none;') do %>

<% User.current.projects.each do |project| %> -
+
<% end %>

<%= l(:text_user_mail_option) %>

<% end %>

+ +

<%=l(:label_preferences)%>

+
+<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %> +

<%= pref_fields.check_box :hide_mail %>

+

<%= pref_fields.select :time_zone, TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %>

+

<%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %>

+<% end %> +
<% end %> <% content_for :sidebar do %> <%= render :partial => 'sidebar' %> <% end %> + +<% html_title(l(:label_my_account)) -%> diff --git a/app/views/my/blocks/_documents.rhtml b/app/views/my/blocks/_documents.rhtml index a34be936f..d222e4203 100644 --- a/app/views/my/blocks/_documents.rhtml +++ b/app/views/my/blocks/_documents.rhtml @@ -1,8 +1,9 @@

<%=l(:label_document_plural)%>

+<% project_ids = @user.projects.select {|p| @user.allowed_to?(:view_documents, p)}.collect(&:id) %> <%= render(:partial => 'documents/document', :collection => Document.find(:all, :limit => 10, :order => "#{Document.table_name}.created_on DESC", - :conditions => "#{Document.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})", - :include => [:project])) unless @user.projects.empty? %> \ No newline at end of file + :conditions => "#{Document.table_name}.project_id in (#{project_ids.join(',')})", + :include => [:project])) unless project_ids.empty? %> \ No newline at end of file diff --git a/app/views/my/blocks/_timelog.rhtml b/app/views/my/blocks/_timelog.rhtml new file mode 100644 index 000000000..a3f74e54d --- /dev/null +++ b/app/views/my/blocks/_timelog.rhtml @@ -0,0 +1,52 @@ +

<%=l(:label_spent_time)%> (<%= l(:label_last_n_days, 7) %>)

+<% +entries = TimeEntry.find(:all, + :conditions => ["#{TimeEntry.table_name}.user_id = ? AND #{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", @user.id, Date.today - 6, Date.today], + :include => [:activity, :project, {:issue => [:tracker, :status]}], + :order => "#{TimeEntry.table_name}.spent_on DESC, #{Project.table_name}.name ASC, #{Tracker.table_name}.position ASC, #{Issue.table_name}.id ASC") +entries_by_day = entries.group_by(&:spent_on) +%> + +
+

<%= l(:label_total) %>: <%= html_hours("%.2f" % entries.sum(&:hours).to_f) %>

+
+ +<% if entries.any? %> + + + + + + + + + +<% entries_by_day.keys.sort.reverse.each do |day| %> + + + + + + + <% entries_by_day[day].each do |entry| -%> + + + + + + + + <% end -%> +<% end -%> + +
<%= l(:label_activity) %><%= l(:label_project) %><%= l(:field_comments) %><%= l(:field_hours) %>
<%= day == Date.today ? l(:label_today).titleize : format_date(day) %><%= html_hours("%.2f" % entries_by_day[day].sum(&:hours).to_f) %>
<%=h entry.activity %><%=h entry.project %> <%= ' - ' + link_to_issue(entry.issue, :title => h("#{entry.issue.subject} (#{entry.issue.status})")) if entry.issue %><%=h entry.comments %><%= html_hours("%.2f" % entry.hours) %> + <% if entry.editable_by?(@user) -%> + <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry}, + :title => l(:button_edit) %> + <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry}, + :confirm => l(:text_are_you_sure), + :method => :post, + :title => l(:button_delete) %> + <% end -%> +
+<% end %> diff --git a/app/views/my/page.rhtml b/app/views/my/page.rhtml index 5c6c906db..4d4c921b6 100644 --- a/app/views/my/page.rhtml +++ b/app/views/my/page.rhtml @@ -31,3 +31,12 @@ <% end if @blocks['right'] %> +<% content_for :header_tags do %> + <%= javascript_include_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu' %> +<% end %> + + +<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> + +<% html_title(l(:label_my_page)) -%> diff --git a/app/views/my/password.rhtml b/app/views/my/password.rhtml index 217a8758e..2e9fd0fa4 100644 --- a/app/views/my/password.rhtml +++ b/app/views/my/password.rhtml @@ -9,7 +9,7 @@

<%= password_field_tag 'new_password', nil, :size => 25 %>
-<%= l(:text_length_between, 4, 12) %>

+<%= l(:text_caracters_minimum, 4) %>

<%= password_field_tag 'new_password_confirmation', nil, :size => 25 %>

diff --git a/app/views/news/_news.rhtml b/app/views/news/_news.rhtml index e48b81ac3..e26d2c4a7 100644 --- a/app/views/news/_news.rhtml +++ b/app/views/news/_news.rhtml @@ -1,6 +1,6 @@

<%= link_to(h(news.project.name), :controller => 'projects', :action => 'show', :id => news.project) + ': ' unless @project %> <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %> -<%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %> +<%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %>
<% unless news.summary.blank? %><%=h news.summary %>
<% end %> -<%= authoring news.created_on, news.author %>

+<%= authoring news.created_on, news.author %>

diff --git a/app/views/news/edit.rhtml b/app/views/news/edit.rhtml index 5e015c4c7..4be566e0b 100644 --- a/app/views/news/edit.rhtml +++ b/app/views/news/edit.rhtml @@ -1,6 +1,14 @@

<%=l(:label_news)%>

-<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" } do |f| %> +<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" }, + :html => { :id => 'news-form' } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> -<% end %> \ No newline at end of file +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
diff --git a/app/views/news/index.rhtml b/app/views/news/index.rhtml index a956f86d0..9cac39002 100644 --- a/app/views/news/index.rhtml +++ b/app/views/news/index.rhtml @@ -1,17 +1,25 @@
<%= link_to_if_authorized(l(:label_news_new), - {:controller => 'projects', :action => 'add_news', :id => @project}, + {:controller => 'news', :action => 'new', :project_id => @project}, :class => 'icon icon-add', :onclick => 'Element.show("add-news"); return false;') if @project %>

<%=l(:label_news_plural)%>

@@ -24,11 +32,20 @@ <%= link_to h(news.title), :controller => 'news', :action => 'show', :id => news %> <%= "(#{news.comments_count} #{lwr(:label_comment, news.comments_count).downcase})" if news.comments_count > 0 %>

<%= authoring news.created_on, news.author %>

+
<%= textilizable(news.description) %> +
<% end %> <% end %> -<%= pagination_links_full @news_pages %> +

<%= pagination_links_full @news_pages %>

+ +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +

<% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :page => nil, :key => User.current.rss_key})) %> <% end %> + +<% html_title(l(:label_news_plural)) -%> diff --git a/app/views/news/new.rhtml b/app/views/news/new.rhtml new file mode 100644 index 000000000..a4d29a0a9 --- /dev/null +++ b/app/views/news/new.rhtml @@ -0,0 +1,14 @@ +

<%=l(:label_news_new)%>

+ +<% labelled_tabular_form_for :news, @news, :url => { :controller => 'news', :action => 'new', :project_id => @project }, + :html => { :id => 'news-form' } do |f| %> +<%= render :partial => 'news/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<%= link_to_remote l(:label_preview), + { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, + :method => 'post', + :update => 'preview', + :with => "Form.serialize('news-form')" + }, :accesskey => accesskey(:preview) %> +<% end %> +
diff --git a/app/views/news/show.rhtml b/app/views/news/show.rhtml index ef5bbcd4c..78be9c247 100644 --- a/app/views/news/show.rhtml +++ b/app/views/news/show.rhtml @@ -10,34 +10,52 @@

<%=h @news.title %>

-

<% unless @news.summary.empty? %><%=h @news.summary %>
<% end %> +

<% unless @news.summary.blank? %><%=h @news.summary %>
<% end %> <%= authoring @news.created_on, @news.author %>

+
<%= textilizable(@news.description) %> +

<%= l(:label_comment_plural) %>

-<% @news.comments.each do |comment| %> +<% @comments.each do |comment| %> <% next if comment.new_record? %> -

<%= format_time(comment.created_on) %> - <%= comment.author.name %>

- <%= link_to_if_authorized l(:button_delete), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> + <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'news', :action => 'destroy_comment', :id => @news, :comment_id => comment}, + :confirm => l(:text_are_you_sure), :method => :post, :title => l(:button_delete) %>
- <%= simple_format(auto_link(h comment.comments))%> -<% end if @news.comments_count > 0 %> +

<%= authoring comment.created_on, comment.author %>

+ <%= textilizable(comment.comments) %> +<% end if @comments.any? %>
<% if authorize_for 'news', 'add_comment' %>

<%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %>

<% form_tag({:action => 'add_comment', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %> -<%= text_area 'comment', 'comments', :cols => 60, :rows => 6 %> +<%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'comment_comments' %>

<%= submit_tag l(:button_add) %>

<% end %> <% end %> + +<% html_title @news.title -%> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index aa30f1eaa..11f7e3933 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -8,29 +8,41 @@

<%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %>

<% end %> -

<%= f.text_area :description, :required => true, :cols => 60, :rows => 5 %><%= l(:text_caracters_maximum, 255) %>

-

<%= f.text_field :identifier, :required => true, :size => 15, :disabled => @project.identifier_frozen? %>
<%= l(:text_length_between, 3, 12) %> <%= l(:text_project_identifier_info) unless @project.identifier_frozen? %>

-

<%= f.text_field :homepage, :size => 40 %>

+

<%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %>

+

<%= f.text_field :identifier, :required => true, :disabled => @project.identifier_frozen? %> +<% unless @project.identifier_frozen? %> +
<%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %> +<% end %>

+

<%= f.text_field :homepage, :size => 60 %>

<%= f.check_box :is_public %>

<%= wikitoolbar_for 'project_description' %> -<% for @custom_value in @custom_values %> -

<%= custom_field_tag_with_label @custom_value %>

+<% @project.custom_field_values.each do |value| %> +

<%= custom_field_tag_with_label :project, value %>

<% end %> - -<% unless @custom_fields.empty? %> -

-<% for custom_field in @custom_fields %> - <%= check_box_tag "custom_field_ids[]", custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %> - <%= custom_field.name %> -<% end %>

-<% end %> - -<% content_for :header_tags do %> -<%= javascript_include_tag 'calendar/calendar' %> -<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %> -<%= javascript_include_tag 'calendar/calendar-setup' %> -<%= stylesheet_link_tag 'calendar' %> +<% unless @trackers.empty? %> +
<%=l(:label_tracker_plural)%> +<% @trackers.each do |tracker| %> + <% end %> +<%= hidden_field_tag 'project[tracker_ids][]', '' %> +
+<% end %> + +<% unless @issue_custom_fields.empty? %> +
<%=l(:label_custom_field_plural)%> +<% @issue_custom_fields.each do |custom_field| %> + +<% end %> +<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %> +
+<% end %> + diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index c902d60a3..fa25812ac 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -1,41 +1,58 @@ -

<%=l(:label_activity)%>: <%= "#{month_name(@month).downcase} #{@year}" %>

+

<%= l(:label_activity) %>

+

<%= "#{l(:label_date_from)} #{format_date(@date_to - @days)} #{l(:label_date_to).downcase} #{format_date(@date_to-1)}" %>

-<% @events_by_day.keys.sort {|x,y| y <=> x }.each do |day| %> -

<%= day_name(day.cwday) %> <%= day.day %>

-
    - <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| %> -
  • <%= format_time(e.event_datetime, false) %> <%= link_to truncate(e.event_title, 100), e.event_url %>
    - <% unless e.event_description.blank? %><%= truncate(e.event_description, 500) %>
    <% end %> - <%= e.event_author if e.respond_to?(:event_author) %>

  • - <% end %> -
-<% end %> +
+<% @events_by_day.keys.sort.reverse.each do |day| %> +

<%= format_activity_day(day) %>

+
+<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> +
+ <%= format_time(e.event_datetime, false) %> + <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> + <%= link_to format_activity_title(e.event_title), e.event_url %>
+
<%= format_activity_description(e.event_description) %> + <%= e.event_author if e.respond_to?(:event_author) %>
+<% end -%> +
+<% end -%> +
-<% if @events_by_day.empty? %>

<%= l(:label_no_data) %>

<% end %> +<%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
-<% prev_params = params.clone.update :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) %> -<%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), - {:update => "content", :url => prev_params}, {:href => url_for(prev_params)} %> +<%= link_to_remote(('« ' + l(:label_previous)), + {:update => "content", :url => params.merge(:from => @date_to - @days), :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(params.merge(:from => @date_to - @days)), + :title => "#{l(:label_date_from)} #{format_date(@date_to - 2*@days)} #{l(:label_date_to).downcase} #{format_date(@date_to - @days - 1)}"}) %>
-<% next_params = params.clone.update :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) %> -<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'), - {:update => "content", :url => next_params}, {:href => url_for(next_params)} %> -  +<%= link_to_remote((l(:label_next) + ' »'), + {:update => "content", :url => params.merge(:from => @date_to + @days), :complete => 'window.scrollTo(0,0)'}, + {:href => url_for(params.merge(:from => @date_to + @days)), + :title => "#{l(:label_date_from)} #{format_date(@date_to)} #{l(:label_date_to).downcase} #{format_date(@date_to + @days - 1)}"}) unless @date_to >= Date.today %>
-
+  +

+ <%= l(:label_export_to) %> + <%= link_to 'Atom', params.merge(:format => :atom, :key => User.current.rss_key).delete_if{|k,v|k=="commit"}, :class => 'feed' %> +

<% content_for :header_tags do %> -<%= auto_discovery_link_tag(:atom, params.merge({:format => 'atom', :year => nil, :month => nil, :key => User.current.rss_key})) %> +<%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :year => nil, :month => nil, :key => User.current.rss_key)) %> <% end %> <% content_for :sidebar do %> -<% form_tag do %> +<% form_tag({}, :method => :get) do %>

<%= l(:label_activity) %>

-

<% @event_types.each do |t| %> -
+

<% @activity.event_types.each do |t| %> +
<% end %>

-

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+<% if @project && @project.active_children.any? %> +

+ <%= hidden_field_tag 'with_subprojects', 0 %> +<% end %> +

<%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>

<% end %> <% end %> + +<% html_title(l(:label_activity)) -%> diff --git a/app/views/projects/add.rhtml b/app/views/projects/add.rhtml index 4818cae4a..bc3d7b09a 100644 --- a/app/views/projects/add.rhtml +++ b/app/views/projects/add.rhtml @@ -3,13 +3,14 @@ <% labelled_tabular_form_for :project, @project, :url => { :action => "add" } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> -
-

+

<%= l(:label_module_plural) %> <% Redmine::AccessControl.available_project_modules.each do |m| %> -<%= check_box_tag 'enabled_modules[]', m, @project.module_enabled?(m) %> <%= m.to_s.humanize %> -<% end %>

-
- + +<% end %> + <%= submit_tag l(:button_save) %> <% end %> diff --git a/app/views/projects/add_document.rhtml b/app/views/projects/add_document.rhtml deleted file mode 100644 index 6c3fe2c7b..000000000 --- a/app/views/projects/add_document.rhtml +++ /dev/null @@ -1,13 +0,0 @@ -

<%=l(:label_document_new)%>

- -<% form_tag( { :action => 'add_document', :id => @project }, :class => "tabular", :multipart => true) do %> -<%= render :partial => 'documents/form' %> - -
-<%= render :partial => 'common/attachments_form'%> -
- -<%= submit_tag l(:button_create) %> -<% end %> - - diff --git a/app/views/projects/add_file.rhtml b/app/views/projects/add_file.rhtml index 839275373..0ee55083d 100644 --- a/app/views/projects/add_file.rhtml +++ b/app/views/projects/add_file.rhtml @@ -7,7 +7,7 @@

<%= select_tag "version_id", options_from_collection_for_select(@versions, "id", "name") %>

-<%= render :partial => 'attachments/form' %> +

<%= render :partial => 'attachments/form' %>

<%= submit_tag l(:button_add) %> <% end %> \ No newline at end of file diff --git a/app/views/projects/add_news.rhtml b/app/views/projects/add_news.rhtml deleted file mode 100644 index a6ecd3da7..000000000 --- a/app/views/projects/add_news.rhtml +++ /dev/null @@ -1,6 +0,0 @@ -

<%=l(:label_news_new)%>

- -<% labelled_tabular_form_for :news, @news, :url => { :action => "add_news" } do |f| %> -<%= render :partial => 'news/form', :locals => { :f => f } %> -<%= submit_tag l(:button_create) %> -<% end %> \ No newline at end of file diff --git a/app/views/projects/calendar.rhtml b/app/views/projects/calendar.rhtml index 2c02d59ad..048d8a5df 100644 --- a/app/views/projects/calendar.rhtml +++ b/app/views/projects/calendar.rhtml @@ -1,5 +1,4 @@ -<% cache(:year => @year, :month => @month, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %> -

<%= l(:label_calendar) %>: <%= "#{month_name(@month).downcase} #{@year}" %>

+

<%= "#{month_name(@month)} #{@year}" %>

@@ -20,12 +19,11 @@ <%= image_tag 'arrow_from.png' %>  <%= l(:text_tip_task_begin_day) %>
<%= image_tag 'arrow_to.png' %>  <%= l(:text_tip_task_end_day) %>
<%= image_tag 'arrow_bw.png' %>  <%= l(:text_tip_task_begin_end_day) %>
-<% end %> <% content_for :sidebar do %>

<%= l(:label_calendar) %>

- <% form_tag() do %> + <% form_tag({}, :method => :get) do %>

<%= select_month(@month, :prefix => "month", :discard_type => true) %> <%= select_year(@year, :prefix => "year", :discard_type => true) %>

@@ -33,8 +31,11 @@
<% end %> <% if @project.active_children.any? %> -
+
+ <%= hidden_field_tag 'with_subprojects', 0 %> <% end %> -

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+

<%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>

<% end %> <% end %> + +<% html_title(l(:label_calendar)) -%> diff --git a/app/views/projects/destroy.rhtml b/app/views/projects/destroy.rhtml index 8ef23197d..a1913c115 100644 --- a/app/views/projects/destroy.rhtml +++ b/app/views/projects/destroy.rhtml @@ -1,14 +1,16 @@

<%=l(:label_confirmation)%>

-
-
-

<%= @project_to_destroy.name %>
-<%=l(:text_project_destroy_confirmation)%>

+
+

<%=h @project_to_destroy %>
+<%=l(:text_project_destroy_confirmation)%> +<% if @project_to_destroy.children.any? %> +
<%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %> +<% end %> +

<% form_tag({:controller => 'projects', :action => 'destroy', :id => @project_to_destroy}) do %> - <%= hidden_field_tag "confirm", 1 %> + <%= submit_tag l(:button_delete) %> <% end %>

-
-
\ No newline at end of file + diff --git a/app/views/projects/gantt.rfpdf b/app/views/projects/gantt.rfpdf index a293906ba..e94fc5814 100644 --- a/app/views/projects/gantt.rfpdf +++ b/app/views/projects/gantt.rfpdf @@ -124,9 +124,9 @@ pdf.SetFontStyle('B',7) 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_end_date = (i.due_before <= @date_to ? i.due_before : @date_to ) - i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor + i_done_date = i.start_date + ((i.due_before - 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 ) diff --git a/app/views/projects/gantt.rhtml b/app/views/projects/gantt.rhtml index 21ef600a8..b18bca34c 100644 --- a/app/views/projects/gantt.rhtml +++ b/app/views/projects/gantt.rhtml @@ -55,8 +55,6 @@ t_height = g_height + headers_height
<% end %> -<% cache(:year => @year_from, :month => @month_from, :months => @months, :zoom => @zoom, :tracker_ids => @selected_tracker_ids, :subprojects => params[:with_subprojects], :lang => current_language) do %> -
@@ -72,10 +70,13 @@ top = headers_height + 8 @events.each do |i| %>
<% if i.is_a? Issue %> - <%= link_to_issue i %><%= " (#{i.project.name})" unless @project && @project == i.project %>: - <%=h i.subject %> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_issue i %>: <%=h i.subject %> <% else %> - <%= link_to_version i, :class => "icon icon-package" %> + + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%= link_to_version i %> + <% end %>
<% top = top + 20 @@ -165,9 +166,9 @@ top = headers_height + 10 @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_end_date = (i.due_before <= @date_to ? i.due_before : @date_to ) - i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor + i_done_date = i.start_date + ((i.due_before - 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 ) @@ -189,7 +190,6 @@ top = headers_height + 10 <%= i.status.name %> <%= (i.done_ratio).to_i %>% - <% # === tooltip === %>
<%= render_issue_tooltip i %> @@ -199,15 +199,13 @@ top = headers_height + 10 %>
 
- <%= i.name %> + <%= h("#{i.project} -") unless @project && @project == i.project %> + <%=h i %>
<% end %> <% top = top + 20 end %> -<% end # cache -%> - <% # # Today red line (excluded from cache) @@ -228,20 +226,24 @@ if Date.today >= @date_from and Date.today <= @date_to %>
-
<%= l(:label_export_to) %> -<%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'pdf'}, :class => 'icon icon-pdf' %> -<%= link_to 'PNG', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'png'}, :class => 'icon icon-image' if respond_to?('gantt_image') %> -
+

+<%= l(:label_export_to) %> +<%= link_to 'PDF', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'pdf'}, :class => 'pdf' %> +<%= content_tag('span', link_to('PNG', {:zoom => @zoom, :year => @year_from, :month => @month_from, :months => @months, :tracker_ids => @selected_tracker_ids, :with_subprojects => params[:with_subprojects], :format => 'png'}, :class => 'image')) if respond_to?('gantt_image') %> +

<% content_for :sidebar do %>

<%= l(:label_gantt) %>

- <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil)) do %> + <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil), :method => :get) do %> <% @trackers.each do |tracker| %>
<% end %> <% if @project.active_children.any? %> -
+
+ <%= hidden_field_tag 'with_subprojects', 0 %> <% end %> -

<%= submit_tag l(:button_apply), :class => 'button-small' %>

+

<%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %>

<% end %> <% end %> + +<% html_title(l(:label_gantt)) -%> diff --git a/app/views/projects/index.rhtml b/app/views/projects/index.rhtml new file mode 100644 index 000000000..4c68717f5 --- /dev/null +++ b/app/views/projects/index.rhtml @@ -0,0 +1,31 @@ +
+ <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.admin? %> + <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> | + <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%> +
+ +

<%=l(:label_project_plural)%>

+ +<% @project_tree.keys.sort.each do |project| %> +

<%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %>

+<%= textilizable(project.short_description, :project => project) %> + +<% if @project_tree[project].any? %> +

<%= l(:label_subproject_plural) %>: + <%= @project_tree[project].sort.collect {|subproject| + link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %>

+<% end %> +<% end %> + +<% if User.current.logged? %> +

+<%= l(:label_my_projects) %> +

+<% end %> + +

+<%= l(:label_export_to) %> +<%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %> +

+ +<% html_title(l(:label_project_plural)) -%> diff --git a/app/views/projects/list.rhtml b/app/views/projects/list.rhtml deleted file mode 100644 index 51c1b544a..000000000 --- a/app/views/projects/list.rhtml +++ /dev/null @@ -1,18 +0,0 @@ -

<%=l(:label_project_plural)%>

- -<% @project_tree.keys.sort.each do |project| %> -

<%= link_to project.name, {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %>

-<%= textilizable(project.description, :project => project) %> - -<% if @project_tree[project].any? %> -

<%= l(:label_subproject_plural) %>: - <%= @project_tree[project].sort.collect {|subproject| - link_to(subproject.name, {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %>

-<% end %> -<% end %> - -<% if User.current.logged? %> -
-<%= l(:label_my_projects) %> -
-<% end %> diff --git a/app/views/projects/list_files.rhtml b/app/views/projects/list_files.rhtml index 0e4f19d81..79e41f16d 100644 --- a/app/views/projects/list_files.rhtml +++ b/app/views/projects/list_files.rhtml @@ -9,10 +9,10 @@ - - - - + <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %> + <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %> + <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %> + <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %> <% if delete_allowed %><% end %> @@ -23,16 +23,14 @@ <% for file in version.attachments %> "> - - + + <% if delete_allowed %> <% end %> @@ -41,4 +39,6 @@ <% end %> <% end %> -
<%=l(:field_version)%><%=l(:field_filename)%><%=l(:label_date)%><%=l(:field_filesize)%>D/LMD5
<%= link_to file.filename, :controller => 'versions', :action => 'download', :id => version, :attachment_id => file %><%= format_date(file.created_on) %><%= link_to_attachment file, :download => true, :title => file.description %><%= format_time(file.created_on) %> <%= number_to_human_size(file.filesize) %> <%= file.downloads %> <%= file.digest %> -
<%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %> -
\ No newline at end of file + + +<% html_title(l(:label_attachment_plural)) -%> diff --git a/app/views/projects/move_issues.rhtml b/app/views/projects/move_issues.rhtml deleted file mode 100644 index b29653037..000000000 --- a/app/views/projects/move_issues.rhtml +++ /dev/null @@ -1,24 +0,0 @@ -

<%=l(:button_move)%>

- - -<% form_tag({:action => 'move_issues', :id => @project}, :class => "tabular") do %> - -
-

-<% for issue in @issues %> - <%= link_to_issue issue %>: <%=h issue.subject %> - <%= hidden_field_tag "issue_ids[]", issue.id %>
-<% end %> -(<%= @issues.length%> <%= lwr(:label_issue, @issues.length)%>)

- -  - - -

-<%= select_tag "new_project_id", options_from_collection_for_select(@projects, "id", "name", @project.id) %>

- -

-<%= select_tag "new_tracker_id", "" + options_from_collection_for_select(@trackers, "id", "name") %>

-
-<%= submit_tag l(:button_move) %> -<% end %> diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml index c2f3cbf34..0778d8138 100644 --- a/app/views/projects/roadmap.rhtml +++ b/app/views/projects/roadmap.rhtml @@ -2,67 +2,49 @@ <% if @versions.empty? %>

<%= l(:label_no_data) %>

-<% end %> - +<% else %> +
<% @versions.each do |version| %> -

<%= version.name %>

- <% if version.completed? %> -

<%= format_date(version.effective_date) %>

- <% elsif version.overdue? %> -

<%= l(:label_roadmap_overdue, distance_of_time_in_words(Time.now, version.effective_date)) %> (<%= format_date(version.effective_date) %>)

- <% elsif version.effective_date %> -

<%=l(:label_roadmap_due_in)%> <%= distance_of_time_in_words Time.now, version.effective_date %> (<%= format_date(version.effective_date) %>)

- <% end %> -

<%=h version.description %>

- - <% if version.fixed_issues.count > 0 %> - <%= progress_bar(version.completed_pourcent, :width => '40em', :legend => ('%0.0f%' % version.completed_pourcent)) %> -

- <%= link_to(version.closed_issues_count, :controller => 'issues', :action => 'index', :project_id => @project, :status_id => 'c', :fixed_version_id => version, :set_filter => 1) %> - <%= lwr(:label_closed_issues, version.closed_issues_count) %> - (<%= '%0.0f' % (version.closed_issues_count.to_f / version.fixed_issues.count * 100) %>%) -   - <%= link_to(version.open_issues_count, :controller => 'issues', :action => 'index', :project_id => @project, :status_id => 'o', :fixed_version_id => version, :set_filter => 1) %> - <%= lwr(:label_open_issues, version.open_issues_count)%> - (<%= '%0.0f' % (version.open_issues_count.to_f / version.fixed_issues.count * 100) %>%) -

- <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> - <% issues = version.fixed_issues.find(:all, - :include => [:status, :tracker], - :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], - :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty? - issues ||= [] - %> -
    - <% if issues.size > 0 %> - <% issues.each do |issue| %> -
  • - <%= link = link_to_issue(issue) - issue.status.is_closed? ? content_tag("del", link) : link %>: <%=h issue.subject %> - <%= content_tag "em", "(#{l(:label_closed_issues)})" if issue.status.is_closed? %> -
  • - <% end %> - <% end %> -
- <% else %> -

<%= l(:label_roadmap_no_issues) %>

+ <%= tag 'a', :name => version.name %> +

<%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %>

+ <%= render :partial => 'versions/overview', :locals => {:version => version} %> + <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> + + <% issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty? + issues ||= [] + %> + <% if issues.size > 0 %> + <% end %> <% end %> +
+<% end %> <% content_for :sidebar do %> -<% form_tag do %> +<% form_tag({}, :method => :get) do %>

<%= l(:label_roadmap) %>

<% @trackers.each do |tracker| %> -