diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index b0a5b42a7..5fa56b167 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -83,9 +83,9 @@ class AccountController < ApplicationController else @user = User.new(params[:user]) @user.admin = false - @user.status = User::STATUS_REGISTERED + @user.register if session[:auth_source_registration] - @user.status = User::STATUS_ACTIVE + @user.activate @user.login = session[:auth_source_registration][:login] @user.auth_source_id = session[:auth_source_registration][:auth_source_id] if @user.save @@ -116,8 +116,8 @@ class AccountController < ApplicationController token = Token.find_by_action_and_value('register', params[:token]) redirect_to(home_url) && return unless token and !token.expired? user = token.user - redirect_to(home_url) && return unless user.status == User::STATUS_REGISTERED - user.status = User::STATUS_ACTIVE + redirect_to(home_url) && return unless user.registered? + user.activate if user.save token.destroy flash[:notice] = l(:notice_account_activated) @@ -170,7 +170,7 @@ class AccountController < ApplicationController user.mail = registration['email'] unless registration['email'].nil? user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil? user.random_password - user.status = User::STATUS_REGISTERED + user.register case Setting.self_registration when '1' @@ -241,7 +241,7 @@ class AccountController < ApplicationController # Pass a block for behavior when a user fails to save def register_automatically(user, &block) # Automatic activation - user.status = User::STATUS_ACTIVE + user.activate user.last_login_on = Time.now if user.save self.logged_user = user diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb new file mode 100644 index 000000000..ae6a67369 --- /dev/null +++ b/app/controllers/activities_controller.rb @@ -0,0 +1,59 @@ +class ActivitiesController < ApplicationController + menu_item :activity + before_filter :find_optional_project + accept_key_auth :index + + def index + @days = Setting.activity_days_default.to_i + + if params[:from] + begin; @date_to = params[:from].to_date + 1; rescue; end + end + + @date_to ||= Date.today + 1 + @date_from = @date_to - @days + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') + @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id])) + + @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, + :with_subprojects => @with_subprojects, + :author => @author) + @activity.scope_select {|t| !params["show_#{t}"].nil?} + @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty? + + events = @activity.events(@date_from, @date_to) + + if events.empty? || stale?(:etag => [events.first, User.current]) + respond_to do |format| + format.html { + @events_by_day = events.group_by(&:event_date) + render :layout => false if request.xhr? + } + format.atom { + title = l(:label_activity) + if @author + title = @author.name + elsif @activity.scope.size == 1 + title = l("label_#{@activity.scope.first.singularize}_plural") + end + render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") + } + end + end + + rescue ActiveRecord::RecordNotFound + render_404 + end + + private + + # TODO: refactor, duplicated in projects_controller + def find_optional_project + return true unless params[:id] + @project = Project.find(params[:id]) + authorize + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6e5634dbc..1299dac36 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -153,7 +153,7 @@ class ApplicationController < ActionController::Base # Authorize the user for the requested action def authorize(ctrl = params[:controller], action = params[:action], global = false) - allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global) + allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global) allowed ? true : deny_access end @@ -169,6 +169,13 @@ class ApplicationController < ActionController::Base render_404 end + # Find project of id params[:project_id] + def find_project_by_project_id + @project = Project.find(params[:project_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + # Find a project based on params[:project_id] # TODO: some subclasses override this, see about merging their logic def find_optional_project @@ -201,7 +208,26 @@ class ApplicationController < ActionController::Base def self.model_object(model) write_inheritable_attribute('model_object', model) end - + + # Filter for bulk issue 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 + @project = @projects.first if @projects.size == 1 + rescue ActiveRecord::RecordNotFound + render_404 + end + + # Check if project is unique before bulk operations + def check_project_uniqueness + unless @project + # TODO: let users bulk edit/move/destroy issues from different projects + render_error 'Can not bulk edit/move/destroy issues from different projects' + return false + end + 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 @@ -218,6 +244,10 @@ class ApplicationController < ActionController::Base end end + def back_url + params[:back_url] || request.env['HTTP_REFERER'] + end + def redirect_back_or_default(default) back_url = CGI.unescape(params[:back_url].to_s) if !back_url.blank? @@ -238,7 +268,7 @@ class ApplicationController < ActionController::Base def render_403 @project = nil respond_to do |format| - format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 } + format.html { render :template => "common/403", :layout => use_layout, :status => 403 } format.atom { head 403 } format.xml { head 403 } format.js { head 403 } @@ -249,7 +279,7 @@ class ApplicationController < ActionController::Base def render_404 respond_to do |format| - format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 } + format.html { render :template => "common/404", :layout => use_layout, :status => 404 } format.atom { head 404 } format.xml { head 404 } format.js { head 404 } @@ -262,7 +292,7 @@ class ApplicationController < ActionController::Base respond_to do |format| format.html { flash.now[:error] = msg - render :text => '', :layout => !request.xhr?, :status => 500 + render :text => '', :layout => use_layout, :status => 500 } format.atom { head 500 } format.xml { head 500 } @@ -270,6 +300,13 @@ class ApplicationController < ActionController::Base format.json { head 500 } end end + + # Picks which layout to use based on the request + # + # @return [boolean, string] name of the layout to use or false for no layout + def use_layout + request.xhr? ? false : 'base' + end def invalid_authenticity_token if api_request? @@ -345,6 +382,21 @@ class ApplicationController < ActionController::Base flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present? end + # Sets the `flash` notice or error based the number of issues that did not save + # + # @param [Array, Issue] issues all of the saved and unsaved Issues + # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved + def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids) + if unsaved_issue_ids.empty? + flash[:notice] = l(:notice_successful_update) unless issues.empty? + else + flash[:error] = l(:notice_failed_to_save_issues, + :count => unsaved_issue_ids.size, + :total => issues.size, + :ids => '#' + unsaved_issue_ids.join(', #')) + end + end + # Rescues an invalid query statement. Just in case... def query_statement_invalid(exception) logger.error "Query::StatementInvalid: #{exception.message}" if logger diff --git a/app/controllers/auto_completes_controller.rb b/app/controllers/auto_completes_controller.rb new file mode 100644 index 000000000..1438106f6 --- /dev/null +++ b/app/controllers/auto_completes_controller.rb @@ -0,0 +1,25 @@ +class AutoCompletesController < ApplicationController + before_filter :find_project + + def issues + @issues = [] + q = params[:q].to_s + if q.match(/^\d+$/) + @issues << @project.issues.visible.find_by_id(q.to_i) + end + unless q.blank? + @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10) + end + render :layout => false + end + + private + + def find_project + project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 541fefada..fa82218de 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -18,6 +18,7 @@ class BoardsController < ApplicationController default_search_scope :messages before_filter :find_project, :find_board_if_available, :authorize + accept_key_auth :index, :show helper :messages include MessagesHelper diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index f2af58086..febacd075 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -1,4 +1,5 @@ class CalendarsController < ApplicationController + menu_item :calendar before_filter :find_optional_project rescue_from Query::StatementInvalid, :with => :query_statement_invalid @@ -31,8 +32,11 @@ class CalendarsController < ApplicationController @calendar.events = events end - render :layout => false if request.xhr? + render :action => 'show', :layout => false if request.xhr? end + def update + show + end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 000000000..7432f831f --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,36 @@ +class CommentsController < ApplicationController + default_search_scope :news + model_object News + before_filter :find_model_object + before_filter :find_project_from_association + before_filter :authorize + + verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } + def create + @comment = Comment.new(params[:comment]) + @comment.author = User.current + if @news.comments << @comment + flash[:notice] = l(:label_comment_added) + end + + redirect_to :controller => 'news', :action => 'show', :id => @news + end + + verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed } + def destroy + @news.comments.find(params[:comment_id]).destroy + redirect_to :controller => 'news', :action => 'show', :id => @news + end + + private + + # ApplicationController's find_model_object sets it based on the controller + # name so it needs to be overriden and set to @news instead + def find_model_object + super + @news = @object + @comment = nil + @news + end + +end diff --git a/app/controllers/context_menus_controller.rb b/app/controllers/context_menus_controller.rb new file mode 100644 index 000000000..3e1438306 --- /dev/null +++ b/app/controllers/context_menus_controller.rb @@ -0,0 +1,43 @@ +class ContextMenusController < ApplicationController + helper :watchers + + def issues + @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) + else + @allowed_statuses = @issues.map do |i| + i.new_statuses_allowed_to(User.current) + end.inject do |memo,s| + memo & s + end + end + @projects = @issues.collect(&:project).compact.uniq + @project = @projects.first if @projects.size == 1 + + @can = {:edit => User.current.allowed_to?(:edit_issues, @projects), + :log_time => (@project && User.current.allowed_to?(:log_time, @project)), + :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)), + :move => (@project && User.current.allowed_to?(:move_issues, @project)), + :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), + :delete => User.current.allowed_to?(:delete_issues, @projects) + } + if @project + @assignables = @project.assignable_users + @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) + @trackers = @project.trackers + else + #when multiple projects, we only keep the intersection of each set + @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a} + @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t} + end + + @priorities = IssuePriority.all.reverse + @statuses = IssueStatus.find(:all, :order => 'position') + @back = back_url + + render :layout => false + end + +end diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb new file mode 100644 index 000000000..d7234985e --- /dev/null +++ b/app/controllers/files_controller.rb @@ -0,0 +1,36 @@ +class FilesController < ApplicationController + menu_item :files + + before_filter :find_project_by_project_id + before_filter :authorize + + helper :sort + include SortHelper + + def index + sort_init 'filename', 'asc' + sort_update 'filename' => "#{Attachment.table_name}.filename", + 'created_on' => "#{Attachment.table_name}.created_on", + 'size' => "#{Attachment.table_name}.filesize", + 'downloads' => "#{Attachment.table_name}.downloads" + + @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)] + @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse + render :layout => !request.xhr? + end + + def new + @versions = @project.versions.sort + end + + def create + container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id])) + attachments = Attachment.attach_files(container, params[:attachments]) + render_attachment_warning_if_needed(container) + + if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added') + Mailer.deliver_attachments_added(attachments[:files]) + end + redirect_to project_files_path(@project) + end +end diff --git a/app/controllers/gantts_controller.rb b/app/controllers/gantts_controller.rb index bc2d6350c..50fd8c13d 100644 --- a/app/controllers/gantts_controller.rb +++ b/app/controllers/gantts_controller.rb @@ -1,8 +1,10 @@ class GanttsController < ApplicationController + menu_item :gantt before_filter :find_optional_project rescue_from Query::StatementInvalid, :with => :query_statement_invalid + helper :gantt helper :issues helper :projects helper :queries @@ -13,33 +15,22 @@ class GanttsController < ApplicationController def show @gantt = Redmine::Helpers::Gantt.new(params) + @gantt.project = @project retrieve_query @query.group_by = nil - if @query.valid? - events = [] - # Issues that have start and due dates - events += @query.issues(:include => [:tracker, :assigned_to, :priority], - :order => "start_date, due_date", - :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] - ) - # Issues that don't have a due date but that are assigned to a version with a date - events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version], - :order => "start_date, effective_date", - :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] - ) - # Versions - events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to]) - - @gantt.events = events - end + @gantt.query = @query if @query.valid? basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' respond_to do |format| format.html { render :action => "show", :layout => !request.xhr? } format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') - format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") } + format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") } end end + def update + show + end + end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 4bd732fc1..29e4e4b07 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -141,14 +141,22 @@ class GroupsController < ApplicationController @membership = Member.edit_membership(params[:membership_id], params[:membership], @group) @membership.save if request.post? respond_to do |format| - format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } - format.js { - render(:update) {|page| - page.replace_html "tab-content-memberships", :partial => 'groups/memberships' - page.visual_effect(:highlight, "member-#{@membership.id}") - } - } - end + if @membership.valid? + format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' } + format.js { + render(:update) {|page| + page.replace_html "tab-content-memberships", :partial => 'groups/memberships' + page.visual_effect(:highlight, "member-#{@membership.id}") + } + } + else + format.js { + render(:update) {|page| + page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', '))) + } + } + end + end end def destroy_membership diff --git a/app/controllers/issue_moves_controller.rb b/app/controllers/issue_moves_controller.rb new file mode 100644 index 000000000..20da38755 --- /dev/null +++ b/app/controllers/issue_moves_controller.rb @@ -0,0 +1,65 @@ +class IssueMovesController < ApplicationController + default_search_scope :issues + before_filter :find_issues, :check_project_uniqueness + before_filter :authorize + + def new + prepare_for_issue_move + render :layout => false if request.xhr? + end + + def create + prepare_for_issue_move + + if request.post? + new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) + unsaved_issue_ids = [] + moved_issues = [] + @issues.each do |issue| + issue.reload + issue.init_journal(User.current) + call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy }) + if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)}) + moved_issues << r + else + unsaved_issue_ids << issue.id + end + end + set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) + + if params[:follow] + if @issues.size == 1 && moved_issues.size == 1 + redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first + else + redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project) + end + else + redirect_to :controller => 'issues', :action => 'index', :project_id => @project + end + return + end + end + + private + + def prepare_for_issue_move + @issues.sort! + @copy = params[:copy_options] && params[:copy_options][:copy] + @allowed_projects = Issue.allowed_target_projects_on_move + @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 + @available_statuses = Workflow.available_statuses(@project) + end + + def extract_changed_attributes_for_move(params) + changed_attributes = {} + [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute| + unless params[valid_attribute].blank? + changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute]) + end + end + changed_attributes + end + +end diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 8b5d73fa3..67488b753 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -19,14 +19,15 @@ class IssuesController < ApplicationController menu_item :new_issue, :only => [:new, :create] default_search_scope :issues - before_filter :find_issue, :only => [:show, :edit, :update, :reply] - before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] - before_filter :find_project, :only => [:new, :create, :update_form, :preview, :auto_complete] - before_filter :authorize, :except => [:index, :changes, :preview, :context_menu] - before_filter :find_optional_project, :only => [:index, :changes] + before_filter :find_issue, :only => [:show, :edit, :update] + before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy] + before_filter :check_project_uniqueness, :only => [:move, :perform_move] + before_filter :find_project, :only => [:new, :create] + before_filter :authorize, :except => [:index] + before_filter :find_optional_project, :only => [:index] before_filter :check_for_default_issue_status, :only => [:new, :create] before_filter :build_new_issue_from_params, :only => [:new, :create] - accept_key_auth :index, :show, :changes + accept_key_auth :index, :show rescue_from Query::StatementInvalid, :with => :query_statement_invalid @@ -47,6 +48,7 @@ class IssuesController < ApplicationController include SortHelper include IssuesHelper helper :timelog + helper :gantt include Redmine::Export::PDF verify :method => [:post, :delete], @@ -54,6 +56,7 @@ class IssuesController < ApplicationController :render => { :nothing => true, :status => :method_not_allowed } verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } + verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed } verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } def index @@ -95,21 +98,6 @@ class IssuesController < ApplicationController render_404 end - def changes - retrieve_query - sort_init 'id', 'desc' - sort_update(@query.sortable_columns) - - if @query.valid? - @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC", - :limit => 25) - 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 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") @journals.each_with_index {|j,i| j.indice = i+1} @@ -124,7 +112,7 @@ class IssuesController < ApplicationController format.html { render :template => 'issues/show.rhtml' } format.xml { render :layout => false } format.json { render :text => @issue.to_json, :layout => false } - format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } + format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' } format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } end end @@ -132,7 +120,10 @@ class IssuesController < ApplicationController # Add a new issue # The new issue will be created from an existing one if copy_from parameter is given def new - render :action => 'new', :layout => !request.xhr? + respond_to do |format| + format.html { render :action => 'new', :layout => !request.xhr? } + format.js { render :partial => 'attributes' } + end end def create @@ -144,7 +135,7 @@ class IssuesController < ApplicationController call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) respond_to do |format| format.html { - redirect_to(params[:continue] ? { :action => 'new', :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } : + redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } : { :action => 'show', :id => @issue }) } format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) } @@ -200,98 +191,32 @@ class IssuesController < ApplicationController end 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 - # Replaces pre blocks with [...] - text = text.to_s.strip.gsub(%r{
((.|\s)*?)}m, '[...]') - content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " - content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" - - render(:update) { |page| - page.<< "$('notes').value = \"#{escape_javascript 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 @issues.sort! - if request.post? - attributes = (params[:issue] || {}).reject {|k,v| v.blank?} - attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} - attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values] - - unsaved_issue_ids = [] - @issues.each do |issue| - issue.reload - journal = issue.init_journal(User.current, params[:notes]) - issue.safe_attributes = attributes - call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) - unless issue.save - # Keep unsaved issue ids to display them in flash error - unsaved_issue_ids << issue.id - end - end - set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) - redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project}) - return - end - @available_statuses = Workflow.available_statuses(@project) - @custom_fields = @project.all_issue_custom_fields + @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w} + @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c} + @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a} + @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t} end - def move + def bulk_update @issues.sort! - @copy = params[:copy_options] && params[:copy_options][:copy] - @allowed_projects = Issue.allowed_target_projects_on_move - @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 - @available_statuses = Workflow.available_statuses(@project) - if request.post? - new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) - unsaved_issue_ids = [] - moved_issues = [] - @issues.each do |issue| - issue.reload - changed_attributes = {} - [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute| - unless params[valid_attribute].blank? - changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute]) - end - end - issue.init_journal(User.current) - call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy }) - if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes}) - moved_issues << r - else - unsaved_issue_ids << issue.id - end - end - set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) + attributes = parse_params_for_bulk_issue_attributes(params) - if params[:follow] - if @issues.size == 1 && moved_issues.size == 1 - redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first - else - redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project) - end - else - redirect_to :controller => 'issues', :action => 'index', :project_id => @project + unsaved_issue_ids = [] + @issues.each do |issue| + issue.reload + journal = issue.init_journal(User.current, params[:notes]) + issue.safe_attributes = attributes + call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) + unless issue.save + # Keep unsaved issue ids to display them in flash error + unsaved_issue_ids << issue.id end - return end - render :layout => false if request.xhr? + set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids) + redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project}) end def destroy @@ -319,82 +244,12 @@ class IssuesController < ApplicationController end @issues.each(&:destroy) respond_to do |format| - format.html { redirect_to :action => 'index', :project_id => @project } + format.html { redirect_back_or_default(:action => 'index', :project_id => @project) } format.xml { head :ok } format.json { head :ok } end 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) - @trackers = @project.trackers - end - - @priorities = IssuePriority.all.reverse - @statuses = IssueStatus.find(:all, :order => 'position') - @back = params[:back_url] || request.env['HTTP_REFERER'] - - render :layout => false - end - - def update_form - if params[:id].blank? - @issue = Issue.new - @issue.project = @project - else - @issue = @project.issues.visible.find(params[:id]) - end - @issue.attributes = params[:issue] - @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq - @priorities = IssuePriority.all - - render :partial => 'attributes' - end - - def preview - @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? - if @issue - @attachements = @issue.attachments - @description = params[:issue] && params[:issue][:description] - if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n") - @description = nil - end - @notes = params[:notes] - else - @description = (params[:issue] ? params[:issue][:description] : nil) - end - render :layout => false - end - - def auto_complete - @issues = [] - q = params[:q].to_s - if q.match(/^\d+$/) - @issues << @project.issues.visible.find_by_id(q.to_i) - end - unless q.blank? - @issues += @project.issues.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10) - end - render :layout => false - end - private def find_issue @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) @@ -403,22 +258,6 @@ private 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' - return false - end - rescue ActiveRecord::RecordNotFound - render_404 - end - def find_project project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] @project = Project.find(project_id) @@ -435,7 +274,7 @@ private @edit_allowed = User.current.allowed_to?(:edit_issues, @project) @time_entry = TimeEntry.new - @notes = params[:notes] + @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil) @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] @@ -448,9 +287,16 @@ private end # TODO: Refactor, lots of extra code in here + # TODO: Changing tracker on an existing issue should not trigger this def build_new_issue_from_params - @issue = Issue.new - @issue.copy_from(params[:copy_from]) if params[:copy_from] + if params[:id].blank? + @issue = Issue.new + @issue.copy_from(params[:copy_from]) if params[:copy_from] + @issue.project = @project + else + @issue = @project.issues.visible.find(params[:id]) + end + @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) @@ -460,7 +306,9 @@ private end if params[:issue].is_a?(Hash) @issue.safe_attributes = params[:issue] - @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project) + if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record? + @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] + end end @issue.author = User.current @issue.start_date ||= Date.today @@ -468,21 +316,17 @@ private @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true) end - def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids) - if unsaved_issue_ids.empty? - flash[:notice] = l(:notice_successful_update) unless issues.empty? - else - flash[:error] = l(:notice_failed_to_save_issues, - :count => unsaved_issue_ids.size, - :total => issues.size, - :ids => '#' + unsaved_issue_ids.join(', #')) - end - end - def check_for_default_issue_status if IssueStatus.default.nil? render_error l(:error_no_default_issue_status) return false end end + + def parse_params_for_bulk_issue_attributes(params) + attributes = (params[:issue] || {}).reject {|k,v| v.blank?} + attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'} + attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values] + attributes + end end diff --git a/app/controllers/journals_controller.rb b/app/controllers/journals_controller.rb index e9fe9099d..a3b1abde4 100644 --- a/app/controllers/journals_controller.rb +++ b/app/controllers/journals_controller.rb @@ -16,7 +16,54 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class JournalsController < ApplicationController - before_filter :find_journal + before_filter :find_journal, :only => [:edit] + before_filter :find_issue, :only => [:new] + before_filter :find_optional_project, :only => [:index] + accept_key_auth :index + + helper :issues + helper :queries + include QueriesHelper + helper :sort + include SortHelper + + def index + retrieve_query + sort_init 'id', 'desc' + sort_update(@query.sortable_columns) + + if @query.valid? + @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC", + :limit => 25) + 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 new + 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 + # Replaces pre blocks with [...] + text = text.to_s.strip.gsub(%r{
((.|\s)*?)}m, '[...]') + content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " + content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" + + render(:update) { |page| + page.<< "$('notes').value = \"#{escape_javascript content}\";" + page.show 'update' + page << "Form.Element.focus('notes');" + page << "Element.scrollTo('update');" + page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;" + } + end def edit if request.post? @@ -38,4 +85,12 @@ private rescue ActiveRecord::RecordNotFound render_404 end + + # TODO: duplicated in IssuesController + def find_issue + @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end end diff --git a/app/controllers/my_controller.rb b/app/controllers/my_controller.rb index f637b49b6..46747b334 100644 --- a/app/controllers/my_controller.rb +++ b/app/controllers/my_controller.rb @@ -54,7 +54,7 @@ class MyController < ApplicationController @pref = @user.pref if request.post? @user.attributes = params[:user] - @user.mail_notification = (params[:notification_option] == 'all') + @user.mail_notification = params[:notification_option] || 'only_my_events' @user.pref.attributes = params[:pref] @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') if @user.save @@ -66,12 +66,8 @@ class MyController < ApplicationController return end end - @notification_options = [[l(:label_user_mail_option_all), 'all'], - [l(:label_user_mail_option_none), 'none']] - # Only users that belong to more than 1 project can select projects for which they are notified - # Note that @user.membership.size would fail since AR ignores :include association option when doing a count - @notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1 - @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected') + @notification_options = @user.valid_notification_options + @notification_option = @user.mail_notification #? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected') end # Manage user's password diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 4d44158a4..eebc0ba02 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -18,10 +18,10 @@ class NewsController < ApplicationController default_search_scope :news model_object News - before_filter :find_model_object, :except => [:new, :index, :preview] - before_filter :find_project_from_association, :except => [:new, :index, :preview] - before_filter :find_project, :only => [:new, :preview] - before_filter :authorize, :except => [:index, :preview] + before_filter :find_model_object, :except => [:new, :create, :index] + before_filter :find_project_from_association, :except => [:new, :create, :index] + before_filter :find_project, :only => [:new, :create] + before_filter :authorize, :except => [:index] before_filter :find_optional_project, :only => :index accept_key_auth :index @@ -46,49 +46,38 @@ class NewsController < ApplicationController def new @news = News.new(:project => @project, :author => User.current) + end + + def create + @news = News.new(:project => @project, :author => User.current) if request.post? @news.attributes = params[:news] if @news.save flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'news', :action => 'index', :project_id => @project + else + render :action => 'new' end end end - + def edit - if request.post? and @news.update_attributes(params[:news]) + end + + def update + if request.put? and @news.update_attributes(params[:news]) flash[:notice] = l(:notice_successful_update) redirect_to :action => 'show', :id => @news - end - end - - def add_comment - @comment = Comment.new(params[:comment]) - @comment.author = User.current - if @news.comments << @comment - flash[:notice] = l(:label_comment_added) - redirect_to :action => 'show', :id => @news else - show - render :action => 'show' + render :action => 'edit' end end - def destroy_comment - @news.comments.find(params[:comment_id]).destroy - redirect_to :action => 'show', :id => @news - end - def destroy @news.destroy redirect_to :action => 'index', :project_id => @project end - def preview - @text = (params[:news] ? params[:news][:description] : nil) - render :partial => 'common/preview' - end - private def find_project @project = Project.find(params[:project_id]) diff --git a/app/controllers/previews_controller.rb b/app/controllers/previews_controller.rb new file mode 100644 index 000000000..612025cb8 --- /dev/null +++ b/app/controllers/previews_controller.rb @@ -0,0 +1,33 @@ +class PreviewsController < ApplicationController + before_filter :find_project + + def issue + @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? + if @issue + @attachements = @issue.attachments + @description = params[:issue] && params[:issue][:description] + if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n") + @description = nil + end + @notes = params[:notes] + else + @description = (params[:issue] ? params[:issue][:description] : nil) + end + render :layout => false + end + + def news + @text = (params[:news] ? params[:news][:description] : nil) + render :partial => 'common/preview' + end + + private + + def find_project + project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/app/controllers/project_enumerations_controller.rb b/app/controllers/project_enumerations_controller.rb new file mode 100644 index 000000000..0b15887fa --- /dev/null +++ b/app/controllers/project_enumerations_controller.rb @@ -0,0 +1,26 @@ +class ProjectEnumerationsController < ApplicationController + before_filter :find_project_by_project_id + before_filter :authorize + + def update + if request.put? && params[:enumerations] + Project.transaction do + params[:enumerations].each do |id, activity| + @project.update_or_create_time_entry_activity(id, activity) + end + end + flash[:notice] = l(:notice_successful_update) + end + + redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project + end + + def destroy + @project.time_entry_activities.each do |time_entry_activity| + time_entry_activity.destroy(time_entry_activity.parent) + end + flash[:notice] = l(:notice_successful_update) + redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project + end + +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 44071d214..c3efbfd9f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -17,24 +17,24 @@ class ProjectsController < ApplicationController 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 - before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ] - before_filter :find_optional_project, :only => :activity - before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ] - before_filter :authorize_global, :only => :add + before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ] + before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy] + before_filter :authorize_global, :only => [:new, :create] before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ] - accept_key_auth :activity, :index - - after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller| + accept_key_auth :index + + after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller| if controller.request.post? controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt' end end - + + # TODO: convert to PUT only + verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed } + helper :sort include SortHelper helper :custom_fields @@ -63,40 +63,45 @@ class ProjectsController < ApplicationController end end - # Add a new project - def add + def new @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @trackers = Tracker.all @project = Project.new(params[:project]) - if request.get? - @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? - @project.trackers = Tracker.all - @project.is_public = Setting.default_projects_public? - @project.enabled_module_names = Setting.default_projects_modules - else - @project.enabled_module_names = params[:enabled_modules] - if validate_parent_id && @project.save - @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') - # Add current user as a project member if he is not admin - unless User.current.admin? - r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first - m = Member.new(:user => User.current, :roles => [r]) - @project.members << m - end - respond_to do |format| - format.html { - flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'projects', :action => 'settings', :id => @project - } - format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) } - end - else - respond_to do |format| - format.html - format.xml { render :xml => @project.errors, :status => :unprocessable_entity } - end + + @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 = Setting.default_projects_modules + end + + def create + @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") + @trackers = Tracker.all + @project = Project.new(params[:project]) + + @project.enabled_module_names = params[:enabled_modules] + if validate_parent_id && @project.save + @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + # Add current user as a project member if he is not admin + unless User.current.admin? + r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first + m = Member.new(:user => User.current, :roles => [r]) + @project.members << m end - end + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_create) + redirect_to :controller => 'projects', :action => 'settings', :id => @project + } + format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) } + end + else + respond_to do |format| + format.html { render :action => 'new' } + format.xml { render :xml => @project.errors, :status => :unprocessable_entity } + end + end + end def copy @@ -120,13 +125,13 @@ class ProjectsController < ApplicationController if validate_parent_id && @project.copy(@source_project, :only => params[:only]) @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') flash[:notice] = l(:notice_successful_create) - redirect_to :controller => 'admin', :action => 'projects' + redirect_to :controller => 'projects', :action => 'settings' elsif !@project.new_record? # Project was created # But some objects were not copied due to validation failures # (eg. issues from disabled trackers) # TODO: inform about that - redirect_to :controller => 'admin', :action => 'projects' + redirect_to :controller => 'projects', :action => 'settings' end end end @@ -177,28 +182,27 @@ class ProjectsController < ApplicationController @wiki ||= @project.wiki end - # Edit @project def edit - if request.get? + end + + def update + @project.attributes = params[:project] + if validate_parent_id && @project.save + @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'settings', :id => @project + } + format.xml { head :ok } + end else - @project.attributes = params[:project] - if validate_parent_id && @project.save - @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id') - respond_to do |format| - format.html { - flash[:notice] = l(:notice_successful_update) - redirect_to :action => 'settings', :id => @project - } - format.xml { head :ok } - end - else - respond_to do |format| - format.html { - settings - render :action => 'settings' - } - format.xml { render :xml => @project.errors, :status => :unprocessable_entity } - end + respond_to do |format| + format.html { + settings + render :action => 'settings' + } + format.xml { render :xml => @project.errors, :status => :unprocessable_entity } end end end @@ -241,120 +245,6 @@ class ProjectsController < ApplicationController @project = nil end - def add_file - if request.post? - container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id])) - attachments = Attachment.attach_files(container, params[:attachments]) - render_attachment_warning_if_needed(container) - - if !attachments.empty? && Setting.notified_events.include?('file_added') - Mailer.deliver_attachments_added(attachments[:files]) - end - redirect_to :controller => 'projects', :action => 'list_files', :id => @project - return - end - @versions = @project.versions.sort - end - - def save_activities - if request.post? && params[:enumerations] - Project.transaction do - params[:enumerations].each do |id, activity| - @project.update_or_create_time_entry_activity(id, activity) - end - end - flash[:notice] = l(:notice_successful_update) - end - - redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project - end - - def reset_activities - @project.time_entry_activities.each do |time_entry_activity| - time_entry_activity.destroy(time_entry_activity.parent) - end - flash[:notice] = l(:notice_successful_update) - redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project - end - - def list_files - sort_init 'filename', 'asc' - sort_update 'filename' => "#{Attachment.table_name}.filename", - 'created_on' => "#{Attachment.table_name}.created_on", - 'size' => "#{Attachment.table_name}.filesize", - 'downloads' => "#{Attachment.table_name}.downloads" - - @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)] - @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse - render :layout => !request.xhr? - end - - def roadmap - @trackers = @project.trackers.find(:all, :order => 'position') - retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?}) - @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id] - - @versions = @project.shared_versions || [] - @versions += @project.rolled_up_versions.visible if @with_subprojects - @versions = @versions.uniq.sort - @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed] - - @issues_by_version = {} - unless @selected_tracker_ids.empty? - @versions.each do |version| - issues = version.fixed_issues.visible.find(:all, - :include => [:project, :status, :tracker, :priority], - :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids}, - :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id") - @issues_by_version[version] = issues - end - end - @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?} - end - - def activity - @days = Setting.activity_days_default.to_i - - if params[:from] - begin; @date_to = params[:from].to_date + 1; rescue; end - end - - @date_to ||= Date.today + 1 - @date_from = @date_to - @days - @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id])) - - @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, - :with_subprojects => @with_subprojects, - :author => @author) - @activity.scope_select {|t| !params["show_#{t}"].nil?} - @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty? - - events = @activity.events(@date_from, @date_to) - - if events.empty? || stale?(:etag => [events.first, User.current]) - respond_to do |format| - format.html { - @events_by_day = events.group_by(&:event_date) - render :layout => false if request.xhr? - } - format.atom { - title = l(:label_activity) - if @author - title = @author.name - elsif @activity.scope.size == 1 - title = l("label_#{@activity.scope.first.singularize}_plural") - end - render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") - } - end - end - - rescue ActiveRecord::RecordNotFound - render_404 - end - private def find_optional_project return true unless params[:id] @@ -364,14 +254,6 @@ private render_404 end - def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil) - 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 } - else - @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s } - end - end - # Validates parent_id param according to user's permissions # TODO: move it to Project model in a validation that depends on User.current def validate_parent_id diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 77e1da56b..2e904ec19 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -26,7 +26,7 @@ class SettingsController < ApplicationController end def edit - @notifiables = %w(issue_added issue_updated news_added document_added file_added message_posted wiki_content_added wiki_content_updated) + @notifiables = Redmine::Notifiable.all if request.post? && params[:settings] && params[:settings].is_a?(Hash) settings = (params[:settings] || {}).dup.symbolize_keys settings.each do |name, value| diff --git a/app/controllers/time_entry_reports_controller.rb b/app/controllers/time_entry_reports_controller.rb new file mode 100644 index 000000000..dd02ff8ff --- /dev/null +++ b/app/controllers/time_entry_reports_controller.rb @@ -0,0 +1,209 @@ +class TimeEntryReportsController < ApplicationController + menu_item :issues + before_filter :find_optional_project + before_filter :load_available_criterias + + helper :sort + include SortHelper + helper :issues + helper :timelog + include TimelogHelper + helper :custom_fields + include CustomFieldsHelper + + def report + @criterias = params[:criterias] || [] + @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} + @criterias.uniq! + @criterias = @criterias[0,3] + + @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' + + 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_condition = '' + + if @project.nil? + sql_condition = Project.allowed_to_condition(User.current, :view_time_entries) + elsif @issue.nil? + sql_condition = @project.project_condition(Setting.display_subprojects_issues?) + else + sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}" + end + + sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours" + sql << " FROM #{TimeEntry.table_name}" + sql << time_report_joins + sql << " WHERE" + sql << " (%s) AND" % sql_condition + sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)] + sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" + + @hours = ActiveRecord::Base.connection.select_all(sql) + + @hours.each do |row| + case @columns + when 'year' + row['year'] = row['tyear'] + when 'month' + 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 + + respond_to do |format| + format.html { render :layout => !request.xhr? } + format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') } + end + end + + private + + # TODO: duplicated in TimelogController + def find_optional_project + if !params[:issue_id].blank? + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + elsif !params[:project_id].blank? + @project = Project.find(params[:project_id]) + end + deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true) + end + + # Retrieves the date range based on predefined ranges or specific from/to param dates + # TODO: duplicated in TimelogController + 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.earilest_date_for_project(@project) || Date.today) + @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today) + end + + def load_available_criterias + @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", + :klass => IssueCategory, + :label => :field_category}, + 'member' => {:sql => "#{TimeEntry.table_name}.user_id", + :klass => User, + :label => :label_member}, + 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", + :klass => Tracker, + :label => :label_tracker}, + 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", + :klass => TimeEntryActivity, + :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 + + # Add list and boolean time entry activity custom fields + TimeEntryActivityCustomField.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 = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)", + :format => cf.field_format, + :label => cf.name} + end + + call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project }) + @available_criterias + end + + def time_report_joins + sql = '' + 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" + # TODO: rename hook + call_hook(:controller_timelog_time_report_joins, {:sql => sql} ) + sql + end + +end diff --git a/app/controllers/timelog_controller.rb b/app/controllers/timelog_controller.rb index 7addff78c..b9f8724a0 100644 --- a/app/controllers/timelog_controller.rb +++ b/app/controllers/timelog_controller.rb @@ -17,11 +17,10 @@ class TimelogController < ApplicationController menu_item :issues - before_filter :find_project, :authorize, :only => [:edit, :destroy] - before_filter :find_optional_project, :only => [:report, :details] - before_filter :load_available_criterias, :only => [:report] + before_filter :find_project, :authorize, :only => [:new, :create, :edit, :destroy] + before_filter :find_optional_project, :only => [:index] - verify :method => :post, :only => :destroy, :redirect_to => { :action => :details } + verify :method => :post, :only => :destroy, :redirect_to => { :action => :index } helper :sort include SortHelper @@ -30,84 +29,7 @@ class TimelogController < ApplicationController helper :custom_fields include CustomFieldsHelper - def report - @criterias = params[:criterias] || [] - @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria} - @criterias.uniq! - @criterias = @criterias[0,3] - - @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month' - - 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_condition = '' - - if @project.nil? - sql_condition = Project.allowed_to_condition(User.current, :view_time_entries) - elsif @issue.nil? - sql_condition = @project.project_condition(Setting.display_subprojects_issues?) - else - sql_condition = "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}" - end - - 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" % sql_condition - sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from), ActiveRecord::Base.connection.quoted_date(@to)] - sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on" - - @hours = ActiveRecord::Base.connection.select_all(sql) - - @hours.each do |row| - case @columns - when 'year' - row['year'] = row['tyear'] - when 'month' - 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 - - respond_to do |format| - format.html { render :layout => !request.xhr? } - format.csv { send_data(report_to_csv(@criterias, @periods, @hours), :type => 'text/csv; header=present', :filename => 'timelog.csv') } - end - end - - def details + def index sort_init 'spent_on', 'desc' sort_update 'spent_on' => 'spent_on', 'user' => 'user_id', @@ -163,6 +85,29 @@ class TimelogController < ApplicationController end end end + + def new + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) + @time_entry.attributes = params[:time_entry] + + call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) + render :action => 'edit' + end + + verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } + def create + @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today) + @time_entry.attributes = params[:time_entry] + + call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry }) + + if @time_entry.save + flash[:notice] = l(:notice_successful_update) + redirect_back_or_default :action => 'index', :project_id => @time_entry.project + else + render :action => 'edit' + end + end def edit (render_403; return) if @time_entry && !@time_entry.editable_by?(User.current) @@ -173,7 +118,7 @@ class TimelogController < ApplicationController if request.post? and @time_entry.save flash[:notice] = l(:notice_successful_update) - redirect_back_or_default :action => 'details', :project_id => @time_entry.project + redirect_back_or_default :action => 'index', :project_id => @time_entry.project return end end @@ -188,7 +133,7 @@ class TimelogController < ApplicationController end redirect_to :back rescue ::ActionController::RedirectBackError - redirect_to :action => 'details', :project_id => @time_entry.project + redirect_to :action => 'index', :project_id => @time_entry.project end private @@ -261,57 +206,8 @@ private end @from, @to = @to, @from if @from && @to && @from > @to - @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1 - @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) + @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today) + @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today) end - def load_available_criterias - @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", - :klass => IssueCategory, - :label => :field_category}, - 'member' => {:sql => "#{TimeEntry.table_name}.user_id", - :klass => User, - :label => :label_member}, - 'tracker' => {:sql => "#{Issue.table_name}.tracker_id", - :klass => Tracker, - :label => :label_tracker}, - 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id", - :klass => TimeEntryActivity, - :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 - - # Add list and boolean time entry activity custom fields - TimeEntryActivityCustomField.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 = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id)", - :format => cf.field_format, - :label => cf.name} - end - - call_hook(:controller_timelog_available_criterias, { :available_criterias => @available_criterias, :project => @project }) - @available_criterias - end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f19cd7831..66979d5e2 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -53,10 +53,8 @@ class UsersController < ApplicationController @user = User.find(params[:id]) @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.member_of?(membership.project)) - end + # show projects based on current user visibility + @memberships = @user.memberships.all(:conditions => Project.visible_by(User.current)) events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10) @events_by_day = events.group_by(&:event_date) @@ -73,64 +71,117 @@ class UsersController < ApplicationController render_404 end - def add - if request.get? - @user = User.new(:language => Setting.default_language) - 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 - if @user.save - Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] - flash[:notice] = l(:notice_successful_create) - redirect_to(params[:continue] ? {:controller => 'users', :action => 'add'} : - {:controller => 'users', :action => 'edit', :id => @user}) - return - end - end + def new + @notification_options = User::MAIL_NOTIFICATION_OPTIONS + @notification_option = Setting.default_notification_option + + @user = User.new(:language => Setting.default_language) @auth_sources = AuthSource.find(:all) end + + verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed } + def create + @notification_options = User::MAIL_NOTIFICATION_OPTIONS + @notification_option = Setting.default_notification_option + + @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 + + # TODO: Similar to My#account + @user.mail_notification = params[:notification_option] || 'only_my_events' + @user.pref.attributes = params[:pref] + @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') + + if @user.save + @user.pref.save + @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : []) + + Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] + flash[:notice] = l(:notice_successful_create) + redirect_to(params[:continue] ? {:controller => 'users', :action => 'new'} : + {:controller => 'users', :action => 'edit', :id => @user}) + return + else + @auth_sources = AuthSource.find(:all) + @notification_option = @user.mail_notification + + render :action => 'new' + end + end def edit @user = User.find(params[:id]) - 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 - @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids] - @user.attributes = params[:user] - # Was the account actived ? (do it before User#save clears the change) - was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE]) - if @user.save - if was_activated - Mailer.deliver_account_activated(@user) - elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil? - Mailer.deliver_account_information(@user, params[:password]) - end - flash[:notice] = l(:notice_successful_update) - redirect_to :back - end - end + @notification_options = @user.valid_notification_options + @notification_option = @user.mail_notification + @auth_sources = AuthSource.find(:all) @membership ||= Member.new + end + + verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed } + def update + @user = User.find(params[:id]) + @notification_options = @user.valid_notification_options + @notification_option = @user.mail_notification + + @user.admin = params[:user][:admin] if params[:user][:admin] + @user.login = params[:user][:login] if params[:user][:login] + if params[:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?) + @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] + end + @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids] + @user.attributes = params[:user] + # Was the account actived ? (do it before User#save clears the change) + was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE]) + # TODO: Similar to My#account + @user.mail_notification = params[:notification_option] || 'only_my_events' + @user.pref.attributes = params[:pref] + @user.pref[:no_self_notified] = (params[:no_self_notified] == '1') + + if @user.save + @user.pref.save + @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : []) + + if was_activated + Mailer.deliver_account_activated(@user) + elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil? + Mailer.deliver_account_information(@user, params[:password]) + end + flash[:notice] = l(:notice_successful_update) + redirect_to :back + else + @auth_sources = AuthSource.find(:all) + @membership ||= Member.new + + render :action => :edit + end rescue ::ActionController::RedirectBackError redirect_to :controller => 'users', :action => 'edit', :id => @user end - + def edit_membership @user = User.find(params[:id]) @membership = Member.edit_membership(params[:membership_id], params[:membership], @user) @membership.save if request.post? respond_to do |format| - format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } - format.js { - render(:update) {|page| - page.replace_html "tab-content-memberships", :partial => 'users/memberships' - page.visual_effect(:highlight, "member-#{@membership.id}") - } - } - end + if @membership.valid? + format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } + format.js { + render(:update) {|page| + page.replace_html "tab-content-memberships", :partial => 'users/memberships' + page.visual_effect(:highlight, "member-#{@membership.id}") + } + } + else + format.js { + render(:update) {|page| + page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', '))) + } + } + end + end end def destroy_membership diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index 05c9743eb..48612c7b8 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -18,15 +18,42 @@ class VersionsController < ApplicationController menu_item :roadmap model_object Version - before_filter :find_model_object, :except => [:new, :close_completed] - before_filter :find_project_from_association, :except => [:new, :close_completed] - before_filter :find_project, :only => [:new, :close_completed] + before_filter :find_model_object, :except => [:index, :new, :create, :close_completed] + before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed] + before_filter :find_project, :only => [:index, :new, :create, :close_completed] before_filter :authorize helper :custom_fields helper :projects + + def index + @trackers = @project.trackers.find(:all, :order => 'position') + retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?}) + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') + project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id] + + @versions = @project.shared_versions || [] + @versions += @project.rolled_up_versions.visible if @with_subprojects + @versions = @versions.uniq.sort + @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed] + + @issues_by_version = {} + unless @selected_tracker_ids.empty? + @versions.each do |version| + issues = version.fixed_issues.visible.find(:all, + :include => [:project, :status, :tracker, :priority], + :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids}, + :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id") + @issues_by_version[version] = issues + end + end + @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?} + end def show + @issues = @version.fixed_issues.visible.find(:all, + :include => [:status, :tracker, :priority], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") end def new @@ -36,6 +63,17 @@ class VersionsController < ApplicationController attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing']) @version.attributes = attributes end + end + + def create + # TODO: refactor with code above in #new + @version = @project.versions.build + if params[:version] + attributes = params[:version].dup + attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing']) + @version.attributes = attributes + end + if request.post? if @version.save respond_to do |format| @@ -52,7 +90,7 @@ class VersionsController < ApplicationController end else respond_to do |format| - format.html + format.html { render :action => 'new' } format.js do render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) } end @@ -60,9 +98,12 @@ class VersionsController < ApplicationController end end end - + def edit - if request.post? && params[:version] + end + + def update + if request.put? && params[:version] attributes = params[:version].dup attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing']) if @version.update_attributes(attributes) @@ -73,7 +114,7 @@ class VersionsController < ApplicationController end def close_completed - if request.post? + if request.put? @project.close_completed_versions end redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project @@ -102,4 +143,13 @@ private rescue ActiveRecord::RecordNotFound render_404 end + + def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil) + 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 } + else + @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s } + end + end + end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index b49a5674c..8f81f66ba 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -20,12 +20,4 @@ module AdminHelper options_for_select([[l(:label_all), ''], [l(:status_active), 1]], selected) end - - def css_project_classes(project) - s = 'project' - s << ' root' if project.root? - s << ' child' if project.child? - s << (project.leaf? ? ' leaf' : ' parent') - s - end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 054bac8af..6ba40eb45 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -32,8 +32,27 @@ module ApplicationHelper end # Display a link if user is authorized + # + # @param [String] name Anchor text (passed to link_to) + # @param [Hash, String] options Hash params or url for the link target (passed to link_to). + # This will checked by authorize_for to see if the user is authorized + # @param [optional, Hash] html_options Options passed to link_to + # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to 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]) + if options.is_a?(String) + begin + route = ActionController::Routing::Routes.recognize_path(options.gsub(/\?.*/,''), :method => options[:method] || :get) + link_controller = route[:controller] + link_action = route[:action] + rescue ActionController::RoutingError # Parse failed, not a route + link_controller, link_action = nil, nil + end + else + link_controller = options[:controller] || params[:controller] + link_action = options[:action] + end + + link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(link_controller, link_action) end # Display a link to remote if user is authorized @@ -102,6 +121,28 @@ module ApplicationHelper link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) end + + def link_to_project(project, options={}) + options[:class] ||= 'project' + link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => options[:class]) + end + + # Generates a link to a project if active + # Examples: + # + # link_to_project(project) # => link to the specified project overview + # link_to_project(project, :action=>'settings') # => link to project settings + # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options + # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) + # + def link_to_project(project, options={}, html_options = nil) + if project.active? + url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) + link_to(h(project), url, html_options) + else + h(project) + end + end def toggle_link(name, id, options={}) onclick = "Element.toggle('#{id}'); " @@ -285,7 +326,7 @@ module ApplicationHelper def time_tag(time) text = distance_of_time_in_words(Time.now, time) if @project - link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time)) + link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time)) else content_tag('acronym', text, :title => format_time(time)) end @@ -368,12 +409,12 @@ module ApplicationHelper ancestors = (@project.root? ? [] : @project.ancestors.visible) if ancestors.any? root = ancestors.shift - b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root') + b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') if ancestors.size > 2 b << '…' ancestors = ancestors[-2, 2] end - b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') } + b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } end b << h(@project) b.join(' » ') @@ -393,6 +434,19 @@ module ApplicationHelper end end + # Returns the theme, controller name, and action as css classes for the + # HTML body. + def body_css_classes + css = [] + if theme = Redmine::Themes.theme(Setting.ui_theme) + css << 'theme-' + theme.name + end + + css << 'controller-' + params[:controller] + css << 'action-' + params[:action] + css.join(' ') + end + def accesskey(s) Redmine::AccessKeys.key_for s end @@ -592,8 +646,7 @@ module ApplicationHelper end when 'project' if p = Project.visible.find_by_id(oid) - link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p}, - :class => 'project' + link = link_to_project(p, {:only_path => only_path}, :class => 'project') end end elsif sep == ':' @@ -635,8 +688,7 @@ module ApplicationHelper end when 'project' if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) - link = link_to h(p.name), {:only_path => only_path, :controller => 'projects', :action => 'show', :id => p}, - :class => 'project' + link = link_to_project(p, {:only_path => only_path}, :class => 'project') end end end @@ -709,6 +761,11 @@ module ApplicationHelper javascript_include_tag('context_menu') + stylesheet_link_tag('context_menu') end + if l(:direction) == 'rtl' + content_for :header_tags do + stylesheet_link_tag('context_menu_rtl') + end + end @context_menu_included = true end javascript_tag "new ContextMenu('#{ url_for(url) }')" @@ -772,7 +829,7 @@ module ApplicationHelper # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe