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 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 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 ') def avatar(user, options = { }) if Setting.gravatar_enabled? - options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default}) + options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default}) email = nil if user.respond_to?(:mail) email = user.mail @@ -780,9 +837,15 @@ module ApplicationHelper email = $1 end return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil + else + '' end end + def favicon + "" + end + private def wiki_helper diff --git a/app/helpers/calendars_helper.rb b/app/helpers/calendars_helper.rb new file mode 100644 index 000000000..08e665dcd --- /dev/null +++ b/app/helpers/calendars_helper.rb @@ -0,0 +1,45 @@ +module CalendarsHelper + def link_to_previous_month(year, month, options={}) + target_year, target_month = if month == 1 + [year - 1, 12] + else + [year, month - 1] + end + + name = if target_month == 12 + "#{month_name(target_month)} #{target_year}" + else + "#{month_name(target_month)}" + end + + link_to_month(('« ' + name), target_year, target_month, options) + end + + def link_to_next_month(year, month, options={}) + target_year, target_month = if month == 12 + [year + 1, 1] + else + [year, month + 1] + end + + name = if target_month == 1 + "#{month_name(target_month)} #{target_year}" + else + "#{month_name(target_month)}" + end + + link_to_month((name + ' »'), target_year, target_month, options) + end + + def link_to_month(link_name, year, month, options={}) + project_id = options[:project].present? ? options[:project].to_param : nil + + link_target = calendar_path(:year => year, :month => month, :project_id => project_id) + + link_to_remote(link_name, + {:update => "content", :url => link_target, :method => :put}, + {:href => link_target}) + + end + +end diff --git a/app/helpers/gantt_helper.rb b/app/helpers/gantt_helper.rb new file mode 100644 index 000000000..38f3765e9 --- /dev/null +++ b/app/helpers/gantt_helper.rb @@ -0,0 +1,24 @@ +# redMine - project management software +# Copyright (C) 2006 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module GanttHelper + def number_of_issues_on_versions(gantt) + versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact + + versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count} + end +end diff --git a/app/helpers/issue_moves_helper.rb b/app/helpers/issue_moves_helper.rb new file mode 100644 index 000000000..b58b4ce5a --- /dev/null +++ b/app/helpers/issue_moves_helper.rb @@ -0,0 +1,2 @@ +module IssueMovesHelper +end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 60798fedf..d84ec6c97 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -28,14 +28,27 @@ module IssuesHelper ancestors << issue unless issue.leaf? end end - + + # Renders a HTML/CSS tooltip + # + # To use, a trigger div is needed. This is a div with the class of "tooltip" + # that contains this method wrapped in a span with the class of "tip" + # + #
<%= link_to_issue(issue) %> + # <%= render_issue_tooltip(issue) %> + #
+ # def render_issue_tooltip(issue) + @cached_label_status ||= l(:field_status) @cached_label_start_date ||= l(:field_start_date) @cached_label_due_date ||= l(:field_due_date) @cached_label_assigned_to ||= l(:field_assigned_to) @cached_label_priority ||= l(:field_priority) - + @cached_label_project ||= l(:field_project) + link_to_issue(issue) + "

" + + "#{@cached_label_project}: #{link_to_project(issue.project)}
" + + "#{@cached_label_status}: #{issue.status.name}
" + "#{@cached_label_start_date}: #{format_date(issue.start_date)}
" + "#{@cached_label_due_date}: #{format_date(issue.due_date)}
" + "#{@cached_label_assigned_to}: #{issue.assigned_to}
" + @@ -241,7 +254,7 @@ module IssuesHelper when :in if gantt.zoom < 4 link_to_remote(l(:text_zoom_in) + image_tag('zoom_in.png', img_attributes.merge(:alt => l(:text_zoom_in))), - {:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :update => 'content'}, + {:url => gantt.params.merge(:zoom => (gantt.zoom+1)), :method => :get, :update => 'content'}, {:href => url_for(gantt.params.merge(:zoom => (gantt.zoom+1)))}) else l(:text_zoom_in) + @@ -251,7 +264,7 @@ module IssuesHelper when :out if gantt.zoom > 1 link_to_remote(l(:text_zoom_out) + image_tag('zoom_out.png', img_attributes.merge(:alt => l(:text_zoom_out))), - {:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :update => 'content'}, + {:url => gantt.params.merge(:zoom => (gantt.zoom-1)), :method => :get, :update => 'content'}, {:href => url_for(gantt.params.merge(:zoom => (gantt.zoom-1)))}) else l(:text_zoom_out) + diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index cf8772430..c8d53f253 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -22,7 +22,7 @@ module JournalsHelper links = [] if !journal.notes.blank? links << link_to_remote(image_tag('comment.png'), - { :url => {:controller => 'issues', :action => 'reply', :id => issue, :journal_id => journal} }, + { :url => {:controller => 'journals', :action => 'new', :id => issue, :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 }, diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 044ccfb77..3b089c111 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -72,7 +72,7 @@ module ProjectsHelper end classes = (ancestors.empty? ? 'root' : 'child') s << "
  • " + - link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") + link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") s << "
    #{textilizable(project.short_description, :project => project)}
    " unless project.description.blank? s << "
    \n" ancestors << project diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 594c8a79a..26be63693 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -50,7 +50,7 @@ module QueriesHelper when 'User' link_to_user value when 'Project' - link_to(h(value), :controller => 'projects', :action => 'show', :id => value) + link_to_project value when 'Version' link_to(h(value), :controller => 'versions', :action => 'show', :id => value) when 'TrueClass' diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index e57b75fcc..6dc33a8e1 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -71,4 +71,14 @@ module SettingsHelper label = options.delete(:label) label != false ? content_tag("label", l(label || "setting_#{setting}")) : '' end + + # Renders a notification field for a Redmine::Notifiable option + def notification_field(notifiable) + return content_tag(:label, + check_box_tag('settings[notified_events][]', + notifiable.name, + Setting.notified_events.include?(notifiable.name)) + + l_or_humanize(notifiable.name, :prefix => 'label_'), + :class => notifiable.parent.present? ? "parent" : '') + end end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 757a91a9f..37cecc05c 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -34,14 +34,14 @@ module UsersHelper end def change_status_link(user) - url = {:controller => 'users', :action => 'edit', :id => user, :page => params[:page], :status => params[:status], :tab => nil} + url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil} if user.locked? - link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock' + link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :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' + link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :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' + link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock' end end diff --git a/app/models/change.rb b/app/models/change.rb index e5c1585b4..657652c9d 100644 --- a/app/models/change.rb +++ b/app/models/change.rb @@ -19,12 +19,13 @@ class Change < ActiveRecord::Base belongs_to :changeset validates_presence_of :changeset_id, :action, :path + before_save :init_path def relative_path changeset.repository.relative_path(path) end - def before_save - path ||= "" + def init_path + self.path ||= "" end end diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 3bd26b111..063a4a48c 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -76,7 +76,6 @@ class Changeset < ActiveRecord::Base def after_create scan_comment_for_issue_ids end - require 'pp' def scan_comment_for_issue_ids return if comments.blank? diff --git a/app/models/issue.rb b/app/models/issue.rb index 7d0682df1..3fbbb4513 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -62,10 +62,28 @@ class Issue < ActiveRecord::Base named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status - named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC" + named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" named_scope :with_limit, lambda { |limit| { :limit => limit} } named_scope :on_active_project, :include => [:status, :project, :tracker], :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] + named_scope :for_gantt, lambda { + { + :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], + :order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC" + } + } + + named_scope :without_version, lambda { + { + :conditions => { :fixed_version_id => nil} + } + } + + named_scope :with_query, lambda {|query| + { + :conditions => Query.merge_conditions(query.statement) + } + } before_create :default_assign before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status @@ -245,7 +263,7 @@ class Issue < ActiveRecord::Base end def done_ratio - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio? + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio status.default_done_ratio else read_attribute(:done_ratio) @@ -308,7 +326,7 @@ class Issue < ActiveRecord::Base # Set the done_ratio using the status if that setting is set. This will keep the done_ratios # even if the user turns off the setting later def update_done_ratio_from_issue_status - if Issue.use_status_for_done_ratio? && status && status.default_done_ratio? + if Issue.use_status_for_done_ratio? && status && status.default_done_ratio self.done_ratio = status.default_done_ratio end end @@ -357,10 +375,24 @@ class Issue < ActiveRecord::Base def overdue? !due_date.nil? && (due_date < Date.today) && !status.is_closed? end + + # Is the amount of work done less than it should for the due date + def behind_schedule? + return false if start_date.nil? || due_date.nil? + done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor + return done_date <= Date.today + end + + # Does this issue have children? + def children? + !leaf? + end # Users the issue can be assigned to def assignable_users - project.assignable_users + users = project.assignable_users + users << author if author + users.uniq.sort end # Versions that the issue can be assigned to @@ -385,9 +417,10 @@ class Issue < ActiveRecord::Base # Returns the mail adresses of users that should be notified def recipients notified = project.notified_users - # Author and assignee are always notified unless they have been locked - notified << author if author && author.active? - notified << assigned_to if assigned_to && assigned_to.active? + # Author and assignee are always notified unless they have been + # locked or don't want to be notified + notified << author if author && author.active? && author.notify_about?(self) + notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self) notified.uniq! # Remove users that can not view the issue notified.reject! {|user| !visible?(user)} @@ -684,7 +717,7 @@ class Issue < ActiveRecord::Base end # done ratio = weighted average ratio of leaves - unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio? + unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio leaves_count = p.leaves.count if leaves_count > 0 average = p.leaves.average(:estimated_hours).to_f @@ -821,7 +854,7 @@ class Issue < ActiveRecord::Base j.id as #{select_field}, count(i.id) as total from - #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j + #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j where i.status_id=s.id and #{where} diff --git a/app/models/issue_status.rb b/app/models/issue_status.rb index fdda12a8c..f376d5d15 100644 --- a/app/models/issue_status.rb +++ b/app/models/issue_status.rb @@ -17,8 +17,10 @@ class IssueStatus < ActiveRecord::Base before_destroy :check_integrity - has_many :workflows, :foreign_key => "old_status_id", :dependent => :delete_all + has_many :workflows, :foreign_key => "old_status_id" acts_as_list + + before_destroy :delete_workflows validates_presence_of :name validates_uniqueness_of :name @@ -89,4 +91,9 @@ private def check_integrity raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id]) end + + # Deletes associated workflows + def delete_workflows + Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}]) + end end diff --git a/app/models/journal.rb b/app/models/journal.rb index a0e1ae877..3e846aeb8 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -65,4 +65,12 @@ class Journal < ActiveRecord::Base def attachments journalized.respond_to?(:attachments) ? journalized.attachments : nil end + + # Returns a string of css classes + def css_classes + s = 'journal' + s << ' has-notes' unless notes.blank? + s << ' has-details' unless details.blank? + s + end end diff --git a/app/models/journal_observer.rb b/app/models/journal_observer.rb index 5604e064e..db7115cdb 100644 --- a/app/models/journal_observer.rb +++ b/app/models/journal_observer.rb @@ -17,6 +17,11 @@ class JournalObserver < ActiveRecord::Observer def after_create(journal) - Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') + if Setting.notified_events.include?('issue_updated') || + (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) || + (Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) || + (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) + Mailer.deliver_issue_edit(journal) + end end end diff --git a/app/models/mailer.rb b/app/models/mailer.rb index f39c53209..6db5a997b 100644 --- a/app/models/mailer.rb +++ b/app/models/mailer.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 'ar_condition' + class Mailer < ActionMailer::Base layout 'mailer' helper :application @@ -80,7 +82,7 @@ class Mailer < ActionMailer::Base def reminder(user, issues, days) set_language_if_valid user.language recipients user.mail - subject l(:mail_subject_reminder, issues.size) + subject l(:mail_subject_reminder, :count => issues.size, :days => days) body :issues => issues, :days => days, :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc') @@ -306,13 +308,16 @@ class Mailer < ActionMailer::Base # * :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) + # * :users => array of user ids who should be reminded def self.reminders(options={}) days = options[:days] || 7 project = options[:project] ? Project.find(options[:project]) : nil tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil + user_ids = options[:users] 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 << ["#{Issue.table_name}.assigned_to_id IN (?)", user_ids] if user_ids.present? 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 diff --git a/app/models/member.rb b/app/models/member.rb index 94751efb2..d840cfc3e 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -82,7 +82,7 @@ class Member < ActiveRecord::Base protected def validate - errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty? + errors.add_on_empty :role if member_roles.empty? && roles.empty? end private diff --git a/app/models/principal.rb b/app/models/principal.rb index 58c3f0497..b3e07dda5 100644 --- a/app/models/principal.rb +++ b/app/models/principal.rb @@ -33,7 +33,11 @@ class Principal < ActiveRecord::Base } before_create :set_default_empty_values - + + def name(formatter = nil) + to_s + end + def <=>(principal) if self.class.name == principal.class.name self.to_s.downcase <=> principal.to_s.downcase diff --git a/app/models/project.rb b/app/models/project.rb index 931f89b55..0bb67e420 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -382,7 +382,7 @@ class Project < ActiveRecord::Base # Returns the mail adresses of users that should be always notified on project events def recipients - members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail} + members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user.mail} end # Returns the users that should be notified on project events @@ -412,6 +412,58 @@ class Project < ActiveRecord::Base def short_description(length = 255) description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description end + + def css_classes + s = 'project' + s << ' root' if root? + s << ' child' if child? + s << (leaf? ? ' leaf' : ' parent') + s + end + + # The earliest start date of a project, based on it's issues and versions + def start_date + if module_enabled?(:issue_tracking) + [ + issues.minimum('start_date'), + shared_versions.collect(&:effective_date), + shared_versions.collect {|v| v.fixed_issues.minimum('start_date')} + ].flatten.compact.min + end + end + + # The latest due date of an issue or version + def due_date + if module_enabled?(:issue_tracking) + [ + issues.maximum('due_date'), + shared_versions.collect(&:effective_date), + shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} + ].flatten.compact.max + end + end + + def overdue? + active? && !due_date.nil? && (due_date < Date.today) + end + + # Returns the percent completed for this project, based on the + # progress on it's versions. + def completed_percent(options={:include_subprojects => false}) + if options.delete(:include_subprojects) + total = self_and_descendants.collect(&:completed_percent).sum + + total / self_and_descendants.count + else + if versions.count > 0 + total = versions.collect(&:completed_pourcent).sum + + total / versions.count + else + 100 + end + end + end # Return true if this project is allowed to do the specified action. # action can be: @@ -441,6 +493,15 @@ class Project < ActiveRecord::Base enabled_modules.clear end end + + # Returns an array of projects that are in this project's hierarchy + # + # Example: parents, children, siblings + def hierarchy + parents = project.self_and_ancestors || [] + descendants = project.descendants || [] + project_hierarchy = parents | descendants # Set union + end # Returns an auto-generated project identifier based on the last identifier used def self.next_identifier diff --git a/app/models/query.rb b/app/models/query.rb index f697a721d..59131afcd 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -187,7 +187,7 @@ class Query < ActiveRecord::Base if project user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] } else - project_ids = User.current.projects.collect(&:id) + project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id) if project_ids.any? # members of the user's projects user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] } @@ -195,6 +195,12 @@ class Query < ActiveRecord::Base 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? + + group_values = Group.all.collect {|g| [g.name, g.id] } + @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? + + role_values = Role.givable.collect {|r| [r.name, r.id] } + @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? if User.current.logged? @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } @@ -219,6 +225,12 @@ class Query < ActiveRecord::Base @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } } end add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) + # project filter + project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p| + pre = (p.level > 0 ? ('--' * p.level + ' ') : '') + ["#{pre}#{p.name}",p.id.to_s] + end + @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} end @available_filters end @@ -426,6 +438,47 @@ class Query < ActiveRecord::Base db_field = 'user_id' sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " sql << sql_for_field(field, '=', v, db_table, db_field) + ')' + elsif field == "member_of_group" # named field + if operator == '*' # Any group + groups = Group.all + operator = '=' # Override the operator since we want to find by assigned_to + elsif operator == "!*" + groups = Group.all + operator = '!' # Override the operator since we want to find by assigned_to + else + groups = Group.find_all_by_id(v) + end + groups ||= [] + + members_of_groups = groups.inject([]) {|user_ids, group| + if group && group.user_ids.present? + user_ids << group.user_ids + end + user_ids.flatten.uniq.compact + }.sort.collect(&:to_s) + + sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' + + elsif field == "assigned_to_role" # named field + if operator == "*" # Any Role + roles = Role.givable + operator = '=' # Override the operator since we want to find by assigned_to + elsif operator == "!*" # No role + roles = Role.givable + operator = '!' # Override the operator since we want to find by assigned_to + else + roles = Role.givable.find_all_by_id(v) + end + roles ||= [] + + members_of_roles = roles.inject([]) {|user_ids, role| + if role && role.members + user_ids << role.members.collect(&:user_id) + end + user_ids.flatten.uniq.compact + }.sort.collect(&:to_s) + + sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')' else # regular field db_table = Issue.table_name diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index 73f39f949..9bf970891 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -81,4 +81,20 @@ class TimeEntry < ActiveRecord::Base yield end end + + def self.earilest_date_for_project(project=nil) + finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries)) + if project + finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)] + end + TimeEntry.minimum(:spent_on, :include => :project, :conditions => finder_conditions.conditions) + end + + def self.latest_date_for_project(project=nil) + finder_conditions = ARCondition.new(Project.allowed_to_condition(User.current, :view_time_entries)) + if project + finder_conditions << ["project_id IN (?)", project.hierarchy.collect(&:id)] + end + TimeEntry.maximum(:spent_on, :include => :project, :conditions => finder_conditions.conditions) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 8148ae3a9..a43631932 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -33,6 +33,15 @@ class User < Principal :username => '#{login}' } + MAIL_NOTIFICATION_OPTIONS = [ + [:all, :label_user_mail_option_all], + [:selected, :label_user_mail_option_selected], + [:none, :label_user_mail_option_none], + [:only_my_events, :label_user_mail_option_only_my_events], + [:only_assigned, :label_user_mail_option_only_assigned], + [:only_owner, :label_user_mail_option_only_owner] + ] + has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, :after_remove => Proc.new {|user, group| group.user_removed(user)} has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify @@ -65,7 +74,7 @@ class User < Principal validates_confirmation_of :password, :allow_nil => true def before_create - self.mail_notification = false + self.mail_notification = Setting.default_notification_option if self.mail_notification.blank? true end @@ -79,6 +88,10 @@ class User < Principal super end + def mail=(arg) + write_attribute(:mail, arg.to_s.strip) + end + def identity_url=(url) if url.blank? write_attribute(:identity_url, '') @@ -160,6 +173,30 @@ class User < Principal self.status == STATUS_LOCKED end + def activate + self.status = STATUS_ACTIVE + end + + def register + self.status = STATUS_REGISTERED + end + + def lock + self.status = STATUS_LOCKED + end + + def activate! + update_attribute(:status, STATUS_ACTIVE) + end + + def register! + update_attribute(:status, STATUS_REGISTERED) + end + + def lock! + update_attribute(:status, STATUS_LOCKED) + end + def check_password?(clear_password) if auth_source_id.present? auth_source.authenticate(self.login, clear_password) @@ -222,6 +259,17 @@ class User < Principal notified_projects_ids end + # Only users that belong to more than 1 project can select projects for which they are notified + def valid_notification_options + # Note that @user.membership.size would fail since AR ignores + # :include association option when doing a count + if memberships.length < 1 + MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == :selected} + else + MAIL_NOTIFICATION_OPTIONS + end + end + # Find a user account by matching the exact login and then a case-insensitive # version. Exact matches will be given priority. def self.find_by_login(login) @@ -296,23 +344,35 @@ class User < Principal !roles_for_project(project).detect {|role| role.member?}.nil? end - # Return true if the user is allowed to do the specified action on project - # action can be: + # Return true if the user is allowed to do the specified action on a specific context + # Action can be: # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') # * a permission Symbol (eg. :edit_project) - def allowed_to?(action, project, options={}) - if project + # Context can be: + # * a project : returns true if user is allowed to do the specified action on this project + # * a group of projects : returns true if user is allowed on every project + # * nil with options[:global] set : check if user has at least one role allowed for this action, + # or falls back to Non Member / Anonymous permissions depending if the user is logged + def allowed_to?(action, context, options={}) + if context && context.is_a?(Project) # No action allowed on archived projects - return false unless project.active? + return false unless context.active? # No action allowed on disabled modules - return false unless project.allows_to?(action) + return false unless context.allows_to?(action) # Admin users are authorized for anything else return true if admin? - roles = roles_for_project(project) + roles = roles_for_project(context) return false unless roles - roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)} + roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)} + elsif context && context.is_a?(Array) + # Authorize if user is authorized on every element of the array + context.map do |project| + allowed_to?(action,project,options) + end.inject do |memo,allowed| + memo && allowed + end elsif options[:global] # Admin users are always authorized return true if admin? @@ -324,6 +384,47 @@ class User < Principal false end end + + # Is the user allowed to do the specified action on any project? + # See allowed_to? for the actions and valid options. + def allowed_to_globally?(action, options) + allowed_to?(action, nil, options.reverse_merge(:global => true)) + end + + # Utility method to help check if a user should be notified about an + # event. + # + # TODO: only supports Issue events currently + def notify_about?(object) + case mail_notification.to_sym + when :all + true + when :selected + # Handled by the Project + when :none + false + when :only_my_events + if object.is_a?(Issue) && (object.author == self || object.assigned_to == self) + true + else + false + end + when :only_assigned + if object.is_a?(Issue) && object.assigned_to == self + true + else + false + end + when :only_owner + if object.is_a?(Issue) && object.author == self + true + else + false + end + else + false + end + end def self.current=(user) @current_user = user diff --git a/app/models/version.rb b/app/models/version.rb index 07e66434d..95e6ad5f6 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -73,6 +73,18 @@ class Version < ActiveRecord::Base def completed? effective_date && (effective_date <= Date.today) && (open_issues_count == 0) end + + def behind_schedule? + if completed_pourcent == 100 + return false + elsif due_date && fixed_issues.present? && fixed_issues.minimum('start_date') # TODO: should use #start_date but that method is wrong... + start_date = fixed_issues.minimum('start_date') + done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor + return done_date <= Date.today + else + false # No issues so it's not late + end + end # Returns the completion percentage of this version based on the amount of open/closed issues # and the time spent on the open issues. @@ -123,6 +135,10 @@ class Version < ActiveRecord::Base end def to_s; name end + + def to_s_with_project + "#{project} - #{name}" + end # Versions are sorted by effective_date and "Project Name - Version name" # Those with no effective_date are at the end, sorted by "Project Name - Version name" diff --git a/app/views/projects/activity.rhtml b/app/views/activities/index.html.erb similarity index 100% rename from app/views/projects/activity.rhtml rename to app/views/activities/index.html.erb diff --git a/app/views/admin/_menu.rhtml b/app/views/admin/_menu.rhtml index 4fc08d888..bd3abebe1 100644 --- a/app/views/admin/_menu.rhtml +++ b/app/views/admin/_menu.rhtml @@ -1,20 +1,5 @@
    -
      -
    • <%= link_to l(:label_project_plural), {:controller => 'admin', :action => 'projects'}, :class => 'projects' %>
    • -
    • <%= link_to l(:label_user_plural), {:controller => 'users'}, :class => 'users' %>
    • -
    • <%= link_to l(:label_group_plural), {:controller => 'groups'}, :class => 'groups' %>
    • -
    • <%= link_to l(:label_ldap_authentication), {:controller => 'ldap_auth_sources', :action => 'index'}, :class => 'server_authentication' %>
    • -
    • <%= link_to l(:label_role_and_permissions), {:controller => 'roles'}, :class => 'roles' %>
    • -
    • <%= link_to l(:label_tracker_plural), {:controller => 'trackers'}, :class => 'trackers' %>
    • -
    • <%= link_to l(:label_issue_status_plural), {:controller => 'issue_statuses'}, :class => 'issue_statuses' %>
    • -
    • <%= link_to l(:label_workflow), {:controller => 'workflows', :action => 'edit'}, :class => 'workflows' %>
    • -
    • <%= link_to l(:label_custom_field_plural), {:controller => 'custom_fields'}, :class => 'custom_fields' %>
    • -
    • <%= link_to l(:label_enumerations), {:controller => 'enumerations'}, :class => 'enumerations' %>
    • -
    • <%= link_to l(:label_settings), {:controller => 'settings'}, :class => 'settings' %>
    • - <% menu_items_for(:admin_menu) do |item| -%> -
    • <%= link_to h(item.caption), item.url, item.html_options %>
    • - <% end -%> -
    • <%= link_to l(:label_plugins), {:controller => 'admin', :action => 'plugins'}, :class => 'plugins' %>
    • -
    • <%= link_to l(:label_information_plural), {:controller => 'admin', :action => 'info'}, :class => 'info' %>
    • -
    +
      + <%= render_menu :admin_menu %> +
    diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index dc7bb97ed..47a2d0583 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -1,5 +1,5 @@
    -<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add' %> +<%= link_to l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add' %>

    <%=l(:label_project_plural)%>

    @@ -26,8 +26,8 @@ <% project_tree(@projects) do |project, level| %> - <%= css_project_classes(project) %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> - <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> + <%= project.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> + <%= link_to_project(project, :action => 'settings') %> <%= textilizable project.short_description, :project => project %> <%= checked_image project.is_public? %> <%= format_date(project.created_on) %> @@ -35,7 +35,7 @@ <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project, :status => params[:status] }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> <%= link_to(l(:button_unarchive), { :controller => 'projects', :action => 'unarchive', :id => project, :status => params[:status] }, :method => :post, :class => 'icon icon-unlock') if !project.active? && (project.parent.nil? || project.parent.active?) %> <%= link_to(l(:button_copy), { :controller => 'projects', :action => 'copy', :id => project }, :class => 'icon icon-copy') %> - <%= link_to(l(:button_delete), { :controller => 'projects', :action => 'destroy', :id => project }, :class => 'icon icon-del') %> + <%= link_to(l(:button_delete), project_destroy_confirm_path(project), :class => 'icon icon-del') %> <% end %> diff --git a/app/views/attachments/_form.rhtml b/app/views/attachments/_form.rhtml index 7702f92e2..6d387d1c2 100644 --- a/app/views/attachments/_form.rhtml +++ b/app/views/attachments/_form.rhtml @@ -1,6 +1,5 @@ -<%= file_field_tag 'attachments[1][file]', :size => 30, :id => nil -%> -
    diff --git a/app/views/issues/auto_complete.html.erb b/app/views/auto_completes/issues.html.erb similarity index 100% rename from app/views/issues/auto_complete.html.erb rename to app/views/auto_completes/issues.html.erb diff --git a/app/views/boards/index.rhtml b/app/views/boards/index.rhtml index 7cc6a0e2f..6310f942e 100644 --- a/app/views/boards/index.rhtml +++ b/app/views/boards/index.rhtml @@ -30,11 +30,11 @@ <% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_messages => 1, :key => User.current.rss_key} %> + <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_messages => 1, :key => User.current.rss_key} %> <% end %> <% 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}) %> + <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %> <% end %> <% html_title l(:label_board_plural) %> diff --git a/app/views/calendars/show.html.erb b/app/views/calendars/show.html.erb index 4541cc0c8..4c93771ff 100644 --- a/app/views/calendars/show.html.erb +++ b/app/views/calendars/show.html.erb @@ -1,6 +1,7 @@

    <%= l(:label_calendar) %>

    -<% form_tag({}, :id => 'query_form') do %> +<% form_tag(calendar_path, :method => :put, :id => 'query_form') do %> + <%= hidden_field_tag('project_id', @project.to_param) if @project%>
    <%= l(:label_filter_plural) %>
    @@ -9,14 +10,7 @@

    -<%= link_to_remote ('« ' + (@month==1 ? "#{month_name(12)} #{@year-1}" : "#{month_name(@month-1)}")), - {:update => "content", :url => { :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1) }}, - {:href => url_for(:action => 'show', :year => (@month==1 ? @year-1 : @year), :month =>(@month==1 ? 12 : @month-1))} - %> | -<%= link_to_remote ((@month==12 ? "#{month_name(1)} #{@year+1}" : "#{month_name(@month+1)}") + ' »'), - {:update => "content", :url => { :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1) }}, - {:href => url_for(:action => 'show', :year => (@month==12 ? @year+1 : @year), :month =>(@month==12 ? 1 : @month+1))} - %> + <%= link_to_previous_month(@year, @month, :project => @project) %> | <%= link_to_next_month(@year, @month, :project => @project) %>

    @@ -32,7 +26,8 @@ }, :class => 'icon icon-checked' %> <%= link_to_remote l(:button_clear), - { :url => { :set_filter => (@query.new_record? ? 1 : nil) }, + { :url => { :project_id => @project, :set_filter => (@query.new_record? ? 1 : nil) }, + :method => :put, :update => "content", }, :class => 'icon icon-reload' if @query.new_record? %>

    @@ -43,9 +38,9 @@ <%= render :partial => 'common/calendar', :locals => {:calendar => @calendar} %>

    - <%= l(:text_tip_task_begin_day) %> - <%= l(:text_tip_task_end_day) %> - <%= l(:text_tip_task_begin_end_day) %> + <%= l(:text_tip_issue_begin_day) %> + <%= l(:text_tip_issue_end_day) %> + <%= l(:text_tip_issue_begin_end_day) %>

    <% end %> diff --git a/app/views/issues/context_menu.rhtml b/app/views/context_menus/issues.html.erb similarity index 86% rename from app/views/issues/context_menu.rhtml rename to app/views/context_menus/issues.html.erb index e8d67ce41..3109ac4dd 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/context_menus/issues.html.erb @@ -4,20 +4,23 @@ <% if !@issue.nil? -%>
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue}, :class => 'icon-edit', :disabled => !@can[:edit] %>
  • -
  • - <%= l(:field_status) %> - -
  • <% else %>
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, :class => 'icon-edit', :disabled => !@can[:edit] %>
  • <% end %> + <% unless @allowed_statuses.empty? %> +
  • + <%= l(:field_status) %> + +
  • + <% end %> + <% unless @trackers.nil? %>
  • <%= l(:field_tracker) %> @@ -29,15 +32,18 @@
  • <% end %> +
  • <%= l(:field_priority) %>
  • + + <% #TODO: allow editing versions when multiple projects %> <% unless @project.nil? || @project.shared_versions.open.empty? -%>
  • <%= l(:field_fixed_version) %> @@ -77,17 +83,19 @@
  • <% end -%> + <% if Issue.use_field_for_done_ratio? %>
  • <%= l(:field_done_ratio) %>
  • <% end %> + <% if !@issue.nil? %> <% if @can[:log_time] -%>
  • <%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, @@ -102,11 +110,11 @@
  • <%= context_menu_link l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon-duplicate', :disabled => !@can[:copy] %>
  • <% end %> -
  • <%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id), :copy_options => {:copy => 't'}}, +
  • <%= context_menu_link l(:button_copy), new_issue_move_path(:ids => @issues.collect(&:id), :copy_options => {:copy => 't'}), :class => 'icon-copy', :disabled => !@can[:move] %>
  • -
  • <%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)}, +
  • <%= context_menu_link l(:button_move), new_issue_move_path(:ids => @issues.collect(&:id)), :class => 'icon-move', :disabled => !@can[:move] %>
  • -
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)}, +
  • <%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id), :back_url => @back}, :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %>
  • <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %> diff --git a/app/views/projects/list_files.rhtml b/app/views/files/index.html.erb similarity index 91% rename from app/views/projects/list_files.rhtml rename to app/views/files/index.html.erb index 2b2e5e870..20710402b 100644 --- a/app/views/projects/list_files.rhtml +++ b/app/views/files/index.html.erb @@ -1,5 +1,5 @@
    -<%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %> +<%= link_to_if_authorized l(:label_attachment_new), new_project_file_path(@project), :class => 'icon icon-add' %>

    <%=l(:label_attachment_plural)%>

    diff --git a/app/views/projects/add_file.rhtml b/app/views/files/new.html.erb similarity index 82% rename from app/views/projects/add_file.rhtml rename to app/views/files/new.html.erb index ab9c7352d..d9d1b6ee1 100644 --- a/app/views/projects/add_file.rhtml +++ b/app/views/files/new.html.erb @@ -2,7 +2,7 @@ <%= error_messages_for 'attachment' %>
    -<% form_tag({ :action => 'add_file', :id => @project }, :multipart => true, :class => "tabular") do %> +<% form_tag(project_files_path(@project), :multipart => true, :class => "tabular") do %> <% if @versions.any? %>

    diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb index 653a41d41..cb2da5adc 100644 --- a/app/views/gantts/show.html.erb +++ b/app/views/gantts/show.html.erb @@ -1,6 +1,8 @@ +<% @gantt.view = self %>

    <%= l(:label_gantt) %>

    -<% form_tag(params.merge(:month => nil, :year => nil, :months => nil), :id => 'query_form') do %> +<% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %> + <%= hidden_field_tag('project_id', @project.to_param) if @project%>
    <%= l(:label_filter_plural) %>
    @@ -27,7 +29,8 @@ }, :class => 'icon icon-checked' %> <%= link_to_remote l(:button_clear), - { :url => { :set_filter => (@query.new_record? ? 1 : nil) }, + { :url => { :project_id => @project, :set_filter => (@query.new_record? ? 1 : nil) }, + :method => :put, :update => "content", }, :class => 'icon icon-reload' if @query.new_record? %>

    @@ -54,11 +57,12 @@ if @gantt.zoom >1 end end +# Width of the entire chart g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom -g_height = [(20 * @gantt.events.length + 6)+150, 206].max +# Collect the number of issues on Versions +g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max t_height = g_height + headers_height %> - @@ -34,7 +34,7 @@
    <% if @project.versions.any? %> - <%= link_to l(:label_close_versions), {:controller => 'versions', :action => 'close_completed', :project_id => @project}, :method => :post %> + <%= link_to l(:label_close_versions), close_completed_project_versions_path(@project), :method => :put %> <% end %>
    diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index fa9771521..9651651ac 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -1,6 +1,6 @@
    <% if User.current.allowed_to?(:add_subprojects, @project) %> - <%= link_to l(:label_subproject_new), {:controller => 'projects', :action => 'add', :parent_id => @project}, :class => 'icon icon-add' %> + <%= link_to l(:label_subproject_new), {:controller => 'projects', :action => 'new', :parent_id => @project}, :class => 'icon icon-add' %> <% end %>
    @@ -51,14 +51,7 @@
    - <% if @users_by_role.any? %> -
    -

    <%=l(:label_member_plural)%>

    -

    <% @users_by_role.keys.sort.each do |role| %> - <%=h role %>: <%= @users_by_role[role].sort.collect{|u| link_to_user u}.join(", ") %>
    - <% end %>

    -
    - <% end %> + <%= render :partial => 'members_box' %> <% if @news.any? && authorize_for('news', 'index') %>
    @@ -74,14 +67,14 @@ <% if @total_hours && User.current.allowed_to?(:view_time_entries, @project) %>

    <%= l(:label_spent_time) %>

    <%= l_hours(@total_hours) %>

    -

    <%= link_to(l(:label_details), {:controller => 'timelog', :action => 'details', :project_id => @project}) %> | - <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %>

    +

    <%= link_to(l(:label_details), {:controller => 'timelog', :action => 'index', :project_id => @project}) %> | + <%= link_to(l(:label_report), {:controller => 'time_entry_reports', :action => 'report', :project_id => @project}) %>

    <% end %> <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %> <% end %> <% content_for :header_tags do %> -<%= auto_discovery_link_tag(:atom, {:action => 'activity', :id => @project, :format => 'atom', :key => User.current.rss_key}) %> +<%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %> <% end %> <% html_title(l(:label_overview)) -%> diff --git a/app/views/queries/_filters.rhtml b/app/views/queries/_filters.rhtml index 58ea1524c..20640eb8d 100644 --- a/app/views/queries/_filters.rhtml +++ b/app/views/queries/_filters.rhtml @@ -53,6 +53,18 @@ function toggle_multi_select(field) { select.multiple = true; } } + +function apply_filters_observer() { + $$("#query_form input[type=text]").invoke("observe", "keypress", function(e){ + if(e.keyCode == Event.KEY_RETURN) { + <%= remote_function(:url => { :set_filter => 1}, + :update => "content", + :with => "Form.serialize('query_form')", + :complete => "e.stop(); apply_filters_observer()") %> + } + }); +} +Event.observe(document,"dom:loaded", apply_filters_observer); //]]> diff --git a/app/views/repositories/diff.rhtml b/app/views/repositories/diff.rhtml index 73e13abf4..24f92a540 100644 --- a/app/views/repositories/diff.rhtml +++ b/app/views/repositories/diff.rhtml @@ -1,7 +1,7 @@

    <%= l(:label_revision) %> <%= format_revision(@rev_to) + ':' if @rev_to %><%= format_revision(@rev) %> <%=h @path %>

    -<% form_tag({}, :method => 'get') do %> +<% form_tag({:path => to_path_param(@path)}, :method => 'get') do %> <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %> <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>

    diff --git a/app/views/settings/_notifications.rhtml b/app/views/settings/_notifications.rhtml index cb1c1abf1..bf2b9d871 100644 --- a/app/views/settings/_notifications.rhtml +++ b/app/views/settings/_notifications.rhtml @@ -7,13 +7,18 @@

    <%= setting_check_box :bcc_recipients %>

    <%= setting_check_box :plain_text_mail %>

    + +

    <%= setting_select(:default_notification_option, User::MAIL_NOTIFICATION_OPTIONS.collect {|o| [l(o.last), o.first.to_s]}) %>

    +
    -
    <%=l(:text_select_mail_notifications)%> - <%= setting_multiselect(:notified_events, - @notifiables.collect {|notifiable| [l_or_humanize(notifiable, :prefix => 'label_'), notifiable]}, :label => false) %> - -

    <%= check_all_links('notified_events') %>

    +
    <%=l(:text_select_mail_notifications)%> +<%= hidden_field_tag 'settings[notified_events][]', '' %> +<% @notifiables.each do |notifiable| %> +<%= notification_field notifiable %> +
    +<% end %> +

    <%= check_all_links('notified_events') %>

    <%= l(:setting_emails_footer) %> diff --git a/app/views/timelog/_report_criteria.rhtml b/app/views/time_entry_reports/_report_criteria.rhtml similarity index 100% rename from app/views/timelog/_report_criteria.rhtml rename to app/views/time_entry_reports/_report_criteria.rhtml diff --git a/app/views/timelog/report.rhtml b/app/views/time_entry_reports/report.rhtml similarity index 95% rename from app/views/timelog/report.rhtml rename to app/views/time_entry_reports/report.rhtml index 533467ef2..5ae9d6550 100644 --- a/app/views/timelog/report.rhtml +++ b/app/views/time_entry_reports/report.rhtml @@ -1,5 +1,5 @@
    -<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
    <%= render_timelog_breadcrumb %> @@ -13,7 +13,7 @@ <%# TODO: get rid of the project_id field, that should already be in the URL %> <%= hidden_field_tag('project_id', params[:project_id]) if @project %> <%= hidden_field_tag('issue_id', params[:issue_id]) if @issue %> - <%= render :partial => 'date_range' %> + <%= render :partial => 'timelog/date_range' %>

    <%= l(:label_details) %>: <%= select_tag 'columns', options_for_select([[l(:label_year), 'year'], [l(:label_month), 'month'], diff --git a/app/views/timelog/_date_range.rhtml b/app/views/timelog/_date_range.rhtml index 2482a9fc3..727de25ed 100644 --- a/app/views/timelog/_date_range.rhtml +++ b/app/views/timelog/_date_range.rhtml @@ -28,9 +28,9 @@

    <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
      -
    • <%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue }), - :class => (@controller.action_name == 'details' ? 'selected' : nil)) %>
    • -
    • <%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}), +
    • <%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }), + :class => (@controller.action_name == 'index' ? 'selected' : nil)) %>
    • +
    • <%= link_to(l(:label_report), url_params.merge({:controller => 'time_entry_reports', :action => 'report', :project_id => @project, :issue_id => @issue}), :class => (@controller.action_name == 'report' ? 'selected' : nil)) %>
    diff --git a/app/views/timelog/edit.rhtml b/app/views/timelog/edit.rhtml index 00d0a77c0..79a95b572 100644 --- a/app/views/timelog/edit.rhtml +++ b/app/views/timelog/edit.rhtml @@ -1,6 +1,6 @@

    <%= l(:label_spent_time) %>

    -<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => 'edit', :id => @time_entry, :project_id => @time_entry.project} do |f| %> +<% labelled_tabular_form_for :time_entry, @time_entry, :url => {:action => (@time_entry.new_record? ? 'create' : 'edit'), :id => @time_entry, :project_id => @time_entry.project} do |f| %> <%= error_messages_for 'time_entry' %> <%= back_url_hidden_field_tag %> diff --git a/app/views/timelog/details.rhtml b/app/views/timelog/index.html.erb similarity index 92% rename from app/views/timelog/details.rhtml rename to app/views/timelog/index.html.erb index a17c06e65..737476f35 100644 --- a/app/views/timelog/details.rhtml +++ b/app/views/timelog/index.html.erb @@ -1,5 +1,5 @@
    -<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time-add' %>
    <%= render_timelog_breadcrumb %> diff --git a/app/views/users/_form.rhtml b/app/views/users/_form.rhtml index 4f9a0ff21..7e50fcdc3 100644 --- a/app/views/users/_form.rhtml +++ b/app/views/users/_form.rhtml @@ -32,4 +32,14 @@ <%= password_field_tag 'password_confirmation', nil, :size => 25 %>

    + +
    +

    <%=l(:field_mail_notification)%>

    +<%= render :partial => 'users/mail_notifications' %> +
    + +
    +

    <%=l(:label_preferences)%>

    +<%= render :partial => 'users/preferences' %> +
    diff --git a/app/views/users/_general.rhtml b/app/views/users/_general.rhtml index e962056a2..a08b3cee3 100644 --- a/app/views/users/_general.rhtml +++ b/app/views/users/_general.rhtml @@ -1,4 +1,4 @@ -<% labelled_tabular_form_for :user, @user, :url => { :controller => 'users', :action => "edit", :tab => nil }, :html => { :class => nil } do |f| %> +<% labelled_tabular_form_for :user, @user, :url => { :controller => 'users', :action => "update", :tab => nil }, :html => { :method => :put, :class => nil } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> <% if @user.active? -%>

    diff --git a/app/views/users/_groups.rhtml b/app/views/users/_groups.rhtml index 4bca77c00..0ab2f11eb 100644 --- a/app/views/users/_groups.rhtml +++ b/app/views/users/_groups.rhtml @@ -1,4 +1,4 @@ -<% form_for(:user, :url => { :action => 'edit' }) do %> +<% form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>

    <% Group.all.sort.each do |group| %>
    diff --git a/app/views/users/_mail_notifications.html.erb b/app/views/users/_mail_notifications.html.erb new file mode 100644 index 000000000..d29250893 --- /dev/null +++ b/app/views/users/_mail_notifications.html.erb @@ -0,0 +1,12 @@ +

    +<%= select_tag 'notification_option', options_for_select(@notification_options.collect {|o| [l(o.last), o.first]}, @notification_option.to_sym), + :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.projects.each do |project| %> +
    +<% end %>

    +

    <%= l(:text_user_mail_option) %>

    +<% end %> +

    <%= check_box_tag 'no_self_notified', 1, @user.pref[:no_self_notified] %>

    + diff --git a/app/views/users/_memberships.rhtml b/app/views/users/_memberships.rhtml index 70921f4ab..441294708 100644 --- a/app/views/users/_memberships.rhtml +++ b/app/views/users/_memberships.rhtml @@ -14,7 +14,9 @@ <% @user.memberships.each do |membership| %> <% next if membership.new_record? %>
    - + <% for user in @users -%> <%= %w(anon active registered locked)[user.status] %>"> - + diff --git a/app/views/users/add.rhtml b/app/views/users/new.html.erb similarity index 90% rename from app/views/users/add.rhtml rename to app/views/users/new.html.erb index 2e0743e87..0e7a33319 100644 --- a/app/views/users/add.rhtml +++ b/app/views/users/new.html.erb @@ -1,6 +1,6 @@

    <%= link_to l(:label_user_plural), :controller => 'users', :action => 'index' %> » <%=l(:label_user_new)%>

    -<% labelled_tabular_form_for :user, @user, :url => { :action => "add" }, :html => { :class => nil } do |f| %> +<% labelled_tabular_form_for :user, @user, :url => { :action => "create" }, :html => { :class => nil } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %>

    diff --git a/app/views/users/show.rhtml b/app/views/users/show.rhtml index 82634b010..afab71110 100644 --- a/app/views/users/show.rhtml +++ b/app/views/users/show.rhtml @@ -1,5 +1,5 @@

    -<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %> +<%= link_to(l(:button_edit), edit_user_path(@user), :class => 'icon icon-edit') if User.current.admin? %>

    <%= avatar @user, :size => "50" %> <%=h @user.name %>

    @@ -24,7 +24,7 @@

    <%=l(:label_project_plural)%>

      <% for membership in @memberships %> -
    • <%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %> +
    • <%= link_to_project(membership.project) %> (<%=h membership.roles.sort.collect(&:to_s).join(', ') %>, <%= format_date(membership.created_on) %>)
    • <% end %>
    @@ -35,7 +35,7 @@
    <% unless @events_by_day.empty? %> -

    <%= link_to l(:label_activity), :controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :from => @events_by_day.keys.first %>

    +

    <%= link_to l(:label_activity), :controller => 'activities', :action => 'index', :id => nil, :user_id => @user, :from => @events_by_day.keys.first %>

    <%=l(:label_reported_issues)%>: <%= Issue.count(:conditions => ["author_id=?", @user.id]) %> @@ -57,11 +57,11 @@

    <% other_formats_links do |f| %> - <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => nil, :user_id => @user, :key => User.current.rss_key} %> + <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => nil, :user_id => @user, :key => User.current.rss_key} %> <% end %> <% content_for :header_tags do %> - <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :user_id => @user, :format => :atom, :key => User.current.rss_key) %> + <%= auto_discovery_link_tag(:atom, :controller => 'activities', :action => 'index', :user_id => @user, :format => :atom, :key => User.current.rss_key) %> <% end %> <% end %> <%= call_hook :view_account_right_bottom, :user => @user %> diff --git a/app/views/versions/_issue_counts.rhtml b/app/views/versions/_issue_counts.rhtml index 4bab5c659..38f3edbcb 100644 --- a/app/views/versions/_issue_counts.rhtml +++ b/app/views/versions/_issue_counts.rhtml @@ -5,7 +5,7 @@ select_tag('status_by', status_by_options_for_select(criteria), :id => 'status_by_select', - :onchange => remote_function(:url => { :action => :status_by, :id => version }, + :onchange => remote_function(:url => status_by_project_version_path(version.project, version), :with => "Form.serialize('status_by_form')"))) %> <% if counts.empty? %> diff --git a/app/views/versions/edit.rhtml b/app/views/versions/edit.rhtml index 1556ebba1..8724fe62a 100644 --- a/app/views/versions/edit.rhtml +++ b/app/views/versions/edit.rhtml @@ -1,6 +1,6 @@

    <%=l(:label_version)%>

    -<% labelled_tabular_form_for :version, @version, :url => { :action => 'edit' } do |f| %> +<% labelled_tabular_form_for :version, @version, :url => project_version_path(@project, @version), :html => {:method => :put} do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> <% end %> diff --git a/app/views/projects/roadmap.rhtml b/app/views/versions/index.html.erb similarity index 96% rename from app/views/projects/roadmap.rhtml rename to app/views/versions/index.html.erb index 100282a7a..d0c5dcac1 100644 --- a/app/views/projects/roadmap.rhtml +++ b/app/views/versions/index.html.erb @@ -51,4 +51,4 @@ <% html_title(l(:label_roadmap)) %> -<%= context_menu :controller => 'issues', :action => 'context_menu' %> +<%= context_menu issues_context_menu_path %> diff --git a/app/views/versions/new.html.erb b/app/views/versions/new.html.erb index a7ef03e76..d60468159 100644 --- a/app/views/versions/new.html.erb +++ b/app/views/versions/new.html.erb @@ -1,6 +1,6 @@

    <%=l(:label_version_new)%>

    -<% labelled_tabular_form_for :version, @version, :url => { :action => 'new' } do |f| %> +<% labelled_tabular_form_for :version, @version, :url => project_versions_path(@project) do |f| %> <%= render :partial => 'versions/form', :locals => { :f => f } %> <%= submit_tag l(:button_create) %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/versions/show.rhtml b/app/views/versions/show.rhtml index 18bc6bc45..6b2b09d0f 100644 --- a/app/views/versions/show.rhtml +++ b/app/views/versions/show.rhtml @@ -1,5 +1,6 @@
    <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => @version}, :class => 'icon icon-edit' %> +<%= link_to_if_authorized(l(:button_edit_associated_wikipage, :page_title => @version.wiki_page_title), {:controller => 'wiki', :action => 'edit', :page => Wiki.titleize(@version.wiki_page_title)}, :class => 'icon icon-edit') unless @version.wiki_page_title.blank? || @project.wiki.nil? %> <%= call_hook(:view_versions_show_contextual, { :version => @version, :project => @project }) %>
    @@ -32,13 +33,10 @@ <%= 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, :priority], - :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %> -<% if issues.size > 0 %> +<% if @issues.present? %> -Fixed #5463: 0.9.4 INSTALL and/or UPGRADE, missing session_store.rb -Fixed #5524: Update_parent_attributes doesn't work for the old parent issue when reparenting -Fixed #5548: SVN Repository: Can not list content of a folder which includes square brackets. -Fixed #5589: "with subproject" malfunction -Fixed #5676: Search for Numeric Value -Fixed #5696: Redmine + PostgreSQL 8.4.4 fails on _dir_list_content.rhtml -Fixed #5698: redmine:email:receive_imap fails silently for mails with subject longer than 255 characters -Fixed #5700: TimelogController#destroy assumes success -Fixed #5751: developer role is mispelled -Fixed #5769: Popup Calendar doesn't Advance in Chrome -Fixed #5771: Problem when importing git repository -Fixed #5823: Error in comments in plugin.rb +* #443: Adds context menu to the roadmap issue lists +* #443: Subtasking +* #741: Description preview while editing an issue +* #1131: Add support for alternate (non-LDAP) authentication +* #1214: REST API for Issues +* #1223: File upload on wiki edit form +* #1755: add "blocked by" as a related issues option +* #2420: Fetching emails from an POP server +* #2482: Named scopes in Issue and ActsAsWatchable plus some view refactoring (logic extraction). +* #2924: Make the right click menu more discoverable using a cursor property +* #2985: Make syntax highlighting pluggable +* #3201: Workflow Check/Uncheck All Rows/Columns +* #3359: Update CodeRay 0.9 +* #3706: Allow assigned_to field configuration on Issue creation by email +* #3936: configurable list of models to include in search +* #4480: Create a link to the user profile from the administration interface +* #4482: Cache textile rendering +* #4572: Make it harder to ruin your database +* #4573: Move github gems to Gemcutter +* #4664: Add pagination to forum threads +* #4732: Make login case-insensitive also for PostgreSQL +* #4812: Create links to other projects +* #4819: Replace images with smushed ones for speed +* #4945: Allow custom fields attached to project to be searchable +* #5121: Fix issues list layout overflow +* #5169: Issue list view hook request +* #5208: Aibility to edit wiki sidebar +* #5281: Remove empty ul tags in the issue history +* #5291: Updated basque translations +* #5328: Automatically add "Repository" menu_item after repository creation +* #5415: Fewer SQL statements generated for watcher_recipients +* #5416: Exclude "fields_for" from overridden methods in TabularFormBuilder +* #5573: Allow issue assignment in email +* #5595: Allow start date and due dates to be set via incoming email +* #5752: The projects view (/projects) renders ul's wrong +* #5781: Allow to use more macros on the welcome page and project list +* Fixed #1288: Unable to past escaped wiki syntax in an issue description +* Fixed #1334: Wiki formatting character *_ and _* +* Fixed #1416: Inline code with less-then/greater-than produces @lt; and @gt; respectively +* Fixed #2473: Login and mail should not be case sensitive +* Fixed #2990: Ruby 1.9 - wrong number of arguments (1 for 0) on rake db:migrate +* Fixed #3089: Text formatting sometimes breaks when combined +* Fixed #3690: Status change info duplicates on the issue screen +* Fixed #3691: Redmine allows two files with the same file name to be uploaded to the same issue +* Fixed #3764: ApplicationHelperTest fails with JRuby +* Fixed #4265: Unclosed code tags in issue descriptions affects main UI +* Fixed #4745: Bug in index.xml.builder (issues) +* Fixed #4852: changing user/roles of project member not possible without javascript +* Fixed #4857: Week number calculation in date picker is wrong if a week starts with Sunday +* Fixed #4883: Bottom "contextual" placement in issue with associated changeset +* Fixed #4918: Revisions r3453 and r3454 broke On-the-fly user creation with LDAP +* Fixed #4935: Navigation to the Master Timesheet page (time_entries) +* Fixed #5043: Flash messages are not displayed after the project settings[module/activity] saved +* Fixed #5081: Broken links on public/help/wiki_syntax_detailed.html +* Fixed #5104: Description of document not wikified on documents index +* Fixed #5108: Issue linking fails inside of []s +* Fixed #5199: diff code coloring using coderay +* Fixed #5233: Add a hook to the issue report (Summary) view +* Fixed #5265: timetracking: subtasks time is added to the main task +* Fixed #5343: acts_as_event Doesn't Accept Outside URLs +* Fixed #5440: UI Inconsistency : Administration > Enumerations table row headers should be enclosed in +* Fixed #5463: 0.9.4 INSTALL and/or UPGRADE, missing session_store.rb +* Fixed #5524: Update_parent_attributes doesn't work for the old parent issue when reparenting +* Fixed #5548: SVN Repository: Can not list content of a folder which includes square brackets. +* Fixed #5589: "with subproject" malfunction +* Fixed #5676: Search for Numeric Value +* Fixed #5696: Redmine + PostgreSQL 8.4.4 fails on _dir_list_content.rhtml +* Fixed #5698: redmine:email:receive_imap fails silently for mails with subject longer than 255 characters +* Fixed #5700: TimelogController#destroy assumes success +* Fixed #5751: developer role is mispelled +* Fixed #5769: Popup Calendar doesn't Advance in Chrome +* Fixed #5771: Problem when importing git repository +* Fixed #5823: Error in comments in plugin.rb == 2010-07-07 v0.9.6 -Fixed: Redmine.pm access by unauthorized users +* Fixed: Redmine.pm access by unauthorized users == 2010-06-24 v0.9.5 -Linkify folder names on revision view -"fiters" and "options" should be hidden in print view via css -Fixed: NoMethodError when no issue params are submitted -Fixed: projects.atom with required authentication -Fixed: External links not correctly displayed in Wiki TOC -Fixed: Member role forms in project settings are not hidden after member added -Fixed: pre can't be inside p -Fixed: session cookie path does not respect RAILS_RELATIVE_URL_ROOT -Fixed: mail handler fails when the from address is empty +* Linkify folder names on revision view +* "fiters" and "options" should be hidden in print view via css +* Fixed: NoMethodError when no issue params are submitted +* Fixed: projects.atom with required authentication +* Fixed: External links not correctly displayed in Wiki TOC +* Fixed: Member role forms in project settings are not hidden after member added +* Fixed: pre can't be inside p +* Fixed: session cookie path does not respect RAILS_RELATIVE_URL_ROOT +* Fixed: mail handler fails when the from address is empty == 2010-05-01 v0.9.4 -Filters collapsed by default on issues index page for a saved query -Fixed: When categories list is too big the popup menu doesn't adjust (ex. in the issue list) -Fixed: remove "main-menu" div when the menu is empty -Fixed: Code syntax highlighting not working in Document page -Fixed: Git blame/annotate fails on moved files -Fixed: Failing test in test_show_atom -Fixed: Migrate from trac - not displayed Wikis -Fixed: Email notifications on file upload sent to empty recipient list -Fixed: Migrating from trac is not possible, fails to allocate memory -Fixed: Lost password no longer flashes a confirmation message -Fixed: Crash while deleting in-use enumeration -Fixed: Hard coded English string at the selection of issue watchers -Fixed: Bazaar v2.1.0 changed behaviour -Fixed: Roadmap display can raise an exception if no trackers are selected -Fixed: Gravatar breaks layout of "logged in" page -Fixed: Reposman.rb on Windows -Fixed: Possible error 500 while moving an issue to another project with SQLite -Fixed: backslashes in issue description/note should be escaped when quoted -Fixed: Long text in
     disrupts Associated revisions
    -Fixed: Links to missing wiki pages not red on project overview page
    -Fixed: Cannot delete a project with subprojects that shares versions
    -Fixed: Update of Subversion changesets broken under Solaris
    -Fixed: "Move issues" permission not working for Non member
    -Fixed: Sidebar overlap on Users tab of Group editor
    -Fixed: Error on db:migrate with table prefix set (hardcoded name in principal.rb)
    -Fixed: Report shows sub-projects for non-members
    -Fixed: 500 internal error when browsing any Redmine page in epiphany
    -Fixed: Watchers selection lost when issue creation fails
    -Fixed: When copying projects, redmine should not generate an email to people who created issues
    -Fixed: Issue "#" table cells should have a class attribute to enable fine-grained CSS theme
    -Fixed: Plugin generators should display help if no parameter is given
    +* Filters collapsed by default on issues index page for a saved query
    +* Fixed: When categories list is too big the popup menu doesn't adjust (ex. in the issue list)
    +* Fixed: remove "main-menu" div when the menu is empty
    +* Fixed: Code syntax highlighting not working in Document page
    +* Fixed: Git blame/annotate fails on moved files
    +* Fixed: Failing test in test_show_atom
    +* Fixed: Migrate from trac - not displayed Wikis
    +* Fixed: Email notifications on file upload sent to empty recipient list
    +* Fixed: Migrating from trac is not possible, fails to allocate memory
    +* Fixed: Lost password no longer flashes a confirmation message
    +* Fixed: Crash while deleting in-use enumeration
    +* Fixed: Hard coded English string at the selection of issue watchers
    +* Fixed: Bazaar v2.1.0 changed behaviour
    +* Fixed: Roadmap display can raise an exception if no trackers are selected
    +* Fixed: Gravatar breaks layout of "logged in" page
    +* Fixed: Reposman.rb on Windows
    +* Fixed: Possible error 500 while moving an issue to another project with SQLite
    +* Fixed: backslashes in issue description/note should be escaped when quoted
    +* Fixed: Long text in 
     disrupts Associated revisions
    +* Fixed: Links to missing wiki pages not red on project overview page
    +* Fixed: Cannot delete a project with subprojects that shares versions
    +* Fixed: Update of Subversion changesets broken under Solaris
    +* Fixed: "Move issues" permission not working for Non member
    +* Fixed: Sidebar overlap on Users tab of Group editor
    +* Fixed: Error on db:migrate with table prefix set (hardcoded name in principal.rb)
    +* Fixed: Report shows sub-projects for non-members
    +* Fixed: 500 internal error when browsing any Redmine page in epiphany
    +* Fixed: Watchers selection lost when issue creation fails
    +* Fixed: When copying projects, redmine should not generate an email to people who created issues
    +* Fixed: Issue "#" table cells should have a class attribute to enable fine-grained CSS theme
    +* Fixed: Plugin generators should display help if no parameter is given
     
     
     == 2010-02-28 v0.9.3
     
    -Adds filter for system shared versions on the cross project issue list
    -Makes project identifiers searchable
    -Remove invalid utf8 sequences from commit comments and author name
    -Fixed: Wrong link when "http" not included in project "Homepage" link
    -Fixed: Escaping in html email templates
    -Fixed: Pound (#) followed by number with leading zero (0) removes leading zero when rendered in wiki
    -Fixed: Deselecting textile text formatting causes interning empty string errors
    -Fixed: error with postgres when entering a non-numeric id for an issue relation
    -Fixed: div.task incorrectly wrapping on Gantt Chart
    -Fixed: Project copy loses wiki pages hierarchy
    -Fixed: parent project field doesn't include blank value when a member with 'add subproject' permission edits a child project
    -Fixed: Repository.fetch_changesets tries to fetch changesets for archived projects
    -Fixed: Duplicated project name for subproject version on gantt chart
    -Fixed: roadmap shows subprojects issues even if subprojects is unchecked
    -Fixed: IndexError if all the :last menu items are deleted from a menu
    -Fixed: Very high CPU usage for a long time when fetching commits from a large Git repository
    +* Adds filter for system shared versions on the cross project issue list
    +* Makes project identifiers searchable
    +* Remove invalid utf8 sequences from commit comments and author name
    +* Fixed: Wrong link when "http" not included in project "Homepage" link
    +* Fixed: Escaping in html email templates
    +* Fixed: Pound (#) followed by number with leading zero (0) removes leading zero when rendered in wiki
    +* Fixed: Deselecting textile text formatting causes interning empty string errors
    +* Fixed: error with postgres when entering a non-numeric id for an issue relation
    +* Fixed: div.task incorrectly wrapping on Gantt Chart
    +* Fixed: Project copy loses wiki pages hierarchy
    +* Fixed: parent project field doesn't include blank value when a member with 'add subproject' permission edits a child project
    +* Fixed: Repository.fetch_changesets tries to fetch changesets for archived projects
    +* Fixed: Duplicated project name for subproject version on gantt chart
    +* Fixed: roadmap shows subprojects issues even if subprojects is unchecked
    +* Fixed: IndexError if all the :last menu items are deleted from a menu
    +* Fixed: Very high CPU usage for a long time when fetching commits from a large Git repository
     
     
     == 2010-02-07 v0.9.2
    diff --git a/lib/redmine.rb b/lib/redmine.rb
    index 238ee110a..10bdf4c24 100644
    --- a/lib/redmine.rb
    +++ b/lib/redmine.rb
    @@ -8,6 +8,7 @@ require 'redmine/core_ext'
     require 'redmine/themes'
     require 'redmine/hook'
     require 'redmine/plugin'
    +require 'redmine/notifiable'
     require 'redmine/wiki_formatting'
     require 'redmine/scm/base'
     
    @@ -44,39 +45,38 @@ end
     
     # Permissions
     Redmine::AccessControl.map do |map|
    -  map.permission :view_project, {:projects => [:show, :activity]}, :public => true
    +  map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
       map.permission :search_project, {:search => :index}, :public => true
    -  map.permission :add_project, {:projects => :add}, :require => :loggedin
    -  map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
    +  map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
    +  map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
       map.permission :select_project_modules, {:projects => :modules}, :require => :member
       map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
    -  map.permission :manage_versions, {:projects => :settings, :versions => [:new, :edit, :close_completed, :destroy]}, :require => :member
    -  map.permission :add_subprojects, {:projects => :add}, :require => :member
    +  map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
    +  map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
       
       map.project_module :issue_tracking do |map|
         # Issue categories
         map.permission :manage_categories, {:projects => :settings, :issue_categories => [:new, :edit, :destroy]}, :require => :member
         # Issues
    -    map.permission :view_issues, {:projects => :roadmap, 
    -                                  :issues => [:index, :changes, :show, :context_menu, :auto_complete],
    -                                  :versions => [:show, :status_by],
    +    map.permission :view_issues, {:issues => [:index, :show],
    +                                  :auto_complete => [:issues],
    +                                  :context_menus => [:issues],
    +                                  :versions => [:index, :show, :status_by],
    +                                  :journals => :index,
                                       :queries => :index,
                                       :reports => [:issue_report, :issue_report_details]}
         map.permission :add_issues, {:issues => [:new, :create, :update_form]}
    -    map.permission :edit_issues, {:issues => [:edit, :update, :reply, :bulk_edit, :update_form]}
    +    map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]}
         map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
         map.permission :manage_subtasks, {}
    -    map.permission :add_issue_notes, {:issues => [:edit, :update, :reply]}
    +    map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]}
         map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
         map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
    -    map.permission :move_issues, {:issues => :move}, :require => :loggedin
    +    map.permission :move_issues, {:issue_moves => [:new, :create]}, :require => :loggedin
         map.permission :delete_issues, {:issues => :destroy}, :require => :member
         # Queries
         map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
         map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
    -    # Gantt & calendar
    -    map.permission :view_gantt, :gantts => :show
    -    map.permission :view_calendar, :calendars => :show
         # Watchers
         map.permission :view_issue_watchers, {}
         map.permission :add_issue_watchers, {:watchers => :new}
    @@ -84,17 +84,17 @@ Redmine::AccessControl.map do |map|
       end
       
       map.project_module :time_tracking do |map|
    -    map.permission :log_time, {:timelog => :edit}, :require => :loggedin
    -    map.permission :view_time_entries, :timelog => [:details, :report]
    -    map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
    -    map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
    -    map.permission :manage_project_activities, {:projects => [:save_activities, :reset_activities]}, :require => :member
    +    map.permission :log_time, {:timelog => [:new, :create, :edit]}, :require => :loggedin
    +    map.permission :view_time_entries, :timelog => [:index], :time_entry_reports => [:report]
    +    map.permission :edit_time_entries, {:timelog => [:new, :create, :edit, :destroy]}, :require => :member
    +    map.permission :edit_own_time_entries, {:timelog => [:new, :create, :edit, :destroy]}, :require => :loggedin
    +    map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
       end
       
       map.project_module :news do |map|
    -    map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
    +    map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
         map.permission :view_news, {:news => [:index, :show]}, :public => true
    -    map.permission :comment_news, {:news => :add_comment}
    +    map.permission :comment_news, {:comments => :create}
       end
     
       map.project_module :documents do |map|
    @@ -103,8 +103,8 @@ Redmine::AccessControl.map do |map|
       end
       
       map.project_module :files do |map|
    -    map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
    -    map.permission :view_files, :projects => :list_files, :versions => :download
    +    map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
    +    map.permission :view_files, :files => :index, :versions => :download
       end
         
       map.project_module :wiki do |map|
    @@ -135,6 +135,14 @@ Redmine::AccessControl.map do |map|
         map.permission :delete_messages, {:messages => :destroy}, :require => :member
         map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
       end
    +
    +  map.project_module :calendar do |map|
    +    map.permission :view_calendar, :calendars => [:show, :update]
    +  end
    +
    +  map.project_module :gantt do |map|
    +    map.permission :view_gantt, :gantts => [:show, :update]
    +  end
     end
     
     Redmine::MenuManager.map :top_menu do |menu|
    @@ -157,24 +165,41 @@ Redmine::MenuManager.map :application_menu do |menu|
     end
     
     Redmine::MenuManager.map :admin_menu do |menu|
    -  # Empty
    +  menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
    +  menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
    +  menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
    +  menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
    +  menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
    +  menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
    +            :html => {:class => 'issue_statuses'}
    +  menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
    +  menu.push :custom_fields, {:controller => 'custom_fields'},  :caption => :label_custom_field_plural,
    +            :html => {:class => 'custom_fields'}
    +  menu.push :enumerations, {:controller => 'enumerations'}
    +  menu.push :settings, {:controller => 'settings'}
    +  menu.push :ldap_authentication, {:controller => 'ldap_auth_sources', :action => 'index'},
    +            :html => {:class => 'server_authentication'}
    +  menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
    +  menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
     end
     
     Redmine::MenuManager.map :project_menu do |menu|
       menu.push :overview, { :controller => 'projects', :action => 'show' }
    -  menu.push :activity, { :controller => 'projects', :action => 'activity' }
    -  menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, 
    +  menu.push :activity, { :controller => 'activities', :action => 'index' }
    +  menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
                   :if => Proc.new { |p| p.shared_versions.any? }
       menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
       menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
                   :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
    +  menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
    +  menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
       menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
       menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
       menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil }, 
                   :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
       menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
                   :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
    -  menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_file_plural
    +  menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
       menu.push :repository, { :controller => 'repositories', :action => 'show' },
                   :if => Proc.new { |p| p.repository && !p.repository.new_record? }
       menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
    diff --git a/lib/redmine/export/pdf.rb b/lib/redmine/export/pdf.rb
    index c24921653..c4a2c3d90 100644
    --- a/lib/redmine/export/pdf.rb
    +++ b/lib/redmine/export/pdf.rb
    @@ -154,7 +154,7 @@ module Redmine
               if query.grouped? && (group = query.group_by_column.value(issue)) != previous_group
                 pdf.SetFontStyle('B',9)
                 pdf.Cell(277, row_height, 
    -              (group.blank? ? 'None' : group.to_s) + " (#{@issue_count_by_group[group]})",
    +              (group.blank? ? 'None' : group.to_s) + " (#{query.issue_count_by_group[group]})",
                   1, 1, 'L')
                 pdf.SetFontStyle('',8)
                 previous_group = group
    @@ -184,7 +184,7 @@ module Redmine
             end
             pdf.Output
           end
    -      
    +
           # Returns a PDF string of a single issue
           def issue_to_pdf(issue)
             pdf = IFPDF.new(current_language)
    @@ -208,7 +208,7 @@ module Redmine
             pdf.SetFontStyle('',9)
             pdf.Cell(60,5, issue.priority.to_s,"RT")        
             pdf.Ln
    -          
    +        
             pdf.SetFontStyle('B',9)
             pdf.Cell(35,5, l(:field_author) + ":","L")
             pdf.SetFontStyle('',9)
    @@ -238,14 +238,14 @@ module Redmine
             pdf.SetFontStyle('',9)
             pdf.Cell(60,5, format_date(issue.due_date),"RB")
             pdf.Ln
    -          
    +        
             for custom_value in issue.custom_field_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)
    @@ -255,7 +255,7 @@ module Redmine
             pdf.SetFontStyle('B',9)
             pdf.Cell(35,5, l(:field_description) + ":")
             pdf.SetFontStyle('',9)
    -        pdf.MultiCell(155,5, @issue.description,"BR")
    +        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)
    @@ -311,184 +311,7 @@ module Redmine
             end
             pdf.Output
           end
    -      
    -      # Returns a PDF string of a gantt chart
    -      def gantt_to_pdf(gantt, project)
    -        pdf = IFPDF.new(current_language)
    -        pdf.SetTitle("#{l(:label_gantt)} #{project}")
    -        pdf.AliasNbPages
    -        pdf.footer_date = format_date(Date.today)
    -        pdf.AddPage("L")
    -        pdf.SetFontStyle('B',12)
    -        pdf.SetX(15)
    -        pdf.Cell(70, 20, project.to_s)
    -        pdf.Ln
    -        pdf.SetFontStyle('B',9)
    -        
    -        subject_width = 70
    -        header_heigth = 5
    -        
    -        headers_heigth = header_heigth
    -        show_weeks = false
    -        show_days = false
    -        
    -        if gantt.months < 7
    -          show_weeks = true
    -          headers_heigth = 2*header_heigth
    -          if gantt.months < 3
    -            show_days = true
    -            headers_heigth = 3*header_heigth
    -          end
    -        end
    -        
    -        g_width = 210
    -        zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
    -        g_height = 120
    -        t_height = g_height + headers_heigth
    -        
    -        y_start = pdf.GetY
    -        
    -        # Months headers
    -        month_f = gantt.date_from
    -        left = subject_width
    -        height = header_heigth
    -        gantt.months.times do 
    -          width = ((month_f >> 1) - month_f) * zoom 
    -          pdf.SetY(y_start)
    -          pdf.SetX(left)
    -          pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
    -          left = left + width
    -          month_f = month_f >> 1
    -        end  
    -        
    -        # Weeks headers
    -        if show_weeks
    -          left = subject_width
    -          height = header_heigth
    -          if gantt.date_from.cwday == 1
    -            # gantt.date_from is monday
    -            week_f = gantt.date_from
    -          else
    -            # find next monday after gantt.date_from
    -            week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
    -            width = (7 - gantt.date_from.cwday + 1) * zoom-1
    -            pdf.SetY(y_start + header_heigth)
    -            pdf.SetX(left)
    -            pdf.Cell(width + 1, height, "", "LTR")
    -            left = left + width+1
    -          end
    -          while week_f <= gantt.date_to
    -            width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
    -            pdf.SetY(y_start + header_heigth)
    -            pdf.SetX(left)
    -            pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
    -            left = left + width
    -            week_f = week_f+7
    -          end
    -        end
    -        
    -        # Days headers
    -        if show_days
    -          left = subject_width
    -          height = header_heigth
    -          wday = gantt.date_from.cwday
    -          pdf.SetFontStyle('B',7)
    -          (gantt.date_to - gantt.date_from + 1).to_i.times do 
    -            width = zoom
    -            pdf.SetY(y_start + 2 * header_heigth)
    -            pdf.SetX(left)
    -            pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
    -            left = left + width
    -            wday = wday + 1
    -            wday = 1 if wday > 7
    -          end
    -        end
    -        
    -        pdf.SetY(y_start)
    -        pdf.SetX(15)
    -        pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
    -        
    -        # Tasks
    -        top = headers_heigth + y_start
    -        pdf.SetFontStyle('B',7)
    -        gantt.events.each do |i|
    -          pdf.SetY(top)
    -          pdf.SetX(15)
    -          
    -          if i.is_a? Issue
    -            pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
    -          else
    -            pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
    -          end
    -        
    -          pdf.SetY(top)
    -          pdf.SetX(subject_width)
    -          pdf.Cell(g_width, 5, "", "LR")
    -        
    -          pdf.SetY(top+1.5)
    -          
    -          if i.is_a? Issue
    -            i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
    -            i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
    -            
    -            i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
    -            i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
    -            i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
    -            
    -            i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
    -            
    -            i_left = ((i_start_date - gantt.date_from)*zoom) 
    -            i_width = ((i_end_date - i_start_date + 1)*zoom)
    -            d_width = ((i_done_date - i_start_date)*zoom)
    -            l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
    -            l_width ||= 0
    -          
    -            pdf.SetX(subject_width + i_left)
    -            pdf.SetFillColor(200,200,200)
    -            pdf.Cell(i_width, 2, "", 0, 0, "", 1)
    -          
    -            if l_width > 0
    -              pdf.SetY(top+1.5)
    -              pdf.SetX(subject_width + i_left)
    -              pdf.SetFillColor(255,100,100)
    -              pdf.Cell(l_width, 2, "", 0, 0, "", 1)
    -            end 
    -            if d_width > 0
    -              pdf.SetY(top+1.5)
    -              pdf.SetX(subject_width + i_left)
    -              pdf.SetFillColor(100,100,255)
    -              pdf.Cell(d_width, 2, "", 0, 0, "", 1)
    -            end
    -            
    -            pdf.SetY(top+1.5)
    -            pdf.SetX(subject_width + i_left + i_width)
    -            pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
    -          else
    -            i_left = ((i.start_date - gantt.date_from)*zoom) 
    -            
    -            pdf.SetX(subject_width + i_left)
    -            pdf.SetFillColor(50,200,50)
    -            pdf.Cell(2, 2, "", 0, 0, "", 1) 
    -        
    -            pdf.SetY(top+1.5)
    -            pdf.SetX(subject_width + i_left + 3)
    -            pdf.Cell(30, 2, "#{i.name}")
    -          end
    -          
    -          top = top + 5
    -          pdf.SetDrawColor(200, 200, 200)
    -          pdf.Line(15, top, subject_width+g_width, top)
    -          if pdf.GetY() > 180
    -            pdf.AddPage("L")
    -            top = 20
    -            pdf.Line(15, top, subject_width+g_width, top)
    -          end
    -          pdf.SetDrawColor(0, 0, 0)
    -        end
    -        
    -        pdf.Line(15, top, subject_width+g_width, top)
    -        pdf.Output
    -      end
    +
         end
       end
     end
    diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb
    index 330b58ee7..cec323720 100644
    --- a/lib/redmine/helpers/gantt.rb
    +++ b/lib/redmine/helpers/gantt.rb
    @@ -19,11 +19,28 @@ module Redmine
       module Helpers
         # Simple class to handle gantt chart data
         class Gantt
    -      attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events
    -    
    +      include ERB::Util
    +      include Redmine::I18n
    +
    +      # :nodoc:
    +      # Some utility methods for the PDF export
    +      class PDF
    +        MaxCharactorsForSubject = 45
    +        TotalWidth = 280
    +        LeftPaneWidth = 100
    +
    +        def self.right_pane_width
    +          TotalWidth - LeftPaneWidth
    +        end
    +      end
    +
    +      attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months
    +      attr_accessor :query
    +      attr_accessor :project
    +      attr_accessor :view
    +      
           def initialize(options={})
             options = options.dup
    -        @events = []
             
             if options[:year] && options[:year].to_i >0
               @year_from = options[:year].to_i
    @@ -51,44 +68,665 @@ module Redmine
             @date_from = Date.civil(@year_from, @month_from, 1)
             @date_to = (@date_from >> @months) - 1
           end
    -      
    -      
    -      def events=(e)
    -        @events = e
    -        # Adds all ancestors
    -        root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq
    -        if root_ids.any?
    -          # Retrieves all nodes
    -          parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"])
    -          # Only add ancestors
    -          @events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}}
    -        end
    -        @events.uniq!
    -        # Sort issues by hierarchy and start dates
    -        @events.sort! {|x,y| 
    -          if x.is_a?(Issue) && y.is_a?(Issue)
    -            gantt_issue_compare(x, y, @events)
    -          else
    -            gantt_start_compare(x, y)
    -          end
    -        }
    -        # Removes issues that have no start or end date
    -        @events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) }
    -        @events
    +
    +      def common_params
    +        { :controller => 'gantts', :action => 'show', :project_id => @project }
           end
           
           def params
    -        { :zoom => zoom, :year => year_from, :month => month_from, :months => months }
    +        common_params.merge({  :zoom => zoom, :year => year_from, :month => month_from, :months => months })
           end
           
           def params_previous
    -        { :year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months }
    +        common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
           end
           
           def params_next
    -        { :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }
    +        common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
           end
    -      
    +
    +            ### Extracted from the HTML view/helpers
    +      # Returns the number of rows that will be rendered on the Gantt chart
    +      def number_of_rows
    +        if @project
    +          return number_of_rows_on_project(@project)
    +        else
    +          Project.roots.inject(0) do |total, project|
    +            total += number_of_rows_on_project(project)
    +          end
    +        end
    +      end
    +
    +      # Returns the number of rows that will be used to list a project on
    +      # the Gantt chart.  This will recurse for each subproject.
    +      def number_of_rows_on_project(project)
    +        # Remove the project requirement for Versions because it will
    +        # restrict issues to only be on the current project.  This
    +        # ends up missing issues which are assigned to shared versions.
    +        @query.project = nil if @query.project
    +
    +        # One Root project
    +        count = 1
    +        # Issues without a Version
    +        count += project.issues.for_gantt.without_version.with_query(@query).count
    +
    +        # Versions
    +        count += project.versions.count
    +
    +        # Issues on the Versions
    +        project.versions.each do |version|
    +          count += version.fixed_issues.for_gantt.with_query(@query).count
    +        end
    +
    +        # Subprojects
    +        project.children.each do |subproject|
    +          count += number_of_rows_on_project(subproject)
    +        end
    +
    +        count
    +      end
    +
    +      # Renders the subjects of the Gantt chart, the left side.
    +      def subjects(options={})
    +        options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
    +
    +        output = ''
    +        if @project
    +          output << render_project(@project, options)
    +        else
    +          Project.roots.each do |project|
    +            output << render_project(project, options)
    +          end
    +        end
    +
    +        output
    +      end
    +
    +      # Renders the lines of the Gantt chart, the right side
    +      def lines(options={})
    +        options = {:indent => 4, :render => :line, :format => :html}.merge(options)
    +        output = ''
    +
    +        if @project
    +          output << render_project(@project, options)
    +        else
    +          Project.roots.each do |project|
    +            output << render_project(project, options)
    +          end
    +        end
    +        
    +        output
    +      end
    +
    +      def render_project(project, options={})
    +        options[:top] = 0 unless options.key? :top
    +        options[:indent_increment] = 20 unless options.key? :indent_increment
    +        options[:top_increment] = 20 unless options.key? :top_increment
    +
    +        output = ''
    +        # Project Header
    +        project_header = if options[:render] == :subject
    +                           subject_for_project(project, options)
    +                         else
    +                           # :line
    +                           line_for_project(project, options)
    +                         end
    +        output << project_header if options[:format] == :html
    +        
    +        options[:top] += options[:top_increment]
    +        options[:indent] += options[:indent_increment]
    +        
    +        # Second, Issues without a version
    +        issues = project.issues.for_gantt.without_version.with_query(@query)
    +        if issues
    +          issue_rendering = render_issues(issues, options)
    +          output << issue_rendering if options[:format] == :html
    +        end
    +
    +        # Third, Versions
    +        project.versions.sort.each do |version|
    +          version_rendering = render_version(version, options)
    +          output << version_rendering if options[:format] == :html
    +        end
    +
    +        # Fourth, subprojects
    +        project.children.each do |project|
    +          subproject_rendering = render_project(project, options)
    +          output << subproject_rendering if options[:format] == :html
    +        end
    +
    +        # Remove indent to hit the next sibling
    +        options[:indent] -= options[:indent_increment]
    +        
    +        output
    +      end
    +
    +      def render_issues(issues, options={})
    +        output = ''
    +        issues.each do |i|
    +          issue_rendering = if options[:render] == :subject
    +                              subject_for_issue(i, options)
    +                            else
    +                              # :line
    +                              line_for_issue(i, options)
    +                            end
    +          output << issue_rendering if options[:format] == :html
    +          options[:top] += options[:top_increment]
    +        end
    +        output
    +      end
    +
    +      def render_version(version, options={})
    +        output = ''
    +        # Version header
    +        version_rendering = if options[:render] == :subject
    +                              subject_for_version(version, options)
    +                            else
    +                              # :line
    +                              line_for_version(version, options)
    +                            end
    +
    +        output << version_rendering if options[:format] == :html
    +        
    +        options[:top] += options[:top_increment]
    +
    +        # Remove the project requirement for Versions because it will
    +        # restrict issues to only be on the current project.  This
    +        # ends up missing issues which are assigned to shared versions.
    +        @query.project = nil if @query.project
    +        
    +        issues = version.fixed_issues.for_gantt.with_query(@query)
    +        if issues
    +          # Indent issues
    +          options[:indent] += options[:indent_increment]
    +          output << render_issues(issues, options)
    +          options[:indent] -= options[:indent_increment]
    +        end
    +
    +        output
    +      end
    +
    +      def subject_for_project(project, options)
    +        case options[:format]
    +        when :html
    +          output = ''
    +
    +          output << "
    " + if project.is_a? Project + output << "" + output << view.link_to_project(project) + output << '' + else + ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project" + '' + end + output << "
    " + + output + when :image + + options[:image].fill('black') + options[:image].stroke('transparent') + options[:image].stroke_width(1) + options[:image].text(options[:indent], options[:top] + 2, project.name) + when :pdf + options[:pdf].SetY(options[:top]) + options[:pdf].SetX(15) + + char_limit = PDF::MaxCharactorsForSubject - options[:indent] + options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") + + options[:pdf].SetY(options[:top]) + options[:pdf].SetX(options[:subject_width]) + options[:pdf].Cell(options[:g_width], 5, "", "LR") + end + end + + def line_for_project(project, options) + # Skip versions that don't have a start_date + if project.is_a?(Project) && project.start_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + + + case options[:format] + when :html + output = '' + i_left = ((project.start_date - self.date_from)*options[:zoom]).floor + + start_date = project.start_date + start_date ||= self.date_from + start_left = ((start_date - self.date_from)*options[:zoom]).floor + + i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to ) + i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor + i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date ) + i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if start_date < Date.today + i_end = ((i_end_date - self.date_from) * options[:zoom]).floor + + i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders) + d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width + l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width + + # Bar graphic + + # Make sure that negative i_left and i_width don't + # overflow the subject + if i_end > 0 && i_left <= options[:g_width] + output << "
     
    " + end + + if l_width > 0 && i_left <= options[:g_width] + output << "
     
    " + end + if d_width > 0 && i_left <= options[:g_width] + output<< "
     
    " + end + + + # Starting diamond + if start_left <= options[:g_width] && start_left > 0 + output << "
     
    " + output << "
    " + output << "
    " + end + + # Ending diamond + # Don't show items too far ahead + if i_end <= options[:g_width] && i_end > 0 + output << "
     
    " + end + + # DIsplay the Project name and % + if i_end <= options[:g_width] + # Display the status even if it's floated off to the left + status_px = i_end + 12 # 12px for the diamond + status_px = 0 if status_px <= 0 + + output << "
    " + output << "#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%" + output << "
    " + end + + output + when :image + options[:image].stroke('transparent') + i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor + + # Make sure negative i_left doesn't overflow the subject + if i_left > options[:subject_width] + options[:image].fill('blue') + options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6) + options[:image].fill('black') + options[:image].text(i_left + 11, options[:top] + 1, project.name) + end + when :pdf + options[:pdf].SetY(options[:top]+1.5) + i_left = ((project.due_date - @date_from)*options[:zoom]) + + # Make sure negative i_left doesn't overflow the subject + if i_left > 0 + options[:pdf].SetX(options[:subject_width] + i_left) + options[:pdf].SetFillColor(50,50,200) + options[:pdf].Cell(2, 2, "", 0, 0, "", 1) + + options[:pdf].SetY(options[:top]+1.5) + options[:pdf].SetX(options[:subject_width] + i_left + 3) + options[:pdf].Cell(30, 2, "#{project.name}") + end + end + else + ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date" + '' + end + end + + def subject_for_version(version, options) + case options[:format] + when :html + output = '' + output << "
    " + if version.is_a? Version + output << "" + output << view.link_to_version(version) + output << '' + else + ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version" + '' + end + output << "
    " + + output + when :image + options[:image].fill('black') + options[:image].stroke('transparent') + options[:image].stroke_width(1) + options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project) + when :pdf + options[:pdf].SetY(options[:top]) + options[:pdf].SetX(15) + + char_limit = PDF::MaxCharactorsForSubject - options[:indent] + options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") + + options[:pdf].SetY(options[:top]) + options[:pdf].SetX(options[:subject_width]) + options[:pdf].Cell(options[:g_width], 5, "", "LR") + end + end + + def line_for_version(version, options) + # Skip versions that don't have a start_date + if version.is_a?(Version) && version.start_date + options[:zoom] ||= 1 + options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] + + case options[:format] + when :html + output = '' + i_left = ((version.start_date - self.date_from)*options[:zoom]).floor + # TODO: or version.fixed_issues.collect(&:start_date).min + start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present? + start_date ||= self.date_from + start_left = ((start_date - self.date_from)*options[:zoom]).floor + + i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to ) + i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor + i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date ) + i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if start_date < Date.today + + i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders) + d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width + l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width + + i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel + + # Bar graphic + + # Make sure that negative i_left and i_width don't + # overflow the subject + if i_width > 0 && i_left <= options[:g_width] + output << "
     
    " + end + if l_width > 0 && i_left <= options[:g_width] + output << "
     
    " + end + if d_width > 0 && i_left <= options[:g_width] + output<< "
     
    " + end + + + # Starting diamond + if start_left <= options[:g_width] && start_left > 0 + output << "
     
    " + output << "
    " + output << "
    " + end + + # Ending diamond + # Don't show items too far ahead + if i_left <= options[:g_width] && i_end > 0 + output << "
     
    " + end + + # Display the Version name and % + if i_end <= options[:g_width] + # Display the status even if it's floated off to the left + status_px = i_end + 12 # 12px for the diamond + status_px = 0 if status_px <= 0 + + output << "
    " + output << h("#{version.project} -") unless @project && @project == version.project + output << "#{h version } #{h version.completed_pourcent.to_i.to_s}%" + output << "
    " + end + + output + when :image + options[:image].stroke('transparent') + i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor + + # Make sure negative i_left doesn't overflow the subject + if i_left > options[:subject_width] + options[:image].fill('green') + options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6) + options[:image].fill('black') + options[:image].text(i_left + 11, options[:top] + 1, version.name) + end + when :pdf + options[:pdf].SetY(options[:top]+1.5) + i_left = ((version.start_date - @date_from)*options[:zoom]) + + # Make sure negative i_left doesn't overflow the subject + if i_left > 0 + options[:pdf].SetX(options[:subject_width] + i_left) + options[:pdf].SetFillColor(50,200,50) + options[:pdf].Cell(2, 2, "", 0, 0, "", 1) + + options[:pdf].SetY(options[:top]+1.5) + options[:pdf].SetX(options[:subject_width] + i_left + 3) + options[:pdf].Cell(30, 2, "#{version.name}") + end + end + else + ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date" + '' + end + end + + def subject_for_issue(issue, options) + case options[:format] + when :html + output = '' + output << "
    " + output << "
    " + if issue.is_a? Issue + css_classes = [] + css_classes << 'issue-overdue' if issue.overdue? + css_classes << 'issue-behind-schedule' if issue.behind_schedule? + css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to + + if issue.assigned_to.present? + assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name + output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string) + end + output << "" + output << view.link_to_issue(issue) + output << ":" + output << h(issue.subject) + output << '' + else + ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue" + '' + end + output << "
    " + + # Tooltip + if issue.is_a? Issue + output << "" + output << view.render_issue_tooltip(issue) + output << "" + end + + output << "
    " + output + when :image + options[:image].fill('black') + options[:image].stroke('transparent') + options[:image].stroke_width(1) + options[:image].text(options[:indent], options[:top] + 2, issue.subject) + when :pdf + options[:pdf].SetY(options[:top]) + options[:pdf].SetX(15) + + char_limit = PDF::MaxCharactorsForSubject - options[:indent] + options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR") + + options[:pdf].SetY(options[:top]) + options[:pdf].SetX(options[:subject_width]) + options[:pdf].Cell(options[:g_width], 5, "", "LR") + end + end + + def line_for_issue(issue, options) + # Skip issues that don't have a due_before (due_date or version's due_date) + if issue.is_a?(Issue) && issue.due_before + case options[:format] + when :html + output = '' + # Handle nil start_dates, rare but can happen. + i_start_date = if issue.start_date && issue.start_date >= self.date_from + issue.start_date + else + self.date_from + end + + i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to ) + i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor + i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date ) + i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = ((i_start_date - self.date_from)*options[:zoom]).floor + i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders) + d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width + l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width + css = "task " + (issue.leaf? ? 'leaf' : 'parent') + + # Make sure that negative i_left and i_width don't + # overflow the subject + if i_width > 0 + output << "
     
    " + end + if l_width > 0 + output << "
     
    " + end + if d_width > 0 + output<< "
     
    " + end + + # Display the status even if it's floated off to the left + status_px = i_left + i_width + 5 + status_px = 5 if status_px <= 0 + + output << "
    " + output << issue.status.name + output << ' ' + output << (issue.done_ratio).to_i.to_s + output << "%" + output << "
    " + + output << "
    " + output << '' + output << view.render_issue_tooltip(issue) + output << "
    " + output + + when :image + # Handle nil start_dates, rare but can happen. + i_start_date = if issue.start_date && issue.start_date >= @date_from + issue.start_date + else + @date_from + end + + i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to ) + i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor + i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) + i_done_date = (i_done_date >= date_to ? date_to : i_done_date ) + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor + i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue + d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width + l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width + + + # Make sure that negative i_left and i_width don't + # overflow the subject + if i_width > 0 + options[:image].fill('grey') + options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6) + options[:image].fill('red') + options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0 + options[:image].fill('blue') + options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0 + end + + # Show the status and % done next to the subject if it overflows + options[:image].fill('black') + if i_width > 0 + options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%") + else + options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%") + end + + when :pdf + options[:pdf].SetY(options[:top]+1.5) + # Handle nil start_dates, rare but can happen. + i_start_date = if issue.start_date && issue.start_date >= @date_from + issue.start_date + else + @date_from + end + + i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to ) + + i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor + i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date ) + i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date ) + + i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today + + i_left = ((i_start_date - @date_from)*options[:zoom]) + i_width = ((i_end_date - i_start_date + 1)*options[:zoom]) + d_width = ((i_done_date - i_start_date)*options[:zoom]) + l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date + l_width ||= 0 + + # Make sure that negative i_left and i_width don't + # overflow the subject + if i_width > 0 + options[:pdf].SetX(options[:subject_width] + i_left) + options[:pdf].SetFillColor(200,200,200) + options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1) + end + + if l_width > 0 + options[:pdf].SetY(options[:top]+1.5) + options[:pdf].SetX(options[:subject_width] + i_left) + options[:pdf].SetFillColor(255,100,100) + options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1) + end + if d_width > 0 + options[:pdf].SetY(options[:top]+1.5) + options[:pdf].SetX(options[:subject_width] + i_left) + options[:pdf].SetFillColor(100,100,255) + options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1) + end + + options[:pdf].SetY(options[:top]+1.5) + + # Make sure that negative i_left and i_width don't + # overflow the subject + if (i_left + i_width) >= 0 + options[:pdf].SetX(options[:subject_width] + i_left + i_width) + else + options[:pdf].SetX(options[:subject_width]) + end + options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%") + end + else + ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before" + '' + end + end + # Generates a gantt image # Only defined if RMagick is avalaible def to_image(format='PNG') @@ -96,12 +734,12 @@ module Redmine show_weeks = @zoom > 1 show_days = @zoom > 2 - subject_width = 320 + subject_width = 400 header_heigth = 18 # width of one day in pixels zoom = @zoom*2 g_width = (@date_to - @date_from + 1)*zoom - g_height = 20 * events.length + 20 + g_height = 20 * number_of_rows + 30 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth) height = g_height + headers_heigth @@ -110,14 +748,7 @@ module Redmine gc = Magick::Draw.new # Subjects - top = headers_heigth + 20 - gc.fill('black') - gc.stroke('transparent') - gc.stroke_width(1) - events.each do |i| - gc.text(4, top + 2, (i.is_a?(Issue) ? i.subject : i.name)) - top = top + 20 - end + subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image) # Months headers month_f = @date_from @@ -195,38 +826,8 @@ module Redmine # content top = headers_heigth + 20 - gc.stroke('transparent') - events.each do |i| - if i.is_a?(Issue) - i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from ) - i_end_date = (i.due_before <= date_to ? i.due_before : date_to ) - 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 ) - i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today - - i_left = subject_width + ((i_start_date - @date_from)*zoom).floor - i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue - d_width = ((i_done_date - i_start_date)*zoom).floor # done width - l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width - - gc.fill('grey') - gc.rectangle(i_left, top, i_left + i_width, top - 6) - gc.fill('red') - gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0 - gc.fill('blue') - gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0 - gc.fill('black') - gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%") - else - i_left = subject_width + ((i.start_date - @date_from)*zoom).floor - gc.fill('green') - gc.rectangle(i_left, top, i_left + 6, top - 6) - gc.fill('black') - gc.text(i_left + 11, top + 1, i.name) - end - top = top + 20 - end + + lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image) # today red line if Date.today >= @date_from and Date.today <= date_to @@ -239,36 +840,137 @@ module Redmine imgl.format = format imgl.to_blob end if Object.const_defined?(:Magick) + + def to_pdf + pdf = ::Redmine::Export::PDF::IFPDF.new(current_language) + pdf.SetTitle("#{l(:label_gantt)} #{project}") + pdf.AliasNbPages + pdf.footer_date = format_date(Date.today) + pdf.AddPage("L") + pdf.SetFontStyle('B',12) + pdf.SetX(15) + pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s) + pdf.Ln + pdf.SetFontStyle('B',9) + + subject_width = PDF::LeftPaneWidth + header_heigth = 5 + + headers_heigth = header_heigth + show_weeks = false + show_days = false + + if self.months < 7 + show_weeks = true + headers_heigth = 2*header_heigth + if self.months < 3 + show_days = true + headers_heigth = 3*header_heigth + end + end + + g_width = PDF.right_pane_width + zoom = (g_width) / (self.date_to - self.date_from + 1) + g_height = 120 + t_height = g_height + headers_heigth + + y_start = pdf.GetY + + # Months headers + month_f = self.date_from + left = subject_width + height = header_heigth + self.months.times do + width = ((month_f >> 1) - month_f) * zoom + pdf.SetY(y_start) + pdf.SetX(left) + pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C") + left = left + width + month_f = month_f >> 1 + end + + # Weeks headers + if show_weeks + left = subject_width + height = header_heigth + if self.date_from.cwday == 1 + # self.date_from is monday + week_f = self.date_from + else + # find next monday after self.date_from + week_f = self.date_from + (7 - self.date_from.cwday + 1) + width = (7 - self.date_from.cwday + 1) * zoom-1 + pdf.SetY(y_start + header_heigth) + pdf.SetX(left) + pdf.Cell(width + 1, height, "", "LTR") + left = left + width+1 + end + while week_f <= self.date_to + width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom + pdf.SetY(y_start + header_heigth) + pdf.SetX(left) + pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") + left = left + width + week_f = week_f+7 + end + end + + # Days headers + if show_days + left = subject_width + height = header_heigth + wday = self.date_from.cwday + pdf.SetFontStyle('B',7) + (self.date_to - self.date_from + 1).to_i.times do + width = zoom + pdf.SetY(y_start + 2 * header_heigth) + pdf.SetX(left) + pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C") + left = left + width + wday = wday + 1 + wday = 1 if wday > 7 + end + end + + pdf.SetY(y_start) + pdf.SetX(15) + pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1) + + # Tasks + top = headers_heigth + y_start + pdf_subjects_and_lines(pdf, { + :top => top, + :zoom => zoom, + :subject_width => subject_width, + :g_width => g_width + }) + + + pdf.Line(15, top, subject_width+g_width, top) + pdf.Output + + + end private - - def gantt_issue_compare(x, y, issues) - if x.parent_id == y.parent_id - gantt_start_compare(x, y) - elsif x.is_ancestor_of?(y) - -1 - elsif y.is_ancestor_of?(x) - 1 + + # Renders both the subjects and lines of the Gantt chart for the + # PDF format + def pdf_subjects_and_lines(pdf, options = {}) + subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options) + line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options) + + if @project + render_project(@project, subject_options) + render_project(@project, line_options) else - ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first - ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first - if ax.nil? && ay.nil? - gantt_start_compare(x, y) - else - gantt_issue_compare(ax || x, ay || y, issues) + Project.roots.each do |project| + render_project(project, subject_options) + render_project(project, line_options) end end end - - def gantt_start_compare(x, y) - if x.start_date.nil? - -1 - elsif y.start_date.nil? - 1 - else - x.start_date <=> y.start_date - end - end + end end end diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb index 1bfca81e4..e7aac9f7e 100644 --- a/lib/redmine/i18n.rb +++ b/lib/redmine/i18n.rb @@ -37,7 +37,7 @@ module Redmine def format_date(date) return nil unless date - Setting.date_format.blank? ? ::I18n.l(date.to_date) : date.strftime(Setting.date_format) + Setting.date_format.blank? ? ::I18n.l(date.to_date, :count => date.strftime('%d')) : date.strftime(Setting.date_format) end def format_time(time, include_date = true) @@ -45,7 +45,7 @@ module Redmine time = time.to_time if time.is_a?(String) zone = User.current.time_zone local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time) - Setting.time_format.blank? ? ::I18n.l(local, :format => (include_date ? :default : :time)) : + Setting.time_format.blank? ? ::I18n.l(local, :count => local.strftime('%d'), :format => (include_date ? :default : :time)) : ((include_date ? "#{format_date(time)} " : "") + "#{local.strftime(Setting.time_format)}") end diff --git a/lib/redmine/notifiable.rb b/lib/redmine/notifiable.rb new file mode 100644 index 000000000..71d1ba501 --- /dev/null +++ b/lib/redmine/notifiable.rb @@ -0,0 +1,25 @@ +module Redmine + class Notifiable < Struct.new(:name, :parent) + + def to_s + name + end + + # TODO: Plugin API for adding a new notification? + def self.all + notifications = [] + notifications << Notifiable.new('issue_added') + notifications << Notifiable.new('issue_updated') + notifications << Notifiable.new('issue_note_added', 'issue_updated') + notifications << Notifiable.new('issue_status_updated', 'issue_updated') + notifications << Notifiable.new('issue_priority_updated', 'issue_updated') + notifications << Notifiable.new('news_added') + notifications << Notifiable.new('document_added') + notifications << Notifiable.new('file_added') + notifications << Notifiable.new('message_posted') + notifications << Notifiable.new('wiki_content_added') + notifications << Notifiable.new('wiki_content_updated') + notifications + end + end +end diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb index d15bce1af..5ed57c264 100644 --- a/lib/redmine/scm/adapters/git_adapter.rb +++ b/lib/redmine/scm/adapters/git_adapter.rb @@ -65,7 +65,7 @@ module Redmine shellout(cmd) do |io| io.each_line do |line| e = line.chomp.to_s - if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/ + if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/ type = $1 sha = $2 size = $3 @@ -86,15 +86,15 @@ module Redmine def lastrev(path,rev) return nil if path.nil? - cmd = "#{GIT_BIN} --git-dir #{target('')} log --pretty=fuller --no-merges -n 1 " + cmd = "#{GIT_BIN} --git-dir #{target('')} log --date=iso --pretty=fuller --no-merges -n 1 " cmd << " #{shell_quote rev} " if rev - cmd << "-- #{path} " unless path.empty? + cmd << "-- #{shell_quote path} " unless path.empty? shellout(cmd) do |io| begin id = io.gets.split[1] author = io.gets.match('Author:\s+(.*)$')[1] 2.times { io.gets } - time = io.gets.match('CommitDate:\s+(.*)$')[1] + time = Time.parse(io.gets.match('CommitDate:\s+(.*)$')[1]).localtime Revision.new({ :identifier => id, @@ -114,14 +114,14 @@ module Redmine def revisions(path, identifier_from, identifier_to, options={}) revisions = Revisions.new - cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw --date=iso --pretty=fuller" - cmd << " --reverse" if options[:reverse] - cmd << " --all" if options[:all] + cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw --date=iso --pretty=fuller " + cmd << " --reverse " if options[:reverse] + cmd << " --all " if options[:all] cmd << " -n #{options[:limit]} " if options[:limit] - cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from - cmd << " #{shell_quote identifier_to} " if identifier_to + cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from + cmd << "#{shell_quote identifier_to}" if identifier_to cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since] - cmd << " -- #{path}" if path && !path.empty? + cmd << " -- #{shell_quote path}" if path && !path.empty? shellout(cmd) do |io| files=[] @@ -165,13 +165,13 @@ module Redmine parsing_descr = 1 changeset[:description] = "" elsif (parsing_descr == 1 || parsing_descr == 2) \ - && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/ + && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/ parsing_descr = 2 fileaction = $1 filepath = $2 files << {:action => fileaction, :path => filepath} elsif (parsing_descr == 1 || parsing_descr == 2) \ - && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\s+(.+)$/ + && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/ parsing_descr = 2 fileaction = $1 filepath = $3 diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb index 5ea62f3e0..3a81e526f 100644 --- a/lib/redmine/version.rb +++ b/lib/redmine/version.rb @@ -4,7 +4,7 @@ module Redmine module VERSION #:nodoc: MAJOR = 1 MINOR = 0 - TINY = 0 + TINY = 2 # Branch values: # * official release: nil diff --git a/lib/tasks/ci.rake b/lib/tasks/ci.rake new file mode 100644 index 000000000..092575317 --- /dev/null +++ b/lib/tasks/ci.rake @@ -0,0 +1,41 @@ +desc "Run the Continous Integration tests for Redmine" +task :ci do + # RAILS_ENV and ENV[] can diverge so force them both to test + ENV['RAILS_ENV'] = 'test' + RAILS_ENV = 'test' + Rake::Task["ci:setup"].invoke + Rake::Task["ci:build"].invoke + Rake::Task["ci:teardown"].invoke +end + +# Tasks can be hooked into by redefining them in a plugin +namespace :ci do + desc "Setup Redmine for a new build." + task :setup do + Rake::Task["ci:dump_environment"].invoke + Rake::Task["db:create"].invoke + Rake::Task["db:migrate"].invoke + Rake::Task["db:schema:dump"].invoke + end + + desc "Build Redmine" + task :build do + Rake::Task["test"].invoke + end + + # Use this to cleanup after building or run post-build analysis. + desc "Finish the build" + task :teardown do + end + + desc "Dump the environment information to a BUILD_ENVIRONMENT ENV variable for debugging" + task :dump_environment do + + ENV['BUILD_ENVIRONMENT'] = ['ruby -v', 'gem -v', 'gem list'].collect do |command| + result = `#{command}` + "$ #{command}\n#{result}" + end.join("\n") + + end +end + diff --git a/lib/tasks/permissions.rake b/lib/tasks/permissions.rake new file mode 100644 index 000000000..02ce1b2a8 --- /dev/null +++ b/lib/tasks/permissions.rake @@ -0,0 +1,9 @@ +namespace :redmine do + desc "List all permissions and the actions registered with them" + task :permissions => :environment do + puts "Permission Name - controller/action pairs" + Redmine::AccessControl.permissions.sort {|a,b| a.name.to_s <=> b.name.to_s }.each do |permission| + puts ":#{permission.name} - #{permission.actions.join(', ')}" + end + end +end diff --git a/lib/tasks/reminder.rake b/lib/tasks/reminder.rake index 73844fb79..d11c7cebb 100644 --- a/lib/tasks/reminder.rake +++ b/lib/tasks/reminder.rake @@ -22,9 +22,10 @@ Available options: * days => number of days to remind about (defaults to 7) * tracker => id of tracker (defaults to all trackers) * project => id or identifier of project (defaults to all projects) + * users => comma separated list of user ids who should be reminded Example: - rake redmine:send_reminders days=7 RAILS_ENV="production" + rake redmine:send_reminders days=7 users="1,23, 56" RAILS_ENV="production" END_DESC namespace :redmine do @@ -33,6 +34,7 @@ namespace :redmine do options[:days] = ENV['days'].to_i if ENV['days'] options[:project] = ENV['project'] if ENV['project'] options[:tracker] = ENV['tracker'].to_i if ENV['tracker'] + options[:users] = (ENV['users'] || '').split(',').each(&:strip!) Mailer.reminders(options) end diff --git a/lib/tasks/yardoc.rake b/lib/tasks/yardoc.rake index c98f3bd4f..aa6c5eeb0 100644 --- a/lib/tasks/yardoc.rake +++ b/lib/tasks/yardoc.rake @@ -2,7 +2,17 @@ begin require 'yard' YARD::Rake::YardocTask.new do |t| - t.files = ['lib/**/*.rb', 'app/**/*.rb', 'vendor/plugins/**/*.rb'] + files = ['lib/**/*.rb', 'app/**/*.rb'] + files << Dir['vendor/plugins/**/*.rb'].reject {|f| f.match(/test/) } # Exclude test files + t.files = files + + static_files = ['doc/CHANGELOG', + 'doc/COPYING', + 'doc/INSTALL', + 'doc/RUNNING_TESTS', + 'doc/UPGRADING'].join(',') + + t.options += ['--output-dir', './doc/app', '--files', static_files] end rescue LoadError diff --git a/public/images/milestone.png b/public/images/milestone.png deleted file mode 100644 index a89791cf5..000000000 Binary files a/public/images/milestone.png and /dev/null differ diff --git a/public/images/milestone_done.png b/public/images/milestone_done.png new file mode 100644 index 000000000..5fdcb415c Binary files /dev/null and b/public/images/milestone_done.png differ diff --git a/public/images/milestone_late.png b/public/images/milestone_late.png new file mode 100644 index 000000000..cf922e954 Binary files /dev/null and b/public/images/milestone_late.png differ diff --git a/public/images/milestone_todo.png b/public/images/milestone_todo.png new file mode 100644 index 000000000..4c051c857 Binary files /dev/null and b/public/images/milestone_todo.png differ diff --git a/public/images/project_marker.png b/public/images/project_marker.png new file mode 100644 index 000000000..4124787d0 Binary files /dev/null and b/public/images/project_marker.png differ diff --git a/public/images/task_done.png b/public/images/task_done.png index 954ebedce..5fdcb415c 100644 Binary files a/public/images/task_done.png and b/public/images/task_done.png differ diff --git a/public/images/version_marker.png b/public/images/version_marker.png new file mode 100644 index 000000000..0368ca290 Binary files /dev/null and b/public/images/version_marker.png differ diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 612739f5c..8f6c51828 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -17,6 +17,13 @@ function toggleCheckboxesBySelector(selector) { for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; } } +function setCheckboxesBySelector(checked, selector) { + var boxes = $$(selector); + boxes.each(function(ele) { + ele.checked = checked; + }); +} + function showAndScrollTo(id, focus) { Element.show(id); if (focus!=null) { Form.Element.focus(focus); } @@ -52,11 +59,10 @@ function addFileField() { d.type = "text"; d.name = "attachments[" + fileFieldCount + "][description]"; d.size = 60; - var dLabel = document.createElement("label"); + var dLabel = new Element('label'); dLabel.addClassName('inline'); // Pulls the languge value used for Optional Description dLabel.update($('attachment_description_label_content').innerHTML) - p = document.getElementById("attachments_fields"); p.appendChild(document.createElement("br")); p.appendChild(f); diff --git a/public/javascripts/calendar/lang/calendar-ca.js b/public/javascripts/calendar/lang/calendar-ca.js index 303f21dfd..9902680e5 100644 --- a/public/javascripts/calendar/lang/calendar-ca.js +++ b/public/javascripts/calendar/lang/calendar-ca.js @@ -45,7 +45,7 @@ Calendar._SDN = new Array // First day of the week. "0" means display Sunday first, "1" means display // Monday first, etc. -Calendar._FD = 0; +Calendar._FD = 1; // full month names Calendar._MN = new Array @@ -84,17 +84,17 @@ Calendar._TT["INFO"] = "Quant al calendari"; Calendar._TT["ABOUT"] = "Selector DHTML de data/hora\n" + "(c) dynarch.com 2002-2005 / Autor: Mihai Bazon\n" + // don't translate this this ;-) -"Per a aconseguir l'última versió visiteu: http://www.dynarch.com/projects/calendar/\n" + -"Distribuït sota la llicència GNU LGPL. Vegeu http://gnu.org/licenses/lgpl.html per a més detalls." + +"Per aconseguir l'última versió visiteu: http://www.dynarch.com/projects/calendar/\n" + +"Distribuït sota la llicència GNU LGPL. Vegeu http://gnu.org/licenses/lgpl.html per obtenir més detalls." + "\n\n" + "Selecció de la data:\n" + -"- Utilitzeu els botons \xab, \xbb per a seleccionar l'any\n" + -"- Utilitzeu els botons " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " per a selecciona el mes\n" + -"- Mantingueu premut el botó del ratolí sobre qualsevol d'aquests botons per a uns selecció més ràpida."; +"- Utilitzeu els botons \xab, \xbb per seleccionar l'any\n" + +"- Utilitzeu els botons " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " per seleccionar el mes\n" + +"- Mantingueu premut el botó del ratolí sobre qualsevol d'aquests botons per a una selecció més ràpida."; Calendar._TT["ABOUT_TIME"] = "\n\n" + "Selecció de l'hora:\n" + -"- Feu clic en qualsevol part de l'hora per a incrementar-la\n" + -"- o premeu majúscules per a disminuir-la\n" + +"- Feu clic en qualsevol part de l'hora per incrementar-la\n" + +"- o premeu majúscules per disminuir-la\n" + "- o feu clic i arrossegueu per a una selecció més ràpida."; Calendar._TT["PREV_YEAR"] = "Any anterior (mantenir per menú)"; @@ -102,8 +102,8 @@ Calendar._TT["PREV_MONTH"] = "Mes anterior (mantenir per menú)"; Calendar._TT["GO_TODAY"] = "Anar a avui"; Calendar._TT["NEXT_MONTH"] = "Mes següent (mantenir per menú)"; Calendar._TT["NEXT_YEAR"] = "Any següent (mantenir per menú)"; -Calendar._TT["SEL_DATE"] = "Sel·lecciona data"; -Calendar._TT["DRAG_TO_MOVE"] = "Arrossega per a moure"; +Calendar._TT["SEL_DATE"] = "Sel·lecciona la data"; +Calendar._TT["DRAG_TO_MOVE"] = "Arrossega per moure"; Calendar._TT["PART_TODAY"] = " (avui)"; // the following is to inform that "%s" is to be the first day of week @@ -117,7 +117,7 @@ Calendar._TT["WEEKEND"] = "0,6"; Calendar._TT["CLOSE"] = "Tanca"; Calendar._TT["TODAY"] = "Avui"; -Calendar._TT["TIME_PART"] = "(Majúscules-)Feu clic o arrossegueu per a canviar el valor"; +Calendar._TT["TIME_PART"] = "(Majúscules-)Feu clic o arrossegueu per canviar el valor"; // date formats Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y"; diff --git a/public/javascripts/calendar/lang/calendar-it.js b/public/javascripts/calendar/lang/calendar-it.js index fbc80c935..2c3379c73 100644 --- a/public/javascripts/calendar/lang/calendar-it.js +++ b/public/javascripts/calendar/lang/calendar-it.js @@ -9,6 +9,9 @@ // Unicode is the answer to a real internationalized world. Also please // include your contact information in the header, as can be seen above. +// Italian translation +// by Diego Pierotto (ita.translations@tiscali.it) + // full day names Calendar._DN = new Array ("Domenica", @@ -83,19 +86,19 @@ Calendar._TT["INFO"] = "Informazioni sul calendario"; Calendar._TT["ABOUT"] = "DHTML Date/Time Selector\n" + -"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) -"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + -"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"(c) dynarch.com 2002-2005 / Autore: Mihai Bazon\n" + // don't translate this this ;-) +"Per l'ultima versione visita: http://www.dynarch.com/projects/calendar/\n" + +"Distribuito sotto i termini GNU LGPL. Vedi http://gnu.org/licenses/lgpl.html per maggiori dettagli." + "\n\n" + -"Date selection:\n" + -"- Use the \xab, \xbb buttons to select year\n" + -"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + -"- Hold mouse button on any of the above buttons for faster selection."; +"Selezione data:\n" + +"- Usa i tasti \xab, \xbb per selezionare l'anno\n" + +"- Usa i tasti " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " per selezionare il mese\n" + +"- Tieni premuto il tasto del mouse su uno qualunque dei tasti sopra per una selezione più veloce."; Calendar._TT["ABOUT_TIME"] = "\n\n" + -"Time selection:\n" + -"- Click on any of the time parts to increase it\n" + -"- or Shift-click to decrease it\n" + -"- or click and drag for faster selection."; +"Selezione ora:\n" + +"- Fai click su una delle ore per incrementarla\n" + +"- oppure Shift-click per diminuirla\n" + +"- oppure click e trascina per una selezione più veloce."; Calendar._TT["PREV_YEAR"] = "Anno prec. (tieni premuto per menu)"; Calendar._TT["PREV_MONTH"] = "Mese prec. (tieni premuto per menu)"; diff --git a/public/javascripts/calendar/lang/calendar-mk.js b/public/javascripts/calendar/lang/calendar-mk.js index 34fcaaee7..863e3bf2b 100644 --- a/public/javascripts/calendar/lang/calendar-mk.js +++ b/public/javascripts/calendar/lang/calendar-mk.js @@ -1,7 +1,7 @@ // ** I18N // Calendar МК language -// Author: Илин Татабитовски, +// Author: Ilin Tatabitovski, // Encoding: UTF-8 // Distributed under the same terms as the calendar itself. @@ -84,26 +84,26 @@ Calendar._TT["INFO"] = "За календарот"; Calendar._TT["ABOUT"] = "DHTML Date/Time Selector\n" + "(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-) -"For latest version visit: http://www.dynarch.com/projects/calendar/\n" + -"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." + +"За последна верзија посети: http://www.dynarch.com/projects/calendar/\n" + +"Дистрибуирано под GNU LGPL. Види http://gnu.org/licenses/lgpl.html за детали." + "\n\n" + -"Date selection:\n" + -"- Use the \xab, \xbb buttons to select year\n" + -"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" + -"- Hold mouse button on any of the above buttons for faster selection."; +"Бирање на дата:\n" + +"- Користи ги \xab, \xbb копчињата за да избереш година\n" + +"- Користи ги " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " копчињата за да избере месеци\n" + +"- Држи го притиснато копчето на глувчето на било кое копче за побрзо бирање."; Calendar._TT["ABOUT_TIME"] = "\n\n" + -"Time selection:\n" + -"- Click on any of the time parts to increase it\n" + -"- or Shift-click to decrease it\n" + -"- or click and drag for faster selection."; +"Бирање на време:\n" + +"- Клик на временските делови за да го зголемиш\n" + +"- или Shift-клик да го намалиш\n" + +"- или клик и влечи за побрзо бирање."; -Calendar._TT["PREV_YEAR"] = "Претходна година (hold for menu)"; -Calendar._TT["PREV_MONTH"] = "Претходен месец (hold for menu)"; +Calendar._TT["PREV_YEAR"] = "Претходна година (држи за мени)"; +Calendar._TT["PREV_MONTH"] = "Претходен месец (држи за мени)"; Calendar._TT["GO_TODAY"] = "Go Today"; -Calendar._TT["NEXT_MONTH"] = "Следен месец (hold for menu)"; -Calendar._TT["NEXT_YEAR"] = "Следна година (hold for menu)"; -Calendar._TT["SEL_DATE"] = "Изберете дата"; -Calendar._TT["DRAG_TO_MOVE"] = "Drag to move"; +Calendar._TT["NEXT_MONTH"] = "Следен месец (држи за мени)"; +Calendar._TT["NEXT_YEAR"] = "Следна година (држи за мени)"; +Calendar._TT["SEL_DATE"] = "Избери дата"; +Calendar._TT["DRAG_TO_MOVE"] = "Влечи да поместиш"; Calendar._TT["PART_TODAY"] = " (денес)"; // the following is to inform that "%s" is to be the first day of week @@ -117,7 +117,7 @@ Calendar._TT["WEEKEND"] = "0,6"; Calendar._TT["CLOSE"] = "Затвори"; Calendar._TT["TODAY"] = "Денес"; -Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value"; +Calendar._TT["TIME_PART"] = "(Shift-)Клик или влечи за да промениш вредност"; // date formats Calendar._TT["DEF_DATE_FORMAT"] = "%d-%m-%Y"; @@ -125,3 +125,4 @@ Calendar._TT["TT_DATE_FORMAT"] = "%a, %e %b"; Calendar._TT["WK"] = "нед"; Calendar._TT["TIME"] = "Време:"; + diff --git a/public/javascripts/calendar/lang/calendar-sr-CY.js b/public/javascripts/calendar/lang/calendar-sr-CY.js deleted file mode 100644 index a43b49e10..000000000 --- a/public/javascripts/calendar/lang/calendar-sr-CY.js +++ /dev/null @@ -1,127 +0,0 @@ -// ** I18N - -// Calendar SR language -// Author: Dragan Matic, -// Encoding: any -// Distributed under the same terms as the calendar itself. - -// For translators: please use UTF-8 if possible. We strongly believe that -// Unicode is the answer to a real internationalized world. Also please -// include your contact information in the header, as can be seen above. - -// full day names -Calendar._DN = new Array -("Недеља", - "Понедељак", - "Уторак", - "Среда", - "Четвртак", - "Петак", - "Субота", - "Недеља"); - -// Please note that the following array of short day names (and the same goes -// for short month names, _SMN) isn't absolutely necessary. We give it here -// for exemplification on how one can customize the short day names, but if -// they are simply the first N letters of the full name you can simply say: -// -// Calendar._SDN_len = N; // short day name length -// Calendar._SMN_len = N; // short month name length -// -// If N = 3 then this is not needed either since we assume a value of 3 if not -// present, to be compatible with translation files that were written before -// this feature. - -// short day names -Calendar._SDN = new Array -("Нед", - "Пон", - "Уто", - "Сре", - "Чет", - "Пет", - "Суб", - "Нед"); - -// First day of the week. "0" means display Sunday first, "1" means display -// Monday first, etc. -Calendar._FD = 1; - -// full month names -Calendar._MN = new Array -("Јануар", - "Фебруар", - "Март", - "Април", - "Мај", - "Јун", - "Јул", - "Август", - "Септембар", - "Октобар", - "Новембар", - "Децембар"); - -// short month names -Calendar._SMN = new Array -("јан", - "феб", - "мар", - "апр", - "мај", - "јун", - "јул", - "авг", - "сеп", - "окт", - "нов", - "дец"); - -// tooltips -Calendar._TT = {}; -Calendar._TT["INFO"] = "О календару"; - -Calendar._TT["ABOUT"] = -"DHTML бирач датума/времена\n" + -"(c) dynarch.com 2002-2005 / Аутор: Mihai Bazon\n" + // don't translate this this ;-) -"За новију верзију посетите: http://www.dynarch.com/projects/calendar/\n" + -"Дистрибуира се под GNU LGPL. Погледајте http://gnu.org/licenses/lgpl.html за детаљe." + -"\n\n" + -"Избор датума:\n" + -"- Користите \xab, \xbb тастере за избор године\n" + -"- Користите " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " тастере за избор месеца\n" + -"- Задржите тастер миша на било ком тастеру изнад за бржи избор."; -Calendar._TT["ABOUT_TIME"] = "\n\n" + -"Избор времена:\n" + -"- Кликните на било који део времена за повећање\n" + -"- или Shift-клик за умањење\n" + -"- или кликните и превуците за бржи одабир."; - -Calendar._TT["PREV_YEAR"] = "Претходна година (задржати за мени)"; -Calendar._TT["PREV_MONTH"] = "Претходни месец (задржати за мени)"; -Calendar._TT["GO_TODAY"] = "На данашњи дан"; -Calendar._TT["NEXT_MONTH"] = "Наредни месец (задржати за мени)"; -Calendar._TT["NEXT_YEAR"] = "Наредна година (задржати за мени)"; -Calendar._TT["SEL_DATE"] = "Избор датума"; -Calendar._TT["DRAG_TO_MOVE"] = "Превуците за премештање"; -Calendar._TT["PART_TODAY"] = " (данас)"; - -// the following is to inform that "%s" is to be the first day of week -// %s will be replaced with the day name. -Calendar._TT["DAY_FIRST"] = "%s као први дан у седмици"; - -// This may be locale-dependent. It specifies the week-end days, as an array -// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 -// means Monday, etc. -Calendar._TT["WEEKEND"] = "6,7"; - -Calendar._TT["CLOSE"] = "Затвори"; -Calendar._TT["TODAY"] = "Данас"; -Calendar._TT["TIME_PART"] = "(Shift-) клик или превлачење за измену вредности"; - -// date formats -Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y."; -Calendar._TT["TT_DATE_FORMAT"] = "%a, %e. %b"; - -Calendar._TT["WK"] = "сед."; -Calendar._TT["TIME"] = "Време:"; diff --git a/public/javascripts/calendar/lang/calendar-sr-yu.js b/public/javascripts/calendar/lang/calendar-sr-yu.js new file mode 100644 index 000000000..8fd5ceb83 --- /dev/null +++ b/public/javascripts/calendar/lang/calendar-sr-yu.js @@ -0,0 +1,127 @@ +// ** I18N + +// Calendar SR language +// Author: Dragan Matic, +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("nedelja", + "ponedeljak", + "utorak", + "sreda", + "četvrtak", + "petak", + "subota", + "nedelja"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("ned", + "pon", + "uto", + "sre", + "čet", + "pet", + "sub", + "ned"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("januar", + "februar", + "mart", + "april", + "maj", + "jun", + "jul", + "avgust", + "septembar", + "oktobar", + "novembar", + "decembar"); + +// short month names +Calendar._SMN = new Array +("jan", + "feb", + "mar", + "apr", + "maj", + "jun", + "jul", + "avg", + "sep", + "okt", + "nov", + "dec"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "O kalendaru"; + +Calendar._TT["ABOUT"] = +"DHTML birač datuma/vremena\n" + +"(c) dynarch.com 2002-2005 / Autor: Mihai Bazon\n" + // don't translate this this ;-) +"Za noviju verziju posetite: http://www.dynarch.com/projects/calendar/\n" + +"Distribuira se pod GNU LGPL. Pogledajte http://gnu.org/licenses/lgpl.html za detalje." + +"\n\n" + +"Izbor datuma:\n" + +"- Koristite \xab, \xbb tastere za izbor godine\n" + +"- Koristite " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " tastere za izbor meseca\n" + +"- Zadržite taster miša na bilo kom tasteru iznad za brži izbor."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Izbor vremena:\n" + +"- Kliknite na bilo koji deo vremena za povećanje\n" + +"- ili Shift-klik za umanjenje\n" + +"- ili kliknite i prevucite za brži odabir."; + +Calendar._TT["PREV_YEAR"] = "Prethodna godina (zadržati za meni)"; +Calendar._TT["PREV_MONTH"] = "Prethodni mesec (zadržati za meni)"; +Calendar._TT["GO_TODAY"] = "Na današnji dan"; +Calendar._TT["NEXT_MONTH"] = "Naredni mesec (zadržati za meni)"; +Calendar._TT["NEXT_YEAR"] = "Naredna godina (zadržati za meni)"; +Calendar._TT["SEL_DATE"] = "Izbor datuma"; +Calendar._TT["DRAG_TO_MOVE"] = "Prevucite za premeštanje"; +Calendar._TT["PART_TODAY"] = " (danas)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "%s kao prvi dan u sedmici"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "6,7"; + +Calendar._TT["CLOSE"] = "Zatvori"; +Calendar._TT["TODAY"] = "Danas"; +Calendar._TT["TIME_PART"] = "(Shift-) klik ili prevlačenje za izmenu vrednosti"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y."; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %e. %b"; + +Calendar._TT["WK"] = "sed."; +Calendar._TT["TIME"] = "Vreme:"; diff --git a/public/javascripts/calendar/lang/calendar-sr.js b/public/javascripts/calendar/lang/calendar-sr.js index 676373349..2fa58d73c 100644 --- a/public/javascripts/calendar/lang/calendar-sr.js +++ b/public/javascripts/calendar/lang/calendar-sr.js @@ -1,127 +1,127 @@ -// ** I18N - -// Calendar SR language -// Author: Dragan Matic, -// Encoding: any -// Distributed under the same terms as the calendar itself. - -// For translators: please use UTF-8 if possible. We strongly believe that -// Unicode is the answer to a real internationalized world. Also please -// include your contact information in the header, as can be seen above. - -// full day names -Calendar._DN = new Array -("Nedelja", - "Ponedeljak", - "Utorak", - "Sreda", - "Četvrtak", - "Petak", - "Subota", - "Nedelja"); - -// Please note that the following array of short day names (and the same goes -// for short month names, _SMN) isn't absolutely necessary. We give it here -// for exemplification on how one can customize the short day names, but if -// they are simply the first N letters of the full name you can simply say: -// -// Calendar._SDN_len = N; // short day name length -// Calendar._SMN_len = N; // short month name length -// -// If N = 3 then this is not needed either since we assume a value of 3 if not -// present, to be compatible with translation files that were written before -// this feature. - -// short day names -Calendar._SDN = new Array -("Ned", - "Pon", - "Uto", - "Sre", - "Čet", - "Pet", - "Sub", - "Ned"); - -// First day of the week. "0" means display Sunday first, "1" means display -// Monday first, etc. -Calendar._FD = 1; - -// full month names -Calendar._MN = new Array -("Januar", - "Februar", - "Mart", - "April", - "Maj", - "Jun", - "Jul", - "Avgust", - "Septembar", - "Oktobar", - "Novembar", - "Decembar"); - -// short month names -Calendar._SMN = new Array -("jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "avg", - "sep", - "okt", - "nov", - "dec"); - -// tooltips -Calendar._TT = {}; -Calendar._TT["INFO"] = "O kalendaru"; - -Calendar._TT["ABOUT"] = -"DHTML birač datuma/vremena\n" + -"(c) dynarch.com 2002-2005 / Autor: Mihai Bazon\n" + // don't translate this this ;-) -"Za noviju verziju posetite: http://www.dynarch.com/projects/calendar/\n" + -"Distribuira se pod GNU LGPL. Pogledajte http://gnu.org/licenses/lgpl.html za detalje." + -"\n\n" + -"Izbor datuma:\n" + -"- Koristite \xab, \xbb tastere za izbor godine\n" + -"- Koristite " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " tastere za izbor meseca\n" + -"- Zadržite taster miša na bilo kom tasteru iznad za brži izbor."; -Calendar._TT["ABOUT_TIME"] = "\n\n" + -"Izbor vremena:\n" + -"- Kliknite na bilo koji deo vremena za povećanje\n" + -"- ili Shift-klik za umanjenje\n" + -"- ili kliknite i prevucite za brži odabir."; - -Calendar._TT["PREV_YEAR"] = "Prethodna godina (zadržati za meni)"; -Calendar._TT["PREV_MONTH"] = "Prethodni mesec (zadržati za meni)"; -Calendar._TT["GO_TODAY"] = "Na današnji dan"; -Calendar._TT["NEXT_MONTH"] = "Naredni mesec (zadržati za meni)"; -Calendar._TT["NEXT_YEAR"] = "Naredna godina (zadržati za meni)"; -Calendar._TT["SEL_DATE"] = "Izbor datuma"; -Calendar._TT["DRAG_TO_MOVE"] = "Prevucite za premeštanje"; -Calendar._TT["PART_TODAY"] = " (danas)"; - -// the following is to inform that "%s" is to be the first day of week -// %s will be replaced with the day name. -Calendar._TT["DAY_FIRST"] = "%s kao prvi dan u sedmici"; - -// This may be locale-dependent. It specifies the week-end days, as an array -// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 -// means Monday, etc. -Calendar._TT["WEEKEND"] = "6,7"; - -Calendar._TT["CLOSE"] = "Zatvori"; -Calendar._TT["TODAY"] = "Danas"; -Calendar._TT["TIME_PART"] = "(Shift-) klik ili prevlačenje za izmenu vrednosti"; - -// date formats -Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y."; -Calendar._TT["TT_DATE_FORMAT"] = "%a, %e. %b"; - -Calendar._TT["WK"] = "sed."; -Calendar._TT["TIME"] = "Vreme:"; +// ** I18N + +// Calendar SR language +// Author: Dragan Matic, +// Encoding: any +// Distributed under the same terms as the calendar itself. + +// For translators: please use UTF-8 if possible. We strongly believe that +// Unicode is the answer to a real internationalized world. Also please +// include your contact information in the header, as can be seen above. + +// full day names +Calendar._DN = new Array +("недеља", + "понедељак", + "уторак", + "среда", + "четвртак", + "петак", + "субота", + "недеља"); + +// Please note that the following array of short day names (and the same goes +// for short month names, _SMN) isn't absolutely necessary. We give it here +// for exemplification on how one can customize the short day names, but if +// they are simply the first N letters of the full name you can simply say: +// +// Calendar._SDN_len = N; // short day name length +// Calendar._SMN_len = N; // short month name length +// +// If N = 3 then this is not needed either since we assume a value of 3 if not +// present, to be compatible with translation files that were written before +// this feature. + +// short day names +Calendar._SDN = new Array +("нед", + "пон", + "уто", + "сре", + "чет", + "пет", + "суб", + "нед"); + +// First day of the week. "0" means display Sunday first, "1" means display +// Monday first, etc. +Calendar._FD = 1; + +// full month names +Calendar._MN = new Array +("јануар", + "фебруар", + "март", + "април", + "мај", + "јун", + "јул", + "август", + "септембар", + "октобар", + "новембар", + "децембар"); + +// short month names +Calendar._SMN = new Array +("јан", + "феб", + "мар", + "апр", + "мај", + "јун", + "јул", + "авг", + "сеп", + "окт", + "нов", + "дец"); + +// tooltips +Calendar._TT = {}; +Calendar._TT["INFO"] = "О календару"; + +Calendar._TT["ABOUT"] = +"DHTML бирач датума/времена\n" + +"(c) dynarch.com 2002-2005 / Аутор: Mihai Bazon\n" + // don't translate this this ;-) +"За новију верзију посетите: http://www.dynarch.com/projects/calendar/\n" + +"Дистрибуира се под GNU LGPL. Погледајте http://gnu.org/licenses/lgpl.html за детаљe." + +"\n\n" + +"Избор датума:\n" + +"- Користите \xab, \xbb тастере за избор године\n" + +"- Користите " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " тастере за избор месеца\n" + +"- Задржите тастер миша на било ком тастеру изнад за бржи избор."; +Calendar._TT["ABOUT_TIME"] = "\n\n" + +"Избор времена:\n" + +"- Кликните на било који део времена за повећање\n" + +"- или Shift-клик за умањење\n" + +"- или кликните и превуците за бржи одабир."; + +Calendar._TT["PREV_YEAR"] = "Претходна година (задржати за мени)"; +Calendar._TT["PREV_MONTH"] = "Претходни месец (задржати за мени)"; +Calendar._TT["GO_TODAY"] = "На данашњи дан"; +Calendar._TT["NEXT_MONTH"] = "Наредни месец (задржати за мени)"; +Calendar._TT["NEXT_YEAR"] = "Наредна година (задржати за мени)"; +Calendar._TT["SEL_DATE"] = "Избор датума"; +Calendar._TT["DRAG_TO_MOVE"] = "Превуците за премештање"; +Calendar._TT["PART_TODAY"] = " (данас)"; + +// the following is to inform that "%s" is to be the first day of week +// %s will be replaced with the day name. +Calendar._TT["DAY_FIRST"] = "%s као први дан у седмици"; + +// This may be locale-dependent. It specifies the week-end days, as an array +// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1 +// means Monday, etc. +Calendar._TT["WEEKEND"] = "6,7"; + +Calendar._TT["CLOSE"] = "Затвори"; +Calendar._TT["TODAY"] = "Данас"; +Calendar._TT["TIME_PART"] = "(Shift-) клик или превлачење за измену вредности"; + +// date formats +Calendar._TT["DEF_DATE_FORMAT"] = "%d.%m.%Y."; +Calendar._TT["TT_DATE_FORMAT"] = "%a, %e. %b"; + +Calendar._TT["WK"] = "сед."; +Calendar._TT["TIME"] = "Време:"; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-it.js b/public/javascripts/jstoolbar/lang/jstoolbar-it.js index bf7fcefb2..99749b40c 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-it.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-it.js @@ -1,3 +1,6 @@ +// Italian translation +// by Diego Pierotto (ita.translations@tiscali.it) + jsToolBar.strings = {}; jsToolBar.strings['Strong'] = 'Grassetto'; jsToolBar.strings['Italic'] = 'Corsivo'; @@ -8,7 +11,7 @@ jsToolBar.strings['Heading 1'] = 'Titolo 1'; jsToolBar.strings['Heading 2'] = 'Titolo 2'; jsToolBar.strings['Heading 3'] = 'Titolo 3'; jsToolBar.strings['Unordered list'] = 'Elenco puntato'; -jsToolBar.strings['Ordered list'] = 'Numerazione'; +jsToolBar.strings['Ordered list'] = 'Elenco numerato'; jsToolBar.strings['Quote'] = 'Aumenta rientro'; jsToolBar.strings['Unquote'] = 'Riduci rientro'; jsToolBar.strings['Preformatted text'] = 'Testo preformattato'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-mk.js b/public/javascripts/jstoolbar/lang/jstoolbar-mk.js index 3af63a1cc..30c68ec6c 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-mk.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-mk.js @@ -1,16 +1,17 @@ jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Strong'; -jsToolBar.strings['Italic'] = 'Italic'; -jsToolBar.strings['Underline'] = 'Underline'; -jsToolBar.strings['Deleted'] = 'Deleted'; -jsToolBar.strings['Code'] = 'Inline Code'; -jsToolBar.strings['Heading 1'] = 'Heading 1'; -jsToolBar.strings['Heading 2'] = 'Heading 2'; -jsToolBar.strings['Heading 3'] = 'Heading 3'; -jsToolBar.strings['Unordered list'] = 'Unordered list'; +jsToolBar.strings['Strong'] = 'Задебелен'; +jsToolBar.strings['Italic'] = 'Закосен'; +jsToolBar.strings['Underline'] = 'Подвлечен'; +jsToolBar.strings['Deleted'] = 'Прецртан'; +jsToolBar.strings['Code'] = 'Код'; +jsToolBar.strings['Heading 1'] = 'Заглавје 1'; +jsToolBar.strings['Heading 2'] = 'Заглавје 2'; +jsToolBar.strings['Heading 3'] = 'Заглавје 3'; +jsToolBar.strings['Unordered list'] = 'Неподредена листа'; jsToolBar.strings['Ordered list'] = 'Подредена листа'; jsToolBar.strings['Quote'] = 'Цитат'; jsToolBar.strings['Unquote'] = 'Отстрани цитат'; -jsToolBar.strings['Preformatted text'] = 'Preformatted text'; -jsToolBar.strings['Wiki link'] = 'Линк до вики страна'; +jsToolBar.strings['Preformatted text'] = 'Форматиран текст'; +jsToolBar.strings['Wiki link'] = 'Врска до вики страна'; jsToolBar.strings['Image'] = 'Слика'; + diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-sr-yu.js b/public/javascripts/jstoolbar/lang/jstoolbar-sr-yu.js new file mode 100644 index 000000000..0e231e029 --- /dev/null +++ b/public/javascripts/jstoolbar/lang/jstoolbar-sr-yu.js @@ -0,0 +1,16 @@ +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Podebljano'; +jsToolBar.strings['Italic'] = 'Kurziv'; +jsToolBar.strings['Underline'] = 'Podvučeno'; +jsToolBar.strings['Deleted'] = 'Obrisano'; +jsToolBar.strings['Code'] = 'Ugrađeni kôd'; +jsToolBar.strings['Heading 1'] = 'Naslov 1'; +jsToolBar.strings['Heading 2'] = 'Naslov 2'; +jsToolBar.strings['Heading 3'] = 'Naslov 3'; +jsToolBar.strings['Unordered list'] = 'Lista nabrajanja'; +jsToolBar.strings['Ordered list'] = 'Uređena lista'; +jsToolBar.strings['Quote'] = 'Pod navodnicima'; +jsToolBar.strings['Unquote'] = 'Ukloni navodnike'; +jsToolBar.strings['Preformatted text'] = 'Prethodno formatiran tekst'; +jsToolBar.strings['Wiki link'] = 'Veza prema Wiki strani'; +jsToolBar.strings['Image'] = 'Slika'; diff --git a/public/javascripts/jstoolbar/lang/jstoolbar-sr.js b/public/javascripts/jstoolbar/lang/jstoolbar-sr.js index 0e231e029..75a768ad0 100644 --- a/public/javascripts/jstoolbar/lang/jstoolbar-sr.js +++ b/public/javascripts/jstoolbar/lang/jstoolbar-sr.js @@ -1,16 +1,16 @@ -jsToolBar.strings = {}; -jsToolBar.strings['Strong'] = 'Podebljano'; -jsToolBar.strings['Italic'] = 'Kurziv'; -jsToolBar.strings['Underline'] = 'Podvučeno'; -jsToolBar.strings['Deleted'] = 'Obrisano'; -jsToolBar.strings['Code'] = 'Ugrađeni kôd'; -jsToolBar.strings['Heading 1'] = 'Naslov 1'; -jsToolBar.strings['Heading 2'] = 'Naslov 2'; -jsToolBar.strings['Heading 3'] = 'Naslov 3'; -jsToolBar.strings['Unordered list'] = 'Lista nabrajanja'; -jsToolBar.strings['Ordered list'] = 'Uređena lista'; -jsToolBar.strings['Quote'] = 'Pod navodnicima'; -jsToolBar.strings['Unquote'] = 'Ukloni navodnike'; -jsToolBar.strings['Preformatted text'] = 'Prethodno formatiran tekst'; -jsToolBar.strings['Wiki link'] = 'Veza prema Wiki strani'; -jsToolBar.strings['Image'] = 'Slika'; +jsToolBar.strings = {}; +jsToolBar.strings['Strong'] = 'Подебљано'; +jsToolBar.strings['Italic'] = 'Курзив'; +jsToolBar.strings['Underline'] = 'Подвучено'; +jsToolBar.strings['Deleted'] = 'Обрисано'; +jsToolBar.strings['Code'] = 'Уграђени кôд'; +jsToolBar.strings['Heading 1'] = 'Наслов 1'; +jsToolBar.strings['Heading 2'] = 'Наслов 2'; +jsToolBar.strings['Heading 3'] = 'Наслов 3'; +jsToolBar.strings['Unordered list'] = 'Листа набрајања'; +jsToolBar.strings['Ordered list'] = 'Уређена листа'; +jsToolBar.strings['Quote'] = 'Под наводницима'; +jsToolBar.strings['Unquote'] = 'Уклони наводнике'; +jsToolBar.strings['Preformatted text'] = 'Претходно форматиран текст'; +jsToolBar.strings['Wiki link'] = 'Веза према Wiki страни'; +jsToolBar.strings['Image'] = 'Слика'; diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 60c50d2b2..b286ee23c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -287,8 +287,8 @@ fieldset#filters td.add-filter { text-align: right; vertical-align: top; } .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} -div#issue-changesets .changeset { padding: 4px;} -div#issue-changesets .changeset { border-bottom: 1px solid #ddd; } +div#issue-changesets div.changeset { padding: 4px;} +div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} div#activity dl, #search-results { margin-left: 2em; } @@ -419,6 +419,7 @@ input#time_entry_comments { width: 90%;} .tabular.settings textarea { width: 99%; } fieldset.settings label { display: block; } +.parent { padding-left: 20px; } .required {color: #bb0000;} .summary {font-style: italic;} @@ -697,6 +698,7 @@ div.wiki pre { border: 1px solid #dadada; width:auto; overflow-x: auto; + overflow-y: hidden; } div.wiki ul.toc { @@ -786,8 +788,10 @@ background-image:url('../images/close_hl.png'); white-space:nowrap; } +.task.label {width:100%;} + .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } -.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; } +.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;} @@ -795,7 +799,17 @@ background-image:url('../images/close_hl.png'); .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;} .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;} -.milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; } +.milestone { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; } +.milestone_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} +.milestone_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} +.milestone_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} +.project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; } +.project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} +.project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} +.project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} + +.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} +.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} /***** Icons *****/ .icon { @@ -839,6 +853,7 @@ padding-bottom: 3px; .icon-comment { background-image: url(../images/comment.png); } .icon-summary { background-image: url(../images/lightning.png); } .icon-server-authentication { background-image: url(../images/server_key.png); } +.icon-issue { background-image: url(../images/ticket.png); } .icon-file { background-image: url(../images/files/default.png); } .icon-file.text-plain { background-image: url(../images/files/text.png); } @@ -897,11 +912,21 @@ td.username img.gravatar { margin: 0 1em 1em 0; } +/* Used on 12px Gravatar img tags without the icon background */ +.icon-gravatar { + float: left; + margin-right: 4px; +} + #activity dt, .journal { clear: left; } +.journal-link { + float: right; +} + h2 img { vertical-align:middle; } .hascontextmenu { cursor: context-menu; } diff --git a/public/stylesheets/calendar.css b/public/stylesheets/calendar.css index c8d2dd619..288ed724f 100644 --- a/public/stylesheets/calendar.css +++ b/public/stylesheets/calendar.css @@ -8,7 +8,7 @@ img.calendar-trigger { div.calendar { position: relative; z-index: 30;} -.calendar, .calendar table { +div.calendar, div.calendar table { border: 1px solid #556; font-size: 11px; color: #000; @@ -19,16 +19,16 @@ div.calendar { position: relative; z-index: 30;} /* Header part -- contains navigation buttons and day names. */ -.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */ +div.calendar .button { /* "<<", "<", ">", ">>" buttons have this class */ text-align: center; /* They are the navigation buttons */ padding: 2px; /* Make the buttons seem like they're pressing */ } -.calendar .nav { +div.calendar .nav { background: #467aa7; } -.calendar thead .title { /* This holds the current "month, year" */ +div.calendar thead .title { /* This holds the current "month, year" */ font-weight: bold; /* Pressing it will take you to the current date */ text-align: center; background: #fff; @@ -36,79 +36,79 @@ div.calendar { position: relative; z-index: 30;} padding: 2px; } -.calendar thead .headrow { /* Row
    containing navigation buttons */ +div.calendar thead .headrow { /* Row containing navigation buttons */ background: #467aa7; color: #fff; } -.calendar thead .daynames { /* Row containing the day names */ +div.calendar thead .daynames { /* Row containing the day names */ background: #bdf; } -.calendar thead .name { /* Cells in footer (only one right now) */ +div.calendar tfoot .footrow { /* The in footer (only one right now) */ text-align: center; background: #556; color: #fff; } -.calendar tfoot .ttip { /* Tooltip (status bar) cell
    @@ -66,26 +70,10 @@ t_height = g_height + headers_height
    -<% -# -# Tasks subjects -# -top = headers_height + 8 -@gantt.events.each do |i| -left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0) - %> -
    - <% if i.is_a? Issue %> - <%= h("#{i.project} -") unless @project && @project == i.project %> - <%= link_to_issue i %> - <% else %> - - <%= link_to_version i %> - - <% end %> -
    - <% top = top + 20 -end %> +<% top = headers_height + 8 %> + +<%= @gantt.subjects(:headers_height => headers_height, :top => top, :g_width => g_width) %> +
    @@ -163,53 +151,9 @@ if show_days end end %> -<% -# -# Tasks -# -top = headers_height + 10 -@gantt.events.each do |i| - if i.is_a? Issue - i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from ) - i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to ) - - i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor - i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date ) - i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date ) - - i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today - - i_left = ((i_start_date - @gantt.date_from)*zoom).floor - i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders) - d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width - l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width - css = "task " + (i.leaf? ? 'leaf' : 'parent') - %> -
     
    - <% if l_width > 0 %> -
     
    - <% end %> - <% if d_width > 0 %> -
     
    - <% end %> -
    - <%= i.status.name %> - <%= (i.done_ratio).to_i %>% -
    -
    - - <%= render_issue_tooltip i %> -
    -<% else - i_left = ((i.start_date - @gantt.date_from)*zoom).floor - %> -
     
    -
    - <%= format_version_name i %> -
    -<% end %> - <% top = top + 20 -end %> +<% top = headers_height + 10 %> + +<%= @gantt.lines(:top => top, :zoom => zoom, :g_width => g_width ) %> <% # @@ -226,8 +170,8 @@ if Date.today >= @gantt.date_from and Date.today <= @gantt.date_to %> - - + +
    <%= link_to_remote ('« ' + l(:label_previous)), {:url => @gantt.params_previous, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %><%= link_to_remote (l(:label_next) + ' »'), {:url => @gantt.params_next, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %><%= link_to_remote ('« ' + l(:label_previous)), {:url => @gantt.params_previous, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_previous)} %><%= link_to_remote (l(:label_next) + ' »'), {:url => @gantt.params_next, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'}, {:href => url_for(@gantt.params_next)} %>
    diff --git a/app/views/issues/move.rhtml b/app/views/issue_moves/new.rhtml similarity index 96% rename from app/views/issues/move.rhtml rename to app/views/issue_moves/new.rhtml index 47388c36b..2dc971df2 100644 --- a/app/views/issues/move.rhtml +++ b/app/views/issue_moves/new.rhtml @@ -6,14 +6,14 @@ <% end -%> -<% form_tag({}, :id => 'move_form') do %> +<% form_tag({:action => 'create'}, :id => 'move_form') do %> <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>

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

    diff --git a/app/views/issues/_action_menu.rhtml b/app/views/issues/_action_menu.rhtml index 693b49237..8a24ac853 100644 --- a/app/views/issues/_action_menu.rhtml +++ b/app/views/issues/_action_menu.rhtml @@ -1,10 +1,10 @@
    <%= 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-add' %> +<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %> <% replace_watcher ||= 'watcher' %> <%= watcher_tag(@issue, User.current, {:id => replace_watcher, :replace => ['watcher','watcher2']}) %> <%= link_to_if_authorized l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-duplicate' %> -<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'move', :id => @issue, :copy_options => {:copy => 't'} }, :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' %> +<%= link_to_if_authorized l(:button_copy), new_issue_move_path(:id => @issue, :copy_options => {:copy => 't'}), :class => 'icon icon-copy' %> +<%= link_to_if_authorized l(:button_move), new_issue_move_path(:id => @issue), :class => 'icon icon-move' %> +<%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => (@issue.leaf? ? l(:text_are_you_sure) : l(:text_are_you_sure_with_children)), :method => :post, :class => 'icon icon-del' %>
    diff --git a/app/views/issues/_attributes.rhtml b/app/views/issues/_attributes.rhtml index 455eb77b2..e10858b09 100644 --- a/app/views/issues/_attributes.rhtml +++ b/app/views/issues/_attributes.rhtml @@ -23,7 +23,7 @@ <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'), l(:label_version_new), 'version[name]', - {:controller => 'versions', :action => 'new', :project_id => @project}, + {:controller => 'versions', :action => 'create', :project_id => @project}, :title => l(:label_version_new), :tabindex => 200) if authorize_for('versions', 'new') %>

    @@ -40,6 +40,6 @@
    -<%= render :partial => 'form_custom_fields' %> +<%= render :partial => 'issues/form_custom_fields' %> <% end %> diff --git a/app/views/issues/_changesets.rhtml b/app/views/issues/_changesets.rhtml index 0b1f10b72..52cd60ff5 100644 --- a/app/views/issues/_changesets.rhtml +++ b/app/views/issues/_changesets.rhtml @@ -3,6 +3,8 @@

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

    - <%= textilizable(changeset, :comments) %> +
    + <%= textilizable(changeset, :comments) %> +
    <% end %> diff --git a/app/views/issues/_edit.rhtml b/app/views/issues/_edit.rhtml index 0c01f80be..ec36b1459 100644 --- a/app/views/issues/_edit.rhtml +++ b/app/views/issues/_edit.rhtml @@ -44,7 +44,7 @@ <%= 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 }, + { :url => preview_issue_path(:project_id => @project, :id => @issue), :method => 'post', :update => 'preview', :with => 'Form.serialize("issue-form")', diff --git a/app/views/issues/_form.rhtml b/app/views/issues/_form.rhtml index 1e3beaf85..136e1decc 100644 --- a/app/views/issues/_form.rhtml +++ b/app/views/issues/_form.rhtml @@ -1,6 +1,8 @@ +<%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %> +
    >

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

    -<%= observe_field :issue_tracker_id, :url => { :action => :update_form, :project_id => @project, :id => @issue }, +<%= observe_field :issue_tracker_id, :url => { :action => :new, :project_id => @project, :id => @issue }, :update => :attributes, :with => "Form.serialize('issue-form')" %> @@ -9,10 +11,7 @@ <% unless (@issue.new_record? && @issue.parent_issue_id.nil?) || !User.current.allowed_to?(:manage_subtasks, @project) %>

    <%= f.text_field :parent_issue_id, :size => 10 %>

    -<%= javascript_tag "observeParentIssueField('#{url_for(:controller => :issues, - :action => :auto_complete, - :id => @issue, - :project_id => @project) }')" %> +<%= javascript_tag "observeParentIssueField('#{auto_complete_issues_path(:id => @issue, :project_id => @project) }')" %> <% end %>

    <%= f.text_area :description, @@ -23,7 +22,7 @@

    - <%= render :partial => 'attributes' %> + <%= render :partial => 'issues/attributes' %>
    <% if @issue.new_record? %> diff --git a/app/views/issues/_history.rhtml b/app/views/issues/_history.rhtml index 7459eb352..4851e5f22 100644 --- a/app/views/issues/_history.rhtml +++ b/app/views/issues/_history.rhtml @@ -1,7 +1,7 @@ <% reply_links = authorize_for('issues', 'edit') -%> <% for journal in journals %> -
    -

    <%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %>
    +
    +

    <%= avatar(journal.user, :size => "24") %> <%= content_tag('a', '', :name => "note-#{journal.indice}")%> <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>

    diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml index 2722b9e7b..1a2953bf5 100644 --- a/app/views/issues/_list.rhtml +++ b/app/views/issues/_list.rhtml @@ -3,7 +3,7 @@
    - <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> @@ -25,7 +25,7 @@ <% previous_group = group %> <% end %> "> - + <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> diff --git a/app/views/issues/_list_simple.rhtml b/app/views/issues/_list_simple.rhtml index 38823765e..dd7f48946 100644 --- a/app/views/issues/_list_simple.rhtml +++ b/app/views/issues/_list_simple.rhtml @@ -14,7 +14,7 @@ <%= check_box_tag("ids[]", issue.id, false, :style => 'display:none;') %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %> - + <% if User.current.allowed_to?(:view_time_entries, @project) %> - + <% end %> @@ -128,6 +128,7 @@ <%= stylesheet_link_tag 'scm' %> <%= javascript_include_tag 'context_menu' %> <%= stylesheet_link_tag 'context_menu' %> + <%= stylesheet_link_tag 'context_menu_rtl' if l(:direction) == 'rtl' %> <% end %> -<%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %> +<%= javascript_tag "new ContextMenu('#{issues_context_menu_path}')" %> diff --git a/app/views/issues/changes.rxml b/app/views/journals/index.rxml similarity index 100% rename from app/views/issues/changes.rxml rename to app/views/journals/index.rxml diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml index 1873191c0..0393815b0 100644 --- a/app/views/layouts/base.rhtml +++ b/app/views/layouts/base.rhtml @@ -5,7 +5,9 @@ <%=h html_title %> +<%= favicon %> <%= stylesheet_link_tag 'application', :media => 'all' %> +<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %> <%= javascript_include_tag :defaults %> <%= heads_for_wiki_formatter %> <%= yield :header_tags -%> - +
    diff --git a/app/views/messages/show.rhtml b/app/views/messages/show.rhtml index 5c949d48f..a27b5d114 100644 --- a/app/views/messages/show.rhtml +++ b/app/views/messages/show.rhtml @@ -30,7 +30,7 @@

    <%= avatar(message.author, :size => "24") %> - <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :anchor => "message-#{message.id}" } %> + <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %> - <%= authoring message.created_on, message.author %>

    diff --git a/app/views/my/account.rhtml b/app/views/my/account.rhtml index befe6be5a..99b58ffe7 100644 --- a/app/views/my/account.rhtml +++ b/app/views/my/account.rhtml @@ -32,24 +32,12 @@

    <%=l(:field_mail_notification)%>

    -<%= select_tag 'notification_option', options_for_select(@notification_options, @notification_option), - :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 %> -

    +<%= render :partial => 'users/mail_notifications' %>

    <%=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, ActiveSupport::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 %> +<%= render :partial => 'users/preferences' %>
    diff --git a/app/views/news/_news.rhtml b/app/views/news/_news.rhtml index e95e8a557..ddbaecabf 100644 --- a/app/views/news/_news.rhtml +++ b/app/views/news/_news.rhtml @@ -1,5 +1,5 @@ -

    <%= 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 %> +

    <%= link_to_project(news.project) + ': ' unless @project %> +<%= link_to h(news.title), news_path(news) %> <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %>
    <% unless news.summary.blank? %><%=h news.summary %>
    <% end %> diff --git a/app/views/news/edit.rhtml b/app/views/news/edit.rhtml index 4be566e0b..0d44bfbb9 100644 --- a/app/views/news/edit.rhtml +++ b/app/views/news/edit.rhtml @@ -1,14 +1,18 @@

    <%=l(:label_news)%>

    -<% labelled_tabular_form_for :news, @news, :url => { :action => "edit" }, - :html => { :id => 'news-form' } do |f| %> +<% labelled_tabular_form_for :news, @news, :url => news_path(@news), + :html => { :id => 'news-form', :method => :put } do |f| %> <%= render :partial => 'form', :locals => { :f => f } %> <%= submit_tag l(:button_save) %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'news', :action => 'preview', :project_id => @project }, - :method => 'post', + { :url => preview_news_path(:project_id => @project), + :method => 'get', :update => 'preview', :with => "Form.serialize('news-form')" }, :accesskey => accesskey(:preview) %> <% end %>
    + +<% content_for :header_tags do %> + <%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/news/index.rhtml b/app/views/news/index.rhtml index 11a39232c..c2a227fe0 100644 --- a/app/views/news/index.rhtml +++ b/app/views/news/index.rhtml @@ -1,19 +1,19 @@
    <%= link_to_if_authorized(l(:label_news_new), - {:controller => 'news', :action => 'new', :project_id => @project}, + new_project_news_path(@project), :class => 'icon icon-add', :onclick => 'Element.show("add-news"); Form.Element.focus("news_title"); return false;') if @project %>
    <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', + <%= 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("ids[]", issue.id, false, :id => nil) %><%= check_box_tag("ids[]", issue.id, false, :id => nil) %> <%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
    <%= link_to(h(issue.project), :controller => 'projects', :action => 'show', :id => issue.project) %><%= link_to_project(issue.project) %> <%=h issue.tracker %> <%= link_to h(truncate(issue.subject, :length => 60)), :controller => 'issues', :action => 'show', :id => issue %> (<%=h issue.status %>) diff --git a/app/views/issues/_relations.rhtml b/app/views/issues/_relations.rhtml index 83a78b1c8..5b27fa6a5 100644 --- a/app/views/issues/_relations.rhtml +++ b/app/views/issues/_relations.rhtml @@ -1,6 +1,6 @@
    <% if authorize_for('issue_relations', 'new') %> - <%= toggle_link l(:button_add), 'new-relation-form'%> + <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %> <% end %>
    @@ -28,6 +28,7 @@ <% remote_form_for(:relation, @relation, :url => {:controller => 'issue_relations', :action => 'new', :issue_id => @issue}, :method => :post, + :complete => "Form.Element.focus('relation_issue_to_id');", :html => {:id => 'new-relation-form', :style => (@relation ? '' : 'display: none;')}) do |f| %> <%= render :partial => 'issue_relations/form', :locals => {:f => f}%> <% end %> diff --git a/app/views/issues/_sidebar.rhtml b/app/views/issues/_sidebar.rhtml index bcf0837f8..db85f97b9 100644 --- a/app/views/issues/_sidebar.rhtml +++ b/app/views/issues/_sidebar.rhtml @@ -6,7 +6,7 @@ <%= call_hook(:view_issues_sidebar_issues_bottom) %> <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %> - <%= link_to(l(:label_calendar), :controller => 'issues', :action => 'calendar', :project_id => @project) %>
    + <%= link_to(l(:label_calendar), :controller => 'calendars', :action => 'show', :project_id => @project) %>
    <% end %> <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %> <%= link_to(l(:label_gantt), :controller => 'gantts', :action => 'show', :project_id => @project) %>
    diff --git a/app/views/issues/bulk_edit.rhtml b/app/views/issues/bulk_edit.rhtml index b01128840..7091e358b 100644 --- a/app/views/issues/bulk_edit.rhtml +++ b/app/views/issues/bulk_edit.rhtml @@ -2,7 +2,7 @@
      <%= @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 %> +<% form_tag(:action => 'bulk_update') do %> <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
    @@ -11,7 +11,7 @@

    - <%= select_tag('issue[tracker_id]', "" + options_from_collection_for_select(@project.trackers, :id, :name)) %> + <%= select_tag('issue[tracker_id]', "" + options_from_collection_for_select(@trackers, :id, :name)) %>

    <% if @available_statuses.any? %>

    @@ -27,20 +27,25 @@ <%= select_tag('issue[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') + content_tag('option', l(:label_nobody), :value => 'none') + - options_from_collection_for_select(@project.assignable_users, :id, :name)) %> + options_from_collection_for_select(@assignables, :id, :name)) %>

    +<% if @project %>

    <%= select_tag('issue[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)) %>

    +<% end %> +<% #TODO: allow editing versions when multiple projects %> +<% if @project %>

    <%= select_tag('issue[fixed_version_id]', content_tag('option', l(:label_no_change_option), :value => '') + content_tag('option', l(:label_none), :value => 'none') + version_options_for_select(@project.shared_versions.open)) %>

    +<% end %> <% @custom_fields.each do |custom_field| %>

    <%= custom_field_tag_for_bulk_edit('issue', custom_field) %>

    diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml index 1778f4d64..ddd5d9080 100644 --- a/app/views/issues/index.rhtml +++ b/app/views/issues/index.rhtml @@ -39,6 +39,7 @@ { :url => { :set_filter => 1 }, :before => 'selectAllOptions("selected_columns");', :update => "content", + :complete => "apply_filters_observer()", :with => "Form.serialize('query_form')" }, :class => 'icon icon-checked' %> @@ -78,7 +79,7 @@ <% 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)) %> + <%= auto_discovery_link_tag(:atom, {:controller => 'journals', :action => 'index', :query_id => @query, :format => 'atom', :page => nil, :key => User.current.rss_key}, :title => l(:label_changes_details)) %> <% end %> -<%= context_menu :controller => 'issues', :action => 'context_menu' %> +<%= context_menu issues_context_menu_path %> diff --git a/app/views/issues/new.rhtml b/app/views/issues/new.rhtml index 839286bdb..310085d7c 100644 --- a/app/views/issues/new.rhtml +++ b/app/views/issues/new.rhtml @@ -9,7 +9,7 @@ <%= submit_tag l(:button_create) %> <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> <%= link_to_remote l(:label_preview), - { :url => { :controller => 'issues', :action => 'preview', :project_id => @project }, + { :url => preview_issue_path(:project_id => @project), :method => 'post', :update => 'preview', :with => "Form.serialize('issue-form')", diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml index b48ff2cd1..d1d93da9a 100644 --- a/app/views/issues/show.rhtml +++ b/app/views/issues/show.rhtml @@ -32,7 +32,7 @@
    <%=l(:field_category)%>:<%=h @issue.category ? @issue.category.name : "-" %><%=l(:label_spent_time)%>:<%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}) : "-" %><%= @issue.spent_hours > 0 ? (link_to l_hours(@issue.spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-" %>
    @@ -32,7 +32,7 @@
    -<%= link_to(l(:button_reset), {:controller => 'projects', :action => 'reset_activities', :id => @project}, +<%= link_to(l(:button_reset), project_project_enumerations_path(@project), :method => :delete, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %> diff --git a/app/views/projects/settings/_versions.rhtml b/app/views/projects/settings/_versions.rhtml index dc81f6265..d41929c2d 100644 --- a/app/views/projects/settings/_versions.rhtml +++ b/app/views/projects/settings/_versions.rhtml @@ -21,7 +21,7 @@

    <% if version.project == @project %> <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %> - <%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> + <%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %> <% end %>
    <%=h membership.project %> + <%= link_to_project membership.project %> + <%=h membership.roles.sort.collect(&:to_s).join(', ') %> <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership }, diff --git a/app/views/users/_preferences.html.erb b/app/views/users/_preferences.html.erb new file mode 100644 index 000000000..85b5990e3 --- /dev/null +++ b/app/views/users/_preferences.html.erb @@ -0,0 +1,6 @@ +<% fields_for :pref, @user.pref, :builder => TabularFormBuilder, :lang => current_language do |pref_fields| %> +

    <%= pref_fields.check_box :hide_mail %>

    +

    <%= pref_fields.select :time_zone, ActiveSupport::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 %> + diff --git a/app/views/users/edit.rhtml b/app/views/users/edit.rhtml index f5538c167..0d9cb0133 100644 --- a/app/views/users/edit.rhtml +++ b/app/views/users/edit.rhtml @@ -1,5 +1,5 @@
    -<%= link_to l(:label_profile), {:controller => 'users', :action => 'show', :id => @user}, :class => 'icon icon-user' %> +<%= link_to l(:label_profile), user_path(@user), :class => 'icon icon-user' %> <%= change_status_link(@user) %>
    diff --git a/app/views/users/index.rhtml b/app/views/users/index.rhtml index 1b4702872..69ad73747 100644 --- a/app/views/users/index.rhtml +++ b/app/views/users/index.rhtml @@ -1,5 +1,5 @@
    -<%= link_to l(:label_user_new), {:action => 'add'}, :class => 'icon icon-add' %> +<%= link_to l(:label_user_new), {:action => 'new'}, :class => 'icon icon-add' %>

    <%=l(:label_user_plural)%>

    @@ -30,7 +30,7 @@
    <%= avatar(user, :size => "14") %><%= link_to h(user.login), :action => 'edit', :id => user %><%= avatar(user, :size => "14") %><%= link_to h(user.login), edit_user_path(user) %> <%= h(user.firstname) %> <%= h(user.lastname) %>
    containing the day names */ +div.calendar thead .name { /* Cells containing the day names */ border-bottom: 1px solid #556; padding: 2px; text-align: center; color: #000; } -.calendar thead .weekend { /* How a weekend day name shows in header */ +div.calendar thead .weekend { /* How a weekend day name shows in header */ color: #a66; } -.calendar thead .hilite { /* How do the buttons in header appear when hover */ +div.calendar thead .hilite { /* How do the buttons in header appear when hover */ background-color: #80b0da; color: #000; padding: 1px; } -.calendar thead .active { /* Active (pressed) buttons in header */ +div.calendar thead .active { /* Active (pressed) buttons in header */ background-color: #77c; padding: 2px 0px 0px 2px; } /* The body part -- contains all the days in month. */ -.calendar tbody .day { /* Cells containing month days dates */ +div.calendar tbody .day { /* Cells containing month days dates */ width: 2em; color: #456; text-align: right; padding: 2px 4px 2px 2px; } -.calendar tbody .day.othermonth { +div.calendar tbody .day.othermonth { font-size: 80%; color: #bbb; } -.calendar tbody .day.othermonth.oweekend { +div.calendar tbody .day.othermonth.oweekend { color: #fbb; } -.calendar table .wn { +div.calendar table .wn { padding: 2px 3px 2px 2px; border-right: 1px solid #000; background: #bdf; } -.calendar tbody .rowhilite td { +div.calendar tbody .rowhilite td { background: #def; } -.calendar tbody .rowhilite td.wn { +div.calendar tbody .rowhilite td.wn { background: #80b0da; } -.calendar tbody td.hilite { /* Hovered cells */ +div.calendar tbody td.hilite { /* Hovered cells */ background: #80b0da; padding: 1px 3px 1px 1px; border: 1px solid #bbb; } -.calendar tbody td.active { /* Active (pressed) cells */ +div.calendar tbody td.active { /* Active (pressed) cells */ background: #cde; padding: 2px 2px 0px 2px; } -.calendar tbody td.selected { /* Cell showing today date */ +div.calendar tbody td.selected { /* Cell showing today date */ font-weight: bold; border: 1px solid #000; padding: 1px 3px 1px 1px; @@ -116,55 +116,55 @@ div.calendar { position: relative; z-index: 30;} color: #000; } -.calendar tbody td.weekend { /* Cells showing weekend days */ +div.calendar tbody td.weekend { /* Cells showing weekend days */ color: #a66; } -.calendar tbody td.today { /* Cell showing selected date */ +div.calendar tbody td.today { /* Cell showing selected date */ font-weight: bold; color: #f00; } -.calendar tbody .disabled { color: #999; } +div.calendar tbody .disabled { color: #999; } -.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */ +div.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */ visibility: hidden; } -.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */ +div.calendar tbody .emptyrow { /* Empty row (some months need less than 6 rows) */ display: none; } /* The footer part -- status bar and "Close" button */ -.calendar tfoot .footrow { /* The
    */ +div.calendar tfoot .ttip { /* Tooltip (status bar) cell */ background: #fff; color: #445; border-top: 1px solid #556; padding: 1px; } -.calendar tfoot .hilite { /* Hover style for buttons in footer */ +div.calendar tfoot .hilite { /* Hover style for buttons in footer */ background: #aaf; border: 1px solid #04f; color: #000; padding: 1px; } -.calendar tfoot .active { /* Active (pressed) style for buttons in footer */ +div.calendar tfoot .active { /* Active (pressed) style for buttons in footer */ background: #77c; padding: 2px 0px 0px 2px; } /* Combo boxes (menus that display months/years for direct selection) */ -.calendar .combo { +div.calendar .combo { position: absolute; display: none; top: 0px; @@ -178,59 +178,59 @@ div.calendar { position: relative; z-index: 30;} z-index: 100; } -.calendar .combo .label, -.calendar .combo .label-IEfix { +div.calendar .combo .label, +div.calendar .combo .label-IEfix { text-align: center; padding: 1px; } -.calendar .combo .label-IEfix { +div.calendar .combo .label-IEfix { width: 4em; } -.calendar .combo .hilite { +div.calendar .combo .hilite { background: #acf; } -.calendar .combo .active { +div.calendar .combo .active { border-top: 1px solid #46a; border-bottom: 1px solid #46a; background: #eef; font-weight: bold; } -.calendar td.time { +div.calendar td.time { border-top: 1px solid #000; padding: 1px 0px; text-align: center; background-color: #f4f0e8; } -.calendar td.time .hour, -.calendar td.time .minute, -.calendar td.time .ampm { +div.calendar td.time .hour, +div.calendar td.time .minute, +div.calendar td.time .ampm { padding: 0px 3px 0px 4px; border: 1px solid #889; font-weight: bold; background-color: #fff; } -.calendar td.time .ampm { +div.calendar td.time .ampm { text-align: center; } -.calendar td.time .colon { +div.calendar td.time .colon { padding: 0px 2px 0px 3px; font-weight: bold; } -.calendar td.time span.hilite { +div.calendar td.time span.hilite { border-color: #000; background-color: #667; color: #fff; } -.calendar td.time span.active { +div.calendar td.time span.active { border-color: #f00; background-color: #000; color: #0f0; diff --git a/public/stylesheets/context_menu_rtl.css b/public/stylesheets/context_menu_rtl.css new file mode 100644 index 000000000..205bb9184 --- /dev/null +++ b/public/stylesheets/context_menu_rtl.css @@ -0,0 +1,9 @@ +#context-menu li.folder ul { left:auto; right:168px; } +#context-menu li.folder>ul { left:auto; right:148px; } +#context-menu li a.submenu { background:url("../images/bullet_arrow_left.png") left no-repeat; } + +#context-menu a { + background-position: 100% 40%; + padding-right: 20px; + padding-left: 0px; +} diff --git a/public/stylesheets/rtl.css b/public/stylesheets/rtl.css new file mode 100644 index 000000000..5dfe320bc --- /dev/null +++ b/public/stylesheets/rtl.css @@ -0,0 +1,62 @@ +body, #wrapper { direction: rtl;} + +#quick-search { float: left; } +#main-menu { margin-left: -500px; left: auto; right: 6px; margin-right: 0px;} +#main-menu li { float: right; } +#top-menu ul { float: right; } +#account { float: left; } +#top-menu #loggedas { float: left; } +#top-menu li { float: right; } +.tabular label.floating +{ + margin-right: 0; + margin-left: auto; + text-align: right; +} +.tabular label +{ + float: right; + margin-left: auto; +} +.tabular p +{ + clear: right; +} +.tabular label.block { text-align: right; } +.icon +{ + background-position: 100% 40%; + padding-right: 20px; + padding-left: 0px; +} +div#activity dt, #search-results dt +{ + background-position: 100% 50%; + padding-right: 20px; + padding-left: 0px; +} +#content .tabs ul li { float: right; } +#content .tabs ul { padding-left: auto; padding-right: 1em; } +table.progress { float: right; } +.contextual { float: left; } +.icon22 { background-position: 100% 40%; padding-right: 26px; padding-left: auto; } +h3, .wiki h2 { padding: 10px 2px 1px 0; } +.tooltip span.tip { text-align: right; } +tr.issue td.subject { text-align: right; } +tr.time-entry td.subject, tr.time-entry td.comments { text-align: right; } +#sidebar { float: left; } +#main.nosidebar #content { border-width: 1px; border-style: solid; border-color: #D7D7D7 #BBBBBB #BBBBBB #D7D7D7;} +.tabular.settings label { margin-left: auto; } +.splitcontentleft { float: right; } +.splitcontentright { float: left; } +p.progress-info { clear: right; } +table.list td.buttons a { padding-right: 20px; } +.filecontent { direction: ltr; } +.entries { direction: ltr; } +.changeset-changes { direction: ltr; padding-left: 2em } +.changesets { direction: ltr; } +div#issue-changesets { float: left; margin-right: 1em; margin-left: 0 } +#activity dt, .journal { clear: right; } +.journal-link { float: left; } +div.wiki pre { direction: ltr; } + diff --git a/test/exemplars/attachment_exemplar.rb b/test/exemplars/attachment_exemplar.rb index 8100fe906..4baaf530f 100644 --- a/test/exemplars/attachment_exemplar.rb +++ b/test/exemplars/attachment_exemplar.rb @@ -12,6 +12,6 @@ class Attachment < ActiveRecord::Base end def self.generate_file - @file = mock_file + @file = ActiveSupport::TestCase.mock_file end end diff --git a/test/exemplars/user_exemplar.rb b/test/exemplars/user_exemplar.rb index 7c4af70b8..def8dc423 100644 --- a/test/exemplars/user_exemplar.rb +++ b/test/exemplars/user_exemplar.rb @@ -3,7 +3,7 @@ class User < Principal generator_for :mail, :method => :next_email generator_for :firstname, :method => :next_firstname generator_for :lastname, :method => :next_lastname - + def self.next_login @gen_login ||= 'user1' @gen_login.succ! diff --git a/test/fixtures/enabled_modules.yml b/test/fixtures/enabled_modules.yml index 0a83168df..5f2ba63d1 100644 --- a/test/fixtures/enabled_modules.yml +++ b/test/fixtures/enabled_modules.yml @@ -63,3 +63,35 @@ enabled_modules_016: name: boards project_id: 2 id: 16 +enabled_modules_017: + name: calendar + project_id: 1 + id: 17 +enabled_modules_018: + name: gantt + project_id: 1 + id: 18 +enabled_modules_019: + name: calendar + project_id: 2 + id: 19 +enabled_modules_020: + name: gantt + project_id: 2 + id: 20 +enabled_modules_021: + name: calendar + project_id: 3 + id: 21 +enabled_modules_022: + name: gantt + project_id: 3 + id: 22 +enabled_modules_023: + name: calendar + project_id: 5 + id: 23 +enabled_modules_024: + name: gantt + project_id: 5 + id: 24 diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml index 3adda3121..eec014ff4 100644 --- a/test/fixtures/issues.yml +++ b/test/fixtures/issues.yml @@ -38,6 +38,7 @@ issues_002: lft: 1 rgt: 2 lock_version: 3 + done_ratio: 30 issues_003: created_on: 2006-07-19 21:07:27 +02:00 project_id: 1 diff --git a/test/fixtures/repositories/git_repository.tar.gz b/test/fixtures/repositories/git_repository.tar.gz index 8158d5832..17cb22943 100644 Binary files a/test/fixtures/repositories/git_repository.tar.gz and b/test/fixtures/repositories/git_repository.tar.gz differ diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 29fc6be04..f26c09c0b 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -12,7 +12,7 @@ users_004: firstname: Robert id: 4 auth_source_id: - mail_notification: true + mail_notification: all login: rhill type: User users_001: @@ -28,7 +28,7 @@ users_001: firstname: redMine id: 1 auth_source_id: - mail_notification: true + mail_notification: all login: admin type: User users_002: @@ -44,7 +44,7 @@ users_002: firstname: John id: 2 auth_source_id: - mail_notification: true + mail_notification: all login: jsmith type: User users_003: @@ -60,7 +60,7 @@ users_003: firstname: Dave id: 3 auth_source_id: - mail_notification: true + mail_notification: all login: dlopper type: User users_005: @@ -77,7 +77,7 @@ users_005: lastname: Lopper2 firstname: Dave2 auth_source_id: - mail_notification: true + mail_notification: all login: dlopper2 type: User users_006: @@ -93,7 +93,7 @@ users_006: lastname: Anonymous firstname: '' auth_source_id: - mail_notification: false + mail_notification: only_my_events login: '' type: AnonymousUser users_007: @@ -109,7 +109,7 @@ users_007: lastname: One firstname: Some auth_source_id: - mail_notification: false + mail_notification: only_my_events login: someone type: User users_008: @@ -125,7 +125,7 @@ users_008: lastname: Misc firstname: User auth_source_id: - mail_notification: false + mail_notification: only_my_events login: miscuser8 type: User users_009: @@ -141,7 +141,7 @@ users_009: lastname: Misc firstname: User auth_source_id: - mail_notification: false + mail_notification: only_my_events login: miscuser9 type: User groups_010: @@ -153,4 +153,4 @@ groups_011: lastname: B Team type: Group - \ No newline at end of file + diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb index 17d19dfe1..1d66d01d3 100644 --- a/test/functional/account_controller_test.rb +++ b/test/functional/account_controller_test.rb @@ -67,6 +67,13 @@ class AccountControllerTest < ActionController::TestCase assert_redirected_to 'my/page' end + def test_login_with_invalid_openid_provider + Setting.self_registration = '0' + Setting.openid = '1' + post :login, :openid_url => 'http;//openid.example.com/good_user' + assert_redirected_to home_url + end + def test_login_with_openid_for_existing_non_active_user Setting.self_registration = '2' Setting.openid = '1' @@ -153,4 +160,65 @@ class AccountControllerTest < ActionController::TestCase assert_redirected_to '' assert_nil @request.session[:user_id] end + + context "GET #register" do + context "with self registration on" do + setup do + Setting.self_registration = '3' + get :register + end + + should_respond_with :success + should_render_template :register + should_assign_to :user + end + + context "with self registration off" do + setup do + Setting.self_registration = '0' + get :register + end + + should_redirect_to('/') { home_url } + end + end + + # See integration/account_test.rb for the full test + context "POST #register" do + context "with self registration on automatic" do + setup do + Setting.self_registration = '3' + post :register, :user => { + :login => 'register', + :password => 'test', + :password_confirmation => 'test', + :firstname => 'John', + :lastname => 'Doe', + :mail => 'register@example.com' + } + end + + should_respond_with :redirect + should_assign_to :user + should_redirect_to('my page') { {:controller => 'my', :action => 'account'} } + + should_create_a_new_user { User.last(:conditions => {:login => 'register'}) } + + should 'set the user status to active' do + user = User.last(:conditions => {:login => 'register'}) + assert user + assert_equal User::STATUS_ACTIVE, user.status + end + end + + context "with self registration off" do + setup do + Setting.self_registration = '0' + post :register + end + + should_redirect_to('/') { home_url } + end + end + end diff --git a/test/functional/activities_controller_test.rb b/test/functional/activities_controller_test.rb new file mode 100644 index 000000000..ba9c33985 --- /dev/null +++ b/test/functional/activities_controller_test.rb @@ -0,0 +1,87 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ActivitiesControllerTest < ActionController::TestCase + fixtures :all + + def test_project_index + get :index, :id => 1, :with_subprojects => 0 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{2.days.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue-edit/ }, + :child => { :tag => "a", + :content => /(#{IssueStatus.find(2).name})/, + } + } + } + end + + def test_previous_project_index + get :index, :id => 1, :from => 3.days.ago.to_date + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{3.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(1).subject}/, + } + } + } + end + + def test_global_index + get :index + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(5).subject}/, + } + } + } + end + + def test_user_index + get :index, :user_id => 2 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:events_by_day) + + assert_tag :tag => "h3", + :content => /#{3.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(1).subject}/, + } + } + } + end + + def test_index_atom_feed + get :index, :format => 'atom' + assert_response :success + assert_template 'common/feed.atom.rxml' + assert_tag :tag => 'entry', :child => { + :tag => 'link', + :attributes => {:href => 'http://test.host/issues/11'}} + end + +end diff --git a/test/functional/auth_sources_controller_test.rb b/test/functional/auth_sources_controller_test.rb index bd97844ed..348e0e098 100644 --- a/test/functional/auth_sources_controller_test.rb +++ b/test/functional/auth_sources_controller_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require File.dirname(__FILE__) + '/../test_helper' class AuthSourcesControllerTest < ActionController::TestCase fixtures :all diff --git a/test/functional/auto_completes_controller_test.rb b/test/functional/auto_completes_controller_test.rb new file mode 100644 index 000000000..25e75fc4b --- /dev/null +++ b/test/functional/auto_completes_controller_test.rb @@ -0,0 +1,20 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class AutoCompletesControllerTest < ActionController::TestCase + fixtures :all + + def test_issues_should_not_be_case_sensitive + get :issues, :project_id => 'ecookbook', :q => 'ReCiPe' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} + end + + def test_issues_should_return_issue_with_given_id + get :issues, :project_id => 'subproject1', :q => '13' + assert_response :success + assert_not_nil assigns(:issues) + assert assigns(:issues).include?(Issue.find(13)) + end + +end diff --git a/test/functional/calendars_controller_test.rb b/test/functional/calendars_controller_test.rb index 79cfe28a0..ad047c669 100644 --- a/test/functional/calendars_controller_test.rb +++ b/test/functional/calendars_controller_test.rb @@ -1,4 +1,4 @@ -require 'test_helper' +require File.dirname(__FILE__) + '/../test_helper' class CalendarsControllerTest < ActionController::TestCase fixtures :all diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb new file mode 100644 index 000000000..1887c4896 --- /dev/null +++ b/test/functional/comments_controller_test.rb @@ -0,0 +1,57 @@ +# 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 File.dirname(__FILE__) + '/../test_helper' + +class CommentsControllerTest < ActionController::TestCase + fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments + + def setup + User.current = nil + end + + def test_add_comment + @request.session[:user_id] = 2 + post :create, :id => 1, :comment => { :comments => 'This is a test comment' } + assert_redirected_to 'news/1' + + comment = News.find(1).comments.find(:first, :order => 'created_on DESC') + assert_not_nil comment + assert_equal 'This is a test comment', comment.comments + assert_equal User.find(2), comment.author + end + + def test_empty_comment_should_not_be_added + @request.session[:user_id] = 2 + assert_no_difference 'Comment.count' do + post :create, :id => 1, :comment => { :comments => '' } + assert_response :redirect + assert_redirected_to 'news/1' + end + end + + def test_destroy_comment + comments_count = News.find(1).comments.size + @request.session[:user_id] = 2 + delete :destroy, :id => 1, :comment_id => 2 + assert_redirected_to 'news/1' + assert_nil Comment.find_by_id(2) + assert_equal comments_count - 1, News.find(1).comments.size + end + + +end diff --git a/test/functional/context_menus_controller_test.rb b/test/functional/context_menus_controller_test.rb new file mode 100644 index 000000000..6d2b45bad --- /dev/null +++ b/test/functional/context_menus_controller_test.rb @@ -0,0 +1,105 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ContextMenusControllerTest < ActionController::TestCase + fixtures :all + + def test_context_menu_one_issue + @request.session[:user_id] = 2 + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/1/edit', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bstatus_id%5D=5', + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bpriority_id%5D=8', + :class => '' } + # Versions + assert_tag :tag => 'a', :content => '2.0', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', + :class => '' } + + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'Duplicate', + :attributes => { :href => '/projects/ecookbook/issues/1/copy', + :class => 'icon-duplicate' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => '/issues/move/new?copy_options%5Bcopy%5D=t&ids%5B%5D=1', + :class => 'icon-copy' } + assert_tag :tag => 'a', :content => 'Move', + :attributes => { :href => '/issues/move/new?ids%5B%5D=1', + :class => 'icon-move' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues/destroy?ids%5B%5D=1', + :class => 'icon-del' } + end + + def test_context_menu_one_issue_by_anonymous + get :issues, :ids => [1] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '#', + :class => 'icon-del disabled' } + end + + def test_context_menu_multiple_issues_of_same_project + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2] + assert_response :success + assert_template 'context_menu' + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&issue%5Bstatus_id%5D=5', + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&issue%5Bpriority_id%5D=8', + :class => '' } + assert_tag :tag => 'a', :content => 'Dave Lopper', + :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&issue%5Bassigned_to_id%5D=3', + :class => '' } + assert_tag :tag => 'a', :content => 'Copy', + :attributes => { :href => '/issues/move/new?copy_options%5Bcopy%5D=t&ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-copy' } + assert_tag :tag => 'a', :content => 'Move', + :attributes => { :href => '/issues/move/new?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-move' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => '/issues/destroy?ids%5B%5D=1&ids%5B%5D=2', + :class => 'icon-del' } + end + + def test_context_menu_multiple_issues_of_different_projects + @request.session[:user_id] = 2 + get :issues, :ids => [1, 2, 6] + assert_response :success + assert_template 'context_menu' + ids = "ids%5B%5D=1&ids%5B%5D=2&ids%5B%5D=6" + assert_tag :tag => 'a', :content => 'Edit', + :attributes => { :href => "/issues/bulk_edit?#{ids}", + :class => 'icon-edit' } + assert_tag :tag => 'a', :content => 'Closed', + :attributes => { :href => "/issues/bulk_edit?#{ids}&issue%5Bstatus_id%5D=5", + :class => '' } + assert_tag :tag => 'a', :content => 'Immediate', + :attributes => { :href => "/issues/bulk_edit?#{ids}&issue%5Bpriority_id%5D=8", + :class => '' } + assert_tag :tag => 'a', :content => 'John Smith', + :attributes => { :href => "/issues/bulk_edit?#{ids}&issue%5Bassigned_to_id%5D=2", + :class => '' } + assert_tag :tag => 'a', :content => 'Delete', + :attributes => { :href => "/issues/destroy?#{ids}", + :class => 'icon-del' } + end + +end diff --git a/test/functional/files_controller_test.rb b/test/functional/files_controller_test.rb new file mode 100644 index 000000000..6035c4be5 --- /dev/null +++ b/test/functional/files_controller_test.rb @@ -0,0 +1,67 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class FilesControllerTest < ActionController::TestCase + fixtures :all + + def setup + @controller = FilesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:containers) + + # file attached to the project + assert_tag :a, :content => 'project_file.zip', + :attributes => { :href => '/attachments/download/8/project_file.zip' } + + # file attached to a project's version + assert_tag :a, :content => 'version_file.zip', + :attributes => { :href => '/attachments/download/9/version_file.zip' } + end + + def test_create_file + set_tmp_attachments_directory + @request.session[:user_id] = 2 + Setting.notified_events = ['file_added'] + ActionMailer::Base.deliveries.clear + + assert_difference 'Attachment.count' do + post :create, :project_id => 1, :version_id => '', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + assert_response :redirect + end + assert_redirected_to 'projects/ecookbook/files' + a = Attachment.find(:first, :order => 'created_on DESC') + assert_equal 'testfile.txt', a.filename + assert_equal Project.find(1), a.container + + mail = ActionMailer::Base.deliveries.last + assert_kind_of TMail::Mail, mail + assert_equal "[eCookbook] New file", mail.subject + assert mail.body.include?('testfile.txt') + end + + def test_create_version_file + set_tmp_attachments_directory + @request.session[:user_id] = 2 + Setting.notified_events = ['file_added'] + + assert_difference 'Attachment.count' do + post :create, :project_id => 1, :version_id => '2', + :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} + assert_response :redirect + end + assert_redirected_to 'projects/ecookbook/files' + a = Attachment.find(:first, :order => 'created_on DESC') + assert_equal 'testfile.txt', a.filename + assert_equal Version.find(2), a.container + end + +end diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb index 4c27de7cd..9aba1d5aa 100644 --- a/test/functional/gantts_controller_test.rb +++ b/test/functional/gantts_controller_test.rb @@ -1,24 +1,24 @@ -require 'test_helper' +require File.dirname(__FILE__) + '/../test_helper' class GanttsControllerTest < ActionController::TestCase fixtures :all context "#gantt" do should "work" do + i2 = Issue.find(2) + i2.update_attribute(:due_date, 1.month.from_now) + get :show, :project_id => 1 assert_response :success assert_template 'show.html.erb' assert_not_nil assigns(:gantt) - events = assigns(:gantt).events - assert_not_nil events # Issue with start and due dates i = Issue.find(1) assert_not_nil i.due_date - assert events.include?(Issue.find(1)) - # Issue with without due date but targeted to a version with date + assert_select "div a.issue", /##{i.id}/ + # Issue with on a targeted version should not be in the events but loaded in the html i = Issue.find(2) - assert_nil i.due_date - assert events.include?(i) + assert_select "div a.issue", /##{i.id}/ end should "work cross project" do @@ -26,8 +26,8 @@ class GanttsControllerTest < ActionController::TestCase assert_response :success assert_template 'show.html.erb' assert_not_nil assigns(:gantt) - events = assigns(:gantt).events - assert_not_nil events + assert_not_nil assigns(:gantt).query + assert_nil assigns(:gantt).project end should "export to pdf" do diff --git a/test/functional/issue_moves_controller_test.rb b/test/functional/issue_moves_controller_test.rb new file mode 100644 index 000000000..7c4005767 --- /dev/null +++ b/test/functional/issue_moves_controller_test.rb @@ -0,0 +1,99 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class IssueMovesControllerTest < ActionController::TestCase + fixtures :all + + def setup + User.current = nil + end + + def test_create_one_issue_to_another_project + @request.session[:user_id] = 2 + post :create, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => '' + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 2, Issue.find(1).project_id + end + + def test_create_one_issue_to_another_project_should_follow_when_needed + @request.session[:user_id] = 2 + post :create, :id => 1, :new_project_id => 2, :follow => '1' + assert_redirected_to '/issues/1' + end + + def test_bulk_create_to_another_project + @request.session[:user_id] = 2 + post :create, :ids => [1, 2], :new_project_id => 2 + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + # Issues moved to project 2 + assert_equal 2, Issue.find(1).project_id + assert_equal 2, Issue.find(2).project_id + # No tracker change + assert_equal 1, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_create_to_another_tracker + @request.session[:user_id] = 2 + post :create, :ids => [1, 2], :new_tracker_id => 2 + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_equal 2, Issue.find(1).tracker_id + assert_equal 2, Issue.find(2).tracker_id + end + + def test_bulk_copy_to_another_project + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 2 do + assert_no_difference 'Project.find(1).issues.count' do + post :create, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'} + end + end + assert_redirected_to 'projects/ecookbook/issues' + end + + context "#create via bulk copy" do + should "allow not changing the issue's attributes" do + @request.session[:user_id] = 2 + issue_before_move = Issue.find(1) + assert_difference 'Issue.count', 1 do + assert_no_difference 'Project.find(1).issues.count' do + post :create, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => '' + end + end + issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2}) + assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id + assert_equal issue_before_move.status_id, issue_after_move.status_id + assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id + end + + should "allow changing the issue's attributes" do + # Fixes random test failure with Mysql + # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) doesn't return the expected results + Issue.delete_all("project_id=2") + + @request.session[:user_id] = 2 + assert_difference 'Issue.count', 2 do + assert_no_difference 'Project.find(1).issues.count' do + post :create, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31' + end + end + + copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) + assert_equal 2, copied_issues.size + copied_issues.each do |issue| + assert_equal 2, issue.project_id, "Project is incorrect" + assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect" + assert_equal 3, issue.status_id, "Status is incorrect" + assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect" + assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect" + end + end + end + + def test_copy_to_another_project_should_follow_when_needed + @request.session[:user_id] = 2 + post :create, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1' + issue = Issue.first(:order => 'id DESC') + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue + end + +end diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb index 670fb2d7e..59b16c32c 100644 --- a/test/functional/issues_controller_test.rb +++ b/test/functional/issues_controller_test.rb @@ -231,13 +231,6 @@ class IssuesControllerTest < ActionController::TestCase assert_equal columns, session[:query][:column_names].map(&:to_s) end - def test_changes - get :changes, :project_id => 1 - assert_response :success - assert_not_nil assigns(:journals) - assert_equal 'application/atom+xml', @response.content_type - end - def test_show_by_anonymous get :show, :id => 1 assert_response :success @@ -307,7 +300,7 @@ class IssuesControllerTest < ActionController::TestCase def test_show_atom get :show, :id => 2, :format => 'atom' assert_response :success - assert_template 'changes.rxml' + assert_template 'journals/index.rxml' # Inline image assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10')) end @@ -365,7 +358,7 @@ class IssuesControllerTest < ActionController::TestCase def test_update_new_form @request.session[:user_id] = 2 - xhr :post, :update_form, :project_id => 1, + xhr :post, :new, :project_id => 1, :issue => {:tracker_id => 2, :subject => 'This is the test_new issue', :description => 'This is the description', @@ -412,7 +405,8 @@ class IssuesControllerTest < ActionController::TestCase :subject => 'This is first issue', :priority_id => 5}, :continue => '' - assert_redirected_to :controller => 'issues', :action => 'new', :issue => {:tracker_id => 3} + assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', + :issue => {:tracker_id => 3} end def test_post_create_without_custom_fields_param @@ -617,7 +611,7 @@ class IssuesControllerTest < ActionController::TestCase def test_update_edit_form @request.session[:user_id] = 2 - xhr :post, :update_form, :project_id => 1, + xhr :post, :new, :project_id => 1, :id => 1, :issue => {:tracker_id => 2, :subject => 'This is the test_new issue', @@ -634,20 +628,6 @@ class IssuesControllerTest < ActionController::TestCase assert_equal 'This is the test_new issue', issue.subject end - def test_reply_to_issue - @request.session[:user_id] = 2 - get :reply, :id => 1 - assert_response :success - assert_select_rjs :show, "update" - end - - def test_reply_to_note - @request.session[:user_id] = 2 - get :reply, :id => 1, :journal_id => 2 - assert_response :success - assert_select_rjs :show, "update" - end - def test_update_using_invalid_http_verbs @request.session[:user_id] = 2 subject = 'Updated by an invalid http verb' @@ -931,10 +911,23 @@ class IssuesControllerTest < ActionController::TestCase assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'} end - def test_bulk_edit + def test_get_bulk_edit_on_different_projects + @request.session[:user_id] = 2 + get :bulk_edit, :ids => [1, 2, 6] + assert_response :success + assert_template 'bulk_edit' + + # Project specific custom field, date type + field = CustomField.find(9) + assert !field.is_for_all? + assert !field.project_ids.include?(Issue.find(6).project_id) + assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'} + end + + def test_bulk_update @request.session[:user_id] = 2 # update issues priority - post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing', + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing', :issue => {:priority_id => 7, :assigned_to_id => '', :custom_field_values => {'2' => ''}} @@ -950,10 +943,43 @@ class IssuesControllerTest < ActionController::TestCase assert_equal 1, journal.details.size end - def test_bullk_edit_should_send_a_notification + def test_bulk_update_on_different_projects + @request.session[:user_id] = 2 + # update issues priority + post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + + assert_response 302 + # check that the issues were updated + assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id) + + issue = Issue.find(1) + journal = issue.journals.find(:first, :order => 'created_on DESC') + assert_equal '125', issue.custom_value_for(2).value + assert_equal 'Bulk editing', journal.notes + assert_equal 1, journal.details.size + end + + def test_bulk_update_on_different_projects_without_rights + @request.session[:user_id] = 3 + user = User.find(3) + action = { :controller => "issues", :action => "bulk_update" } + assert user.allowed_to?(action, Issue.find(1).project) + assert ! user.allowed_to?(action, Issue.find(6).project) + post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail', + :issue => {:priority_id => 7, + :assigned_to_id => '', + :custom_field_values => {'2' => ''}} + assert_response 403 + assert_not_equal "Bulk should fail", Journal.last.notes + end + + def test_bullk_update_should_send_a_notification @request.session[:user_id] = 2 ActionMailer::Base.deliveries.clear - post(:bulk_edit, + post(:bulk_update, { :ids => [1, 2], :notes => 'Bulk editing', @@ -968,10 +994,10 @@ class IssuesControllerTest < ActionController::TestCase assert_equal 2, ActionMailer::Base.deliveries.size end - def test_bulk_edit_status + def test_bulk_update_status @request.session[:user_id] = 2 # update issues priority - post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing status', + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status', :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '5'} @@ -981,10 +1007,10 @@ class IssuesControllerTest < ActionController::TestCase assert issue.closed? end - def test_bulk_edit_custom_field + def test_bulk_update_custom_field @request.session[:user_id] = 2 # update issues priority - post :bulk_edit, :ids => [1, 2], :notes => 'Bulk editing custom field', + post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field', :issue => {:priority_id => '', :assigned_to_id => '', :custom_field_values => {'2' => '777'}} @@ -999,20 +1025,20 @@ class IssuesControllerTest < ActionController::TestCase assert_equal '777', journal.details.first.value end - def test_bulk_unassign + def test_bulk_update_unassign assert_not_nil Issue.find(2).assigned_to @request.session[:user_id] = 2 # unassign issues - post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'} + post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'} assert_response 302 # check that the issues were updated assert_nil Issue.find(2).assigned_to end - def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject + def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject @request.session[:user_id] = 2 - post :bulk_edit, :ids => [1,2], :issue => {:fixed_version_id => 4} + post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4} assert_response :redirect issues = Issue.find([1,2]) @@ -1022,223 +1048,21 @@ class IssuesControllerTest < ActionController::TestCase end end - def test_post_bulk_edit_should_redirect_back_using_the_back_url_parameter + def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter @request.session[:user_id] = 2 - post :bulk_edit, :ids => [1,2], :back_url => '/issues' + post :bulk_update, :ids => [1,2], :back_url => '/issues' assert_response :redirect assert_redirected_to '/issues' end - def test_post_bulk_edit_should_not_redirect_back_using_the_back_url_parameter_off_the_host + def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host @request.session[:user_id] = 2 - post :bulk_edit, :ids => [1,2], :back_url => 'http://google.com' + post :bulk_update, :ids => [1,2], :back_url => 'http://google.com' assert_response :redirect assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier end - - def test_move_one_issue_to_another_project - @request.session[:user_id] = 2 - post :move, :id => 1, :new_project_id => 2, :tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => '' - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - assert_equal 2, Issue.find(1).project_id - end - - def test_move_one_issue_to_another_project_should_follow_when_needed - @request.session[:user_id] = 2 - post :move, :id => 1, :new_project_id => 2, :follow => '1' - assert_redirected_to '/issues/1' - end - - def test_bulk_move_to_another_project - @request.session[:user_id] = 2 - post :move, :ids => [1, 2], :new_project_id => 2 - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - # Issues moved to project 2 - assert_equal 2, Issue.find(1).project_id - assert_equal 2, Issue.find(2).project_id - # No tracker change - assert_equal 1, Issue.find(1).tracker_id - assert_equal 2, Issue.find(2).tracker_id - end - - def test_bulk_move_to_another_tracker - @request.session[:user_id] = 2 - post :move, :ids => [1, 2], :new_tracker_id => 2 - assert_redirected_to :action => 'index', :project_id => 'ecookbook' - assert_equal 2, Issue.find(1).tracker_id - assert_equal 2, Issue.find(2).tracker_id - end - - def test_bulk_copy_to_another_project - @request.session[:user_id] = 2 - assert_difference 'Issue.count', 2 do - assert_no_difference 'Project.find(1).issues.count' do - post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'} - end - end - assert_redirected_to 'projects/ecookbook/issues' - end - - context "#move via bulk copy" do - should "allow not changing the issue's attributes" do - @request.session[:user_id] = 2 - issue_before_move = Issue.find(1) - assert_difference 'Issue.count', 1 do - assert_no_difference 'Project.find(1).issues.count' do - post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => '', :status_id => '', :start_date => '', :due_date => '' - end - end - issue_after_move = Issue.first(:order => 'id desc', :conditions => {:project_id => 2}) - assert_equal issue_before_move.tracker_id, issue_after_move.tracker_id - assert_equal issue_before_move.status_id, issue_after_move.status_id - assert_equal issue_before_move.assigned_to_id, issue_after_move.assigned_to_id - end - - should "allow changing the issue's attributes" do - # Fixes random test failure with Mysql - # where Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) doesn't return the expected results - Issue.delete_all("project_id=2") - - @request.session[:user_id] = 2 - assert_difference 'Issue.count', 2 do - assert_no_difference 'Project.find(1).issues.count' do - post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :new_tracker_id => '', :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31' - end - end - - copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2}) - assert_equal 2, copied_issues.size - copied_issues.each do |issue| - assert_equal 2, issue.project_id, "Project is incorrect" - assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect" - assert_equal 3, issue.status_id, "Status is incorrect" - assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect" - assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect" - end - end - end - - def test_copy_to_another_project_should_follow_when_needed - @request.session[:user_id] = 2 - post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1' - issue = Issue.first(:order => 'id DESC') - assert_redirected_to :controller => 'issues', :action => 'show', :id => issue - end - - def test_context_menu_one_issue - @request.session[:user_id] = 2 - get :context_menu, :ids => [1] - assert_response :success - assert_template 'context_menu' - assert_tag :tag => 'a', :content => 'Edit', - :attributes => { :href => '/issues/1/edit', - :class => 'icon-edit' } - assert_tag :tag => 'a', :content => 'Closed', - :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5', - :class => '' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bpriority_id%5D=8', - :class => '' } - # Versions - assert_tag :tag => 'a', :content => '2.0', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bfixed_version_id%5D=3', - :class => '' } - assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bfixed_version_id%5D=4', - :class => '' } - - assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&issue%5Bassigned_to_id%5D=3', - :class => '' } - assert_tag :tag => 'a', :content => 'Duplicate', - :attributes => { :href => '/projects/ecookbook/issues/1/copy', - :class => 'icon-duplicate' } - assert_tag :tag => 'a', :content => 'Copy', - :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&ids%5B%5D=1', - :class => 'icon-copy' } - assert_tag :tag => 'a', :content => 'Move', - :attributes => { :href => '/issues/move?ids%5B%5D=1', - :class => 'icon-move' } - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => '/issues/destroy?ids%5B%5D=1', - :class => 'icon-del' } - end - - def test_context_menu_one_issue_by_anonymous - get :context_menu, :ids => [1] - assert_response :success - assert_template 'context_menu' - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => '#', - :class => 'icon-del disabled' } - end - - def test_context_menu_multiple_issues_of_same_project - @request.session[:user_id] = 2 - get :context_menu, :ids => [1, 2] - assert_response :success - assert_template 'context_menu' - assert_tag :tag => 'a', :content => 'Edit', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2', - :class => 'icon-edit' } - assert_tag :tag => 'a', :content => 'Immediate', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&issue%5Bpriority_id%5D=8', - :class => '' } - assert_tag :tag => 'a', :content => 'Dave Lopper', - :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&issue%5Bassigned_to_id%5D=3', - :class => '' } - assert_tag :tag => 'a', :content => 'Copy', - :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&ids%5B%5D=1&ids%5B%5D=2', - :class => 'icon-copy' } - assert_tag :tag => 'a', :content => 'Move', - :attributes => { :href => '/issues/move?ids%5B%5D=1&ids%5B%5D=2', - :class => 'icon-move' } - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => '/issues/destroy?ids%5B%5D=1&ids%5B%5D=2', - :class => 'icon-del' } - end - - def test_context_menu_multiple_issues_of_different_project - @request.session[:user_id] = 2 - get :context_menu, :ids => [1, 2, 4] - assert_response :success - assert_template 'context_menu' - assert_tag :tag => 'a', :content => 'Delete', - :attributes => { :href => '#', - :class => 'icon-del disabled' } - end - - def test_preview_new_issue - @request.session[:user_id] = 2 - post :preview, :project_id => '1', :issue => {:description => 'Foo'} - assert_response :success - assert_template 'preview' - assert_not_nil assigns(:description) - end - - def test_preview_notes - @request.session[:user_id] = 2 - post :preview, :project_id => '1', :id => 1, :issue => {:description => Issue.find(1).description}, :notes => 'Foo' - assert_response :success - assert_template 'preview' - assert_not_nil assigns(:notes) - end - - def test_auto_complete_should_not_be_case_sensitive - get :auto_complete, :project_id => 'ecookbook', :q => 'ReCiPe' - assert_response :success - assert_not_nil assigns(:issues) - assert assigns(:issues).detect {|issue| issue.subject.match /recipe/} - end - - def test_auto_complete_should_return_issue_with_given_id - get :auto_complete, :project_id => 'subproject1', :q => '13' - assert_response :success - assert_not_nil assigns(:issues) - assert assigns(:issues).include?(Issue.find(13)) - end def test_destroy_issue_with_no_time_entries assert_nil TimeEntry.find_by_issue_id(2) @@ -1283,6 +1107,13 @@ class IssuesControllerTest < ActionController::TestCase assert_equal 2, TimeEntry.find(2).issue_id end + def test_destroy_issues_from_different_projects + @request.session[:user_id] = 2 + post :destroy, :ids => [1, 2, 6], :todo => 'destroy' + assert_redirected_to :controller => 'issues', :action => 'index' + assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6)) + end + def test_default_search_scope get :index assert_tag :div, :attributes => {:id => 'quick-search'}, diff --git a/test/functional/journals_controller_test.rb b/test/functional/journals_controller_test.rb index 0a11bab3e..83e015af6 100644 --- a/test/functional/journals_controller_test.rb +++ b/test/functional/journals_controller_test.rb @@ -31,6 +31,27 @@ class JournalsControllerTest < ActionController::TestCase User.current = nil end + def test_index + get :index, :project_id => 1 + assert_response :success + assert_not_nil assigns(:journals) + assert_equal 'application/atom+xml', @response.content_type + end + + def test_reply_to_issue + @request.session[:user_id] = 2 + get :new, :id => 1 + assert_response :success + assert_select_rjs :show, "update" + end + + def test_reply_to_note + @request.session[:user_id] = 2 + get :new, :id => 1, :journal_id => 2 + assert_response :success + assert_select_rjs :show, "update" + end + def test_get_edit @request.session[:user_id] = 1 xhr :get, :edit, :id => 2 diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb index 3abb9a7fb..de5c055a4 100644 --- a/test/functional/news_controller_test.rb +++ b/test/functional/news_controller_test.rb @@ -65,12 +65,12 @@ class NewsControllerTest < ActionController::TestCase assert_template 'new' end - def test_post_new + def test_post_create ActionMailer::Base.deliveries.clear Setting.notified_events << 'news_added' @request.session[:user_id] = 2 - post :new, :project_id => 1, :news => { :title => 'NewsControllerTest', + post :create, :project_id => 1, :news => { :title => 'NewsControllerTest', :description => 'This is the description', :summary => '' } assert_redirected_to 'projects/ecookbook/news' @@ -90,17 +90,17 @@ class NewsControllerTest < ActionController::TestCase assert_template 'edit' end - def test_post_edit + def test_put_update @request.session[:user_id] = 2 - post :edit, :id => 1, :news => { :description => 'Description changed by test_post_edit' } + put :update, :id => 1, :news => { :description => 'Description changed by test_post_edit' } assert_redirected_to 'news/1' news = News.find(1) assert_equal 'Description changed by test_post_edit', news.description end - def test_post_new_with_validation_failure + def test_post_create_with_validation_failure @request.session[:user_id] = 2 - post :new, :project_id => 1, :news => { :title => '', + post :create, :project_id => 1, :news => { :title => '', :description => 'This is the description', :summary => '' } assert_response :success @@ -111,50 +111,10 @@ class NewsControllerTest < ActionController::TestCase :content => /1 error/ end - def test_add_comment - @request.session[:user_id] = 2 - post :add_comment, :id => 1, :comment => { :comments => 'This is a NewsControllerTest comment' } - assert_redirected_to 'news/1' - - comment = News.find(1).comments.find(:first, :order => 'created_on DESC') - assert_not_nil comment - assert_equal 'This is a NewsControllerTest comment', comment.comments - assert_equal User.find(2), comment.author - end - - def test_empty_comment_should_not_be_added - @request.session[:user_id] = 2 - assert_no_difference 'Comment.count' do - post :add_comment, :id => 1, :comment => { :comments => '' } - assert_response :success - assert_template 'show' - end - end - - def test_destroy_comment - comments_count = News.find(1).comments.size - @request.session[:user_id] = 2 - post :destroy_comment, :id => 1, :comment_id => 2 - assert_redirected_to 'news/1' - assert_nil Comment.find_by_id(2) - assert_equal comments_count - 1, News.find(1).comments.size - end - def test_destroy @request.session[:user_id] = 2 - post :destroy, :id => 1 + delete :destroy, :id => 1 assert_redirected_to 'projects/ecookbook/news' assert_nil News.find_by_id(1) end - - def test_preview - get :preview, :project_id => 1, - :news => {:title => '', - :description => 'News description', - :summary => ''} - assert_response :success - assert_template 'common/_preview' - assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' }, - :content => /News description/ - end end diff --git a/test/functional/previews_controller_test.rb b/test/functional/previews_controller_test.rb new file mode 100644 index 000000000..4140f1428 --- /dev/null +++ b/test/functional/previews_controller_test.rb @@ -0,0 +1,32 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PreviewsControllerTest < ActionController::TestCase + fixtures :all + + def test_preview_new_issue + @request.session[:user_id] = 2 + post :issue, :project_id => '1', :issue => {:description => 'Foo'} + assert_response :success + assert_template 'preview' + assert_not_nil assigns(:description) + end + + def test_preview_issue_notes + @request.session[:user_id] = 2 + post :issue, :project_id => '1', :id => 1, :issue => {:description => Issue.find(1).description}, :notes => 'Foo' + assert_response :success + assert_template 'preview' + assert_not_nil assigns(:notes) + end + + def test_news + get :news, :project_id => 1, + :news => {:title => '', + :description => 'News description', + :summary => ''} + assert_response :success + assert_template 'common/_preview' + assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' }, + :content => /News description/ + end +end diff --git a/test/functional/project_enumerations_controller_test.rb b/test/functional/project_enumerations_controller_test.rb new file mode 100644 index 000000000..942399a96 --- /dev/null +++ b/test/functional/project_enumerations_controller_test.rb @@ -0,0 +1,189 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ProjectEnumerationsControllerTest < ActionController::TestCase + fixtures :all + + def setup + @request.session[:user_id] = nil + Setting.default_language = 'en' + end + + def test_update_to_override_system_activities + @request.session[:user_id] = 2 # manager + billable_field = TimeEntryActivityCustomField.find_by_name("Billable") + + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate + "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value + "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value + "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes + } + + assert_response :redirect + assert_redirected_to 'projects/ecookbook/settings/activities' + + # Created project specific activities... + project = Project.find('ecookbook') + + # ... Design + design = project.time_entry_activities.find_by_name("Design") + assert design, "Project activity not found" + + assert_equal 9, design.parent_id # Relate to the system activity + assert_not_equal design.parent.id, design.id # Different records + assert_equal design.parent.name, design.name # Same name + assert !design.active? + + # ... Development + development = project.time_entry_activities.find_by_name("Development") + assert development, "Project activity not found" + + assert_equal 10, development.parent_id # Relate to the system activity + assert_not_equal development.parent.id, development.id # Different records + assert_equal development.parent.name, development.name # Same name + assert development.active? + assert_equal "0", development.custom_value_for(billable_field).value + + # ... Inactive Activity + previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity") + assert previously_inactive, "Project activity not found" + + assert_equal 14, previously_inactive.parent_id # Relate to the system activity + assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records + assert_equal previously_inactive.parent.name, previously_inactive.name # Same name + assert previously_inactive.active? + assert_equal "1", previously_inactive.custom_value_for(billable_field).value + + # ... QA + assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified" + end + + def test_update_will_update_project_specific_activities + @request.session[:user_id] = 2 # manager + + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific', + :parent => TimeEntryActivity.find(:first), + :project => Project.find(1), + :active => true + }) + assert project_activity.save + project_activity_two = TimeEntryActivity.new({ + :name => 'Project Specific Two', + :parent => TimeEntryActivity.find(:last), + :project => Project.find(1), + :active => true + }) + assert project_activity_two.save + + + put :update, :project_id => 1, :enumerations => { + project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate + project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate + } + + assert_response :redirect + assert_redirected_to 'projects/ecookbook/settings/activities' + + # Created project specific activities... + project = Project.find('ecookbook') + assert_equal 2, project.time_entry_activities.count + + activity_one = project.time_entry_activities.find_by_name(project_activity.name) + assert activity_one, "Project activity not found" + assert_equal project_activity.id, activity_one.id + assert !activity_one.active? + + activity_two = project.time_entry_activities.find_by_name(project_activity_two.name) + assert activity_two, "Project activity not found" + assert_equal project_activity_two.id, activity_two.id + assert !activity_two.active? + end + + def test_update_when_creating_new_activities_will_convert_existing_data + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size + + @request.session[:user_id] = 2 # manager + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate + } + assert_response :redirect + + # No more TimeEntries using the system activity + assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities" + # All TimeEntries using project activity + project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1) + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity" + end + + def test_update_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised + # TODO: Need to cause an exception on create but these tests + # aren't setup for mocking. Just create a record now so the + # second one is a dupicate + parent = TimeEntryActivity.find(9) + TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true}) + TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'}) + + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size + assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size + + @request.session[:user_id] = 2 # manager + put :update, :project_id => 1, :enumerations => { + "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design + "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value + } + assert_response :redirect + + # TimeEntries shouldn't have been reassigned on the failed record + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities" + # TimeEntries shouldn't have been reassigned on the saved record either + assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities" + end + + def test_destroy + @request.session[:user_id] = 2 # manager + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific', + :parent => TimeEntryActivity.find(:first), + :project => Project.find(1), + :active => true + }) + assert project_activity.save + project_activity_two = TimeEntryActivity.new({ + :name => 'Project Specific Two', + :parent => TimeEntryActivity.find(:last), + :project => Project.find(1), + :active => true + }) + assert project_activity_two.save + + delete :destroy, :project_id => 1 + assert_response :redirect + assert_redirected_to 'projects/ecookbook/settings/activities' + + assert_nil TimeEntryActivity.find_by_id(project_activity.id) + assert_nil TimeEntryActivity.find_by_id(project_activity_two.id) + end + + def test_destroy_should_reassign_time_entries_back_to_the_system_activity + @request.session[:user_id] = 2 # manager + project_activity = TimeEntryActivity.new({ + :name => 'Project Specific Design', + :parent => TimeEntryActivity.find(9), + :project => Project.find(1), + :active => true + }) + assert project_activity.save + assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9]) + assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size + + delete :destroy, :project_id => 1 + assert_response :redirect + assert_redirected_to 'projects/ecookbook/settings/activities' + + assert_nil TimeEntryActivity.find_by_id(project_activity.id) + assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity" + assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity" + end + +end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 988496cec..8a9bbe6d5 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -87,20 +87,64 @@ class ProjectsControllerTest < ActionController::TestCase end end - context "#add" do + context "#new" do context "by admin user" do setup do @request.session[:user_id] = 1 end should "accept get" do - get :add + get :new assert_response :success - assert_template 'add' + assert_template 'new' + end + + end + + context "by non-admin user with add_project permission" do + setup do + Role.non_member.add_permission! :add_project + @request.session[:user_id] = 9 + end + + should "accept get" do + get :new + assert_response :success + assert_template 'new' + assert_no_tag :select, :attributes => {:name => 'project[parent_id]'} + end + end + + context "by non-admin user with add_subprojects permission" do + setup do + Role.find(1).remove_permission! :add_project + Role.find(1).add_permission! :add_subprojects + @request.session[:user_id] = 2 end - should "accept post" do - post :add, :project => { :name => "blog", + should "accept get" do + get :new, :parent_id => 'ecookbook' + assert_response :success + assert_template 'new' + # parent project selected + assert_tag :select, :attributes => {:name => 'project[parent_id]'}, + :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}} + # no empty value + assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}, + :child => {:tag => 'option', :attributes => {:value => ''}} + end + end + + end + + context "POST :create" do + context "by admin user" do + setup do + @request.session[:user_id] = 1 + end + + should "create a new project" do + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -115,8 +159,8 @@ class ProjectsControllerTest < ActionController::TestCase assert_nil project.parent end - should "accept post with parent" do - post :add, :project => { :name => "blog", + should "create a new subproject" do + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -137,15 +181,8 @@ class ProjectsControllerTest < ActionController::TestCase @request.session[:user_id] = 9 end - should "accept get" do - get :add - assert_response :success - assert_template 'add' - assert_no_tag :select, :attributes => {:name => 'project[parent_id]'} - end - - should "accept post" do - post :add, :project => { :name => "blog", + should "accept create a Project" do + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -166,7 +203,7 @@ class ProjectsControllerTest < ActionController::TestCase should "fail with parent_id" do assert_no_difference 'Project.count' do - post :add, :project => { :name => "blog", + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -188,20 +225,8 @@ class ProjectsControllerTest < ActionController::TestCase @request.session[:user_id] = 2 end - should "accept get" do - get :add, :parent_id => 'ecookbook' - assert_response :success - assert_template 'add' - # parent project selected - assert_tag :select, :attributes => {:name => 'project[parent_id]'}, - :child => {:tag => 'option', :attributes => {:value => '1', :selected => 'selected'}} - # no empty value - assert_no_tag :select, :attributes => {:name => 'project[parent_id]'}, - :child => {:tag => 'option', :attributes => {:value => ''}} - end - - should "accept post with parent_id" do - post :add, :project => { :name => "blog", + should "create a project with a parent_id" do + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -214,7 +239,7 @@ class ProjectsControllerTest < ActionController::TestCase should "fail without parent_id" do assert_no_difference 'Project.count' do - post :add, :project => { :name => "blog", + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -230,7 +255,7 @@ class ProjectsControllerTest < ActionController::TestCase should "fail with unauthorized parent_id" do assert !User.find(2).member_of?(Project.find(6)) assert_no_difference 'Project.count' do - post :add, :project => { :name => "blog", + post :create, :project => { :name => "blog", :description => "weblog", :identifier => "blog", :is_public => 1, @@ -293,9 +318,9 @@ class ProjectsControllerTest < ActionController::TestCase assert_template 'settings' end - def test_edit + def test_update @request.session[:user_id] = 2 # manager - post :edit, :id => 1, :project => {:name => 'Test changed name', + post :update, :id => 1, :project => {:name => 'Test changed name', :issue_custom_field_ids => ['']} assert_redirected_to 'projects/ecookbook/settings' project = Project.find(1) @@ -317,170 +342,6 @@ class ProjectsControllerTest < ActionController::TestCase assert_nil Project.find_by_id(1) end - def test_add_file - set_tmp_attachments_directory - @request.session[:user_id] = 2 - Setting.notified_events = ['file_added'] - ActionMailer::Base.deliveries.clear - - assert_difference 'Attachment.count' do - post :add_file, :id => 1, :version_id => '', - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} - end - assert_redirected_to 'projects/ecookbook/files' - a = Attachment.find(:first, :order => 'created_on DESC') - assert_equal 'testfile.txt', a.filename - assert_equal Project.find(1), a.container - - mail = ActionMailer::Base.deliveries.last - assert_kind_of TMail::Mail, mail - assert_equal "[eCookbook] New file", mail.subject - assert mail.body.include?('testfile.txt') - end - - def test_add_version_file - set_tmp_attachments_directory - @request.session[:user_id] = 2 - Setting.notified_events = ['file_added'] - - assert_difference 'Attachment.count' do - post :add_file, :id => 1, :version_id => '2', - :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}} - end - assert_redirected_to 'projects/ecookbook/files' - a = Attachment.find(:first, :order => 'created_on DESC') - assert_equal 'testfile.txt', a.filename - assert_equal Version.find(2), a.container - end - - def test_list_files - get :list_files, :id => 1 - assert_response :success - assert_template 'list_files' - assert_not_nil assigns(:containers) - - # file attached to the project - assert_tag :a, :content => 'project_file.zip', - :attributes => { :href => '/attachments/download/8/project_file.zip' } - - # file attached to a project's version - assert_tag :a, :content => 'version_file.zip', - :attributes => { :href => '/attachments/download/9/version_file.zip' } - end - - def test_roadmap - get :roadmap, :id => 1 - assert_response :success - assert_template 'roadmap' - assert_not_nil assigns(:versions) - # Version with no date set appears - assert assigns(:versions).include?(Version.find(3)) - # Completed version doesn't appear - assert !assigns(:versions).include?(Version.find(1)) - end - - def test_roadmap_with_completed_versions - get :roadmap, :id => 1, :completed => 1 - assert_response :success - assert_template 'roadmap' - assert_not_nil assigns(:versions) - # Version with no date set appears - assert assigns(:versions).include?(Version.find(3)) - # Completed version appears - assert assigns(:versions).include?(Version.find(1)) - end - - def test_roadmap_showing_subprojects_versions - @subproject_version = Version.generate!(:project => Project.find(3)) - get :roadmap, :id => 1, :with_subprojects => 1 - assert_response :success - assert_template 'roadmap' - assert_not_nil assigns(:versions) - - assert assigns(:versions).include?(Version.find(4)), "Shared version not found" - assert assigns(:versions).include?(@subproject_version), "Subproject version not found" - end - def test_project_activity - get :activity, :id => 1, :with_subprojects => 0 - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events_by_day) - - assert_tag :tag => "h3", - :content => /#{2.days.ago.to_date.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue-edit/ }, - :child => { :tag => "a", - :content => /(#{IssueStatus.find(2).name})/, - } - } - } - end - - def test_previous_project_activity - get :activity, :id => 1, :from => 3.days.ago.to_date - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events_by_day) - - assert_tag :tag => "h3", - :content => /#{3.day.ago.to_date.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue/ }, - :child => { :tag => "a", - :content => /#{Issue.find(1).subject}/, - } - } - } - end - - def test_global_activity - get :activity - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events_by_day) - - assert_tag :tag => "h3", - :content => /#{5.day.ago.to_date.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue/ }, - :child => { :tag => "a", - :content => /#{Issue.find(5).subject}/, - } - } - } - end - - def test_user_activity - get :activity, :user_id => 2 - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events_by_day) - - assert_tag :tag => "h3", - :content => /#{3.day.ago.to_date.day}/, - :sibling => { :tag => "dl", - :child => { :tag => "dt", - :attributes => { :class => /issue/ }, - :child => { :tag => "a", - :content => /#{Issue.find(1).subject}/, - } - } - } - end - - def test_activity_atom_feed - get :activity, :format => 'atom' - assert_response :success - assert_template 'common/feed.atom.rxml' - assert_tag :tag => 'entry', :child => { - :tag => 'link', - :attributes => {:href => 'http://test.host/issues/11'}} - end - def test_archive @request.session[:user_id] = 1 # admin post :archive, :id => 1 @@ -528,6 +389,17 @@ class ProjectsControllerTest < ActionController::TestCase assert_redirected_to :controller => 'admin', :action => 'projects' end + context "POST :copy" do + should "TODO: test the rest of the method" + + should "redirect to the project settings when successful" do + @request.session[:user_id] = 1 # admin + post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'} + assert_response :redirect + assert_redirected_to :controller => 'projects', :action => 'settings' + end + end + def test_jump_should_redirect_to_active_tab get :show, :id => 1, :jump => 'issues' assert_redirected_to 'projects/ecookbook/issues' @@ -545,184 +417,6 @@ class ProjectsControllerTest < ActionController::TestCase assert_template 'show' end - def test_reset_activities - @request.session[:user_id] = 2 # manager - project_activity = TimeEntryActivity.new({ - :name => 'Project Specific', - :parent => TimeEntryActivity.find(:first), - :project => Project.find(1), - :active => true - }) - assert project_activity.save - project_activity_two = TimeEntryActivity.new({ - :name => 'Project Specific Two', - :parent => TimeEntryActivity.find(:last), - :project => Project.find(1), - :active => true - }) - assert project_activity_two.save - - delete :reset_activities, :id => 1 - assert_response :redirect - assert_redirected_to 'projects/ecookbook/settings/activities' - - assert_nil TimeEntryActivity.find_by_id(project_activity.id) - assert_nil TimeEntryActivity.find_by_id(project_activity_two.id) - end - - def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity - @request.session[:user_id] = 2 # manager - project_activity = TimeEntryActivity.new({ - :name => 'Project Specific Design', - :parent => TimeEntryActivity.find(9), - :project => Project.find(1), - :active => true - }) - assert project_activity.save - assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9]) - assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size - - delete :reset_activities, :id => 1 - assert_response :redirect - assert_redirected_to 'projects/ecookbook/settings/activities' - - assert_nil TimeEntryActivity.find_by_id(project_activity.id) - assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity" - assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity" - end - - def test_save_activities_to_override_system_activities - @request.session[:user_id] = 2 # manager - billable_field = TimeEntryActivityCustomField.find_by_name("Billable") - - post :save_activities, :id => 1, :enumerations => { - "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate - "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value - "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value - "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes - } - - assert_response :redirect - assert_redirected_to 'projects/ecookbook/settings/activities' - - # Created project specific activities... - project = Project.find('ecookbook') - - # ... Design - design = project.time_entry_activities.find_by_name("Design") - assert design, "Project activity not found" - - assert_equal 9, design.parent_id # Relate to the system activity - assert_not_equal design.parent.id, design.id # Different records - assert_equal design.parent.name, design.name # Same name - assert !design.active? - - # ... Development - development = project.time_entry_activities.find_by_name("Development") - assert development, "Project activity not found" - - assert_equal 10, development.parent_id # Relate to the system activity - assert_not_equal development.parent.id, development.id # Different records - assert_equal development.parent.name, development.name # Same name - assert development.active? - assert_equal "0", development.custom_value_for(billable_field).value - - # ... Inactive Activity - previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity") - assert previously_inactive, "Project activity not found" - - assert_equal 14, previously_inactive.parent_id # Relate to the system activity - assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records - assert_equal previously_inactive.parent.name, previously_inactive.name # Same name - assert previously_inactive.active? - assert_equal "1", previously_inactive.custom_value_for(billable_field).value - - # ... QA - assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified" - end - - def test_save_activities_will_update_project_specific_activities - @request.session[:user_id] = 2 # manager - - project_activity = TimeEntryActivity.new({ - :name => 'Project Specific', - :parent => TimeEntryActivity.find(:first), - :project => Project.find(1), - :active => true - }) - assert project_activity.save - project_activity_two = TimeEntryActivity.new({ - :name => 'Project Specific Two', - :parent => TimeEntryActivity.find(:last), - :project => Project.find(1), - :active => true - }) - assert project_activity_two.save - - - post :save_activities, :id => 1, :enumerations => { - project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate - project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate - } - - assert_response :redirect - assert_redirected_to 'projects/ecookbook/settings/activities' - - # Created project specific activities... - project = Project.find('ecookbook') - assert_equal 2, project.time_entry_activities.count - - activity_one = project.time_entry_activities.find_by_name(project_activity.name) - assert activity_one, "Project activity not found" - assert_equal project_activity.id, activity_one.id - assert !activity_one.active? - - activity_two = project.time_entry_activities.find_by_name(project_activity_two.name) - assert activity_two, "Project activity not found" - assert_equal project_activity_two.id, activity_two.id - assert !activity_two.active? - end - - def test_save_activities_when_creating_new_activities_will_convert_existing_data - assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size - - @request.session[:user_id] = 2 # manager - post :save_activities, :id => 1, :enumerations => { - "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate - } - assert_response :redirect - - # No more TimeEntries using the system activity - assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities" - # All TimeEntries using project activity - project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1) - assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity" - end - - def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised - # TODO: Need to cause an exception on create but these tests - # aren't setup for mocking. Just create a record now so the - # second one is a dupicate - parent = TimeEntryActivity.find(9) - TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true}) - TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'}) - - assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size - assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size - - @request.session[:user_id] = 2 # manager - post :save_activities, :id => 1, :enumerations => { - "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design - "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value - } - assert_response :redirect - - # TimeEntries shouldn't have been reassigned on the failed record - assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities" - # TimeEntries shouldn't have been reassigned on the saved record either - assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities" - end - # A hook that is manually registered later class ProjectBasedTemplate < Redmine::Hook::ViewListener def view_layouts_base_html_head(context) diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb index 317261a13..941fbcf1b 100644 --- a/test/functional/repositories_git_controller_test.rb +++ b/test/functional/repositories_git_controller_test.rb @@ -50,7 +50,7 @@ class RepositoriesGitControllerTest < ActionController::TestCase assert_response :success assert_template 'show' assert_not_nil assigns(:entries) - assert_equal 7, assigns(:entries).size + assert_equal 9, assigns(:entries).size assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} assert assigns(:entries).detect {|e| e.name == 'this_is_a_really_long_and_verbose_directory_name' && e.kind == 'dir'} assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} @@ -58,6 +58,8 @@ class RepositoriesGitControllerTest < ActionController::TestCase assert assigns(:entries).detect {|e| e.name == 'copied_README' && e.kind == 'file'} assert assigns(:entries).detect {|e| e.name == 'new_file.txt' && e.kind == 'file'} assert assigns(:entries).detect {|e| e.name == 'renamed_test.txt' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == 'filemane with spaces.txt' && e.kind == 'file'} + assert assigns(:entries).detect {|e| e.name == ' filename with a leading space.txt ' && e.kind == 'file'} end def test_browse_branch diff --git a/test/functional/time_entry_reports_controller_test.rb b/test/functional/time_entry_reports_controller_test.rb new file mode 100644 index 000000000..0465c88ec --- /dev/null +++ b/test/functional/time_entry_reports_controller_test.rb @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +require File.dirname(__FILE__) + '/../test_helper' + +class TimeEntryReportsControllerTest < ActionController::TestCase + fixtures :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values + + def test_report_no_criteria + get :report, :project_id => 1 + assert_response :success + assert_template 'report' + end + + def test_report_all_projects + get :report + assert_response :success + assert_template 'report' + end + + def test_report_all_projects_denied + r = Role.anonymous + r.permissions.delete(:view_time_entries) + r.permissions_will_change! + r.save + get :report + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport' + end + + def test_report_all_projects_one_criteria + get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "8.65", "%.2f" % assigns(:total_hours) + end + + def test_report_all_time + get :report, :project_id => 1, :criterias => ['project', 'issue'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + end + + def test_report_all_time_by_day + get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day' + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + assert_tag :tag => 'th', :content => '2007-03-12' + end + + def test_report_one_criteria + get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "8.65", "%.2f" % assigns(:total_hours) + end + + def test_report_two_criterias + get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "162.90", "%.2f" % assigns(:total_hours) + end + + def test_report_one_day + get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "4.25", "%.2f" % assigns(:total_hours) + end + + def test_report_at_issue_level + get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "154.25", "%.2f" % assigns(:total_hours) + end + + def test_report_custom_field_criteria + get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_not_nil assigns(:criterias) + assert_equal 3, assigns(:criterias).size + assert_equal "162.90", "%.2f" % assigns(:total_hours) + # Custom field column + assert_tag :tag => 'th', :content => 'Database' + # Custom field row + assert_tag :tag => 'td', :content => 'MySQL', + :sibling => { :tag => 'td', :attributes => { :class => 'hours' }, + :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' }, + :content => '1' }} + # Second custom field column + assert_tag :tag => 'th', :content => 'Billable' + end + + def test_report_one_criteria_no_result + get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project'] + assert_response :success + assert_template 'report' + assert_not_nil assigns(:total_hours) + assert_equal "0.00", "%.2f" % assigns(:total_hours) + end + + def test_report_all_projects_csv_export + get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first + # Total row + assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last + end + + def test_report_csv_export + get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv" + assert_response :success + assert_equal 'text/csv', @response.content_type + lines = @response.body.chomp.split("\n") + # Headers + assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first + # Total row + assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last + end + +end diff --git a/test/functional/timelog_controller_test.rb b/test/functional/timelog_controller_test.rb index 9e30a0ae8..91b8ca47c 100644 --- a/test/functional/timelog_controller_test.rb +++ b/test/functional/timelog_controller_test.rb @@ -31,9 +31,9 @@ class TimelogControllerTest < ActionController::TestCase @response = ActionController::TestResponse.new end - def test_get_edit + def test_get_new @request.session[:user_id] = 3 - get :edit, :project_id => 1 + get :new, :project_id => 1 assert_response :success assert_template 'edit' # Default activity selected @@ -41,6 +41,15 @@ class TimelogControllerTest < ActionController::TestCase :content => 'Development' end + def test_get_new_should_only_show_active_time_entry_activities + @request.session[:user_id] = 3 + get :new, :project_id => 1 + assert_response :success + assert_template 'edit' + assert_no_tag :tag => 'option', :content => 'Inactive Activity' + + end + def test_get_edit_existing_time @request.session[:user_id] = 2 get :edit, :id => 2, :project_id => nil @@ -50,15 +59,6 @@ class TimelogControllerTest < ActionController::TestCase assert_tag :tag => 'form', :attributes => { :action => '/projects/ecookbook/timelog/edit/2' } end - def test_get_edit_should_only_show_active_time_entry_activities - @request.session[:user_id] = 3 - get :edit, :project_id => 1 - assert_response :success - assert_template 'edit' - assert_no_tag :tag => 'option', :content => 'Inactive Activity' - - end - def test_get_edit_with_an_existing_time_entry_with_inactive_activity te = TimeEntry.find(1) te.activity = TimeEntryActivity.find_by_name("Inactive Activity") @@ -72,18 +72,18 @@ class TimelogControllerTest < ActionController::TestCase assert_tag :tag => 'option', :content => '--- Please select ---' end - def test_post_edit + def test_post_create # TODO: should POST to issues’ time log instead of project. change form # and routing @request.session[:user_id] = 3 - post :edit, :project_id => 1, + post :create, :project_id => 1, :time_entry => {:comments => 'Some work on TimelogControllerTest', # Not the default activity :activity_id => '11', :spent_on => '2008-03-14', :issue_id => '1', :hours => '7.3'} - assert_redirected_to :action => 'details', :project_id => 'ecookbook' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' i = Issue.find(1) t = TimeEntry.find_by_comments('Some work on TimelogControllerTest') @@ -104,7 +104,7 @@ class TimelogControllerTest < ActionController::TestCase post :edit, :id => 1, :time_entry => {:issue_id => '2', :hours => '8'} - assert_redirected_to :action => 'details', :project_id => 'ecookbook' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' entry.reload assert_equal 8, entry.hours @@ -115,7 +115,7 @@ class TimelogControllerTest < ActionController::TestCase def test_destroy @request.session[:user_id] = 2 post :destroy, :id => 1 - assert_redirected_to :action => 'details', :project_id => 'ecookbook' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' assert_equal I18n.t(:notice_successful_delete), flash[:notice] assert_nil TimeEntry.find_by_id(1) end @@ -129,7 +129,7 @@ class TimelogControllerTest < ActionController::TestCase @request.session[:user_id] = 2 post :destroy, :id => 1 - assert_redirected_to :action => 'details', :project_id => 'ecookbook' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' assert_equal I18n.t(:notice_unable_delete_time_entry), flash[:error] assert_not_nil TimeEntry.find_by_id(1) @@ -137,145 +137,18 @@ class TimelogControllerTest < ActionController::TestCase TimeEntry.before_destroy.reject! {|callback| callback.method == :stop_callback_chain } end - def test_report_no_criteria - get :report, :project_id => 1 + def test_index_all_projects + get :index assert_response :success - assert_template 'report' - end - - def test_report_all_projects - get :report - assert_response :success - assert_template 'report' - end - - def test_report_all_projects_denied - r = Role.anonymous - r.permissions.delete(:view_time_entries) - r.permissions_will_change! - r.save - get :report - assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftime_entries%2Freport' - end - - def test_report_all_projects_one_criteria - get :report, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project'] - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "8.65", "%.2f" % assigns(:total_hours) - end - - def test_report_all_time - get :report, :project_id => 1, :criterias => ['project', 'issue'] - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "162.90", "%.2f" % assigns(:total_hours) - end - - def test_report_all_time_by_day - get :report, :project_id => 1, :criterias => ['project', 'issue'], :columns => 'day' - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "162.90", "%.2f" % assigns(:total_hours) - assert_tag :tag => 'th', :content => '2007-03-12' - end - - def test_report_one_criteria - get :report, :project_id => 1, :columns => 'week', :from => "2007-04-01", :to => "2007-04-30", :criterias => ['project'] - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "8.65", "%.2f" % assigns(:total_hours) - end - - def test_report_two_criterias - get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"] - assert_response :success - assert_template 'report' + assert_template 'index' assert_not_nil assigns(:total_hours) assert_equal "162.90", "%.2f" % assigns(:total_hours) end - def test_report_one_day - get :report, :project_id => 1, :columns => 'day', :from => "2007-03-23", :to => "2007-03-23", :criterias => ["member", "activity"] + def test_index_at_project_level + get :index, :project_id => 1 assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "4.25", "%.2f" % assigns(:total_hours) - end - - def test_report_at_issue_level - get :report, :project_id => 1, :issue_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-12-31", :criterias => ["member", "activity"] - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "154.25", "%.2f" % assigns(:total_hours) - end - - def test_report_custom_field_criteria - get :report, :project_id => 1, :criterias => ['project', 'cf_1', 'cf_7'] - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_not_nil assigns(:criterias) - assert_equal 3, assigns(:criterias).size - assert_equal "162.90", "%.2f" % assigns(:total_hours) - # Custom field column - assert_tag :tag => 'th', :content => 'Database' - # Custom field row - assert_tag :tag => 'td', :content => 'MySQL', - :sibling => { :tag => 'td', :attributes => { :class => 'hours' }, - :child => { :tag => 'span', :attributes => { :class => 'hours hours-int' }, - :content => '1' }} - # Second custom field column - assert_tag :tag => 'th', :content => 'Billable' - end - - def test_report_one_criteria_no_result - get :report, :project_id => 1, :columns => 'week', :from => "1998-04-01", :to => "1998-04-30", :criterias => ['project'] - assert_response :success - assert_template 'report' - assert_not_nil assigns(:total_hours) - assert_equal "0.00", "%.2f" % assigns(:total_hours) - end - - def test_report_all_projects_csv_export - get :report, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv" - assert_response :success - assert_equal 'text/csv', @response.content_type - lines = @response.body.chomp.split("\n") - # Headers - assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first - # Total row - assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last - end - - def test_report_csv_export - get :report, :project_id => 1, :columns => 'month', :from => "2007-01-01", :to => "2007-06-30", :criterias => ["project", "member", "activity"], :format => "csv" - assert_response :success - assert_equal 'text/csv', @response.content_type - lines = @response.body.chomp.split("\n") - # Headers - assert_equal 'Project,Member,Activity,2007-1,2007-2,2007-3,2007-4,2007-5,2007-6,Total', lines.first - # Total row - assert_equal 'Total,"","","","",154.25,8.65,"","",162.90', lines.last - end - - def test_details_all_projects - get :details - assert_response :success - assert_template 'details' - assert_not_nil assigns(:total_hours) - assert_equal "162.90", "%.2f" % assigns(:total_hours) - end - - def test_details_at_project_level - get :details, :project_id => 1 - assert_response :success - assert_template 'details' + assert_template 'index' assert_not_nil assigns(:entries) assert_equal 4, assigns(:entries).size # project and subproject @@ -283,14 +156,14 @@ class TimelogControllerTest < ActionController::TestCase assert_not_nil assigns(:total_hours) assert_equal "162.90", "%.2f" % assigns(:total_hours) # display all time by default - assert_equal '2007-03-11'.to_date, assigns(:from) + assert_equal '2007-03-12'.to_date, assigns(:from) assert_equal '2007-04-22'.to_date, assigns(:to) end - def test_details_at_project_level_with_date_range - get :details, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30' + def test_index_at_project_level_with_date_range + get :index, :project_id => 1, :from => '2007-03-20', :to => '2007-04-30' assert_response :success - assert_template 'details' + assert_template 'index' assert_not_nil assigns(:entries) assert_equal 3, assigns(:entries).size assert_not_nil assigns(:total_hours) @@ -299,57 +172,57 @@ class TimelogControllerTest < ActionController::TestCase assert_equal '2007-04-30'.to_date, assigns(:to) end - def test_details_at_project_level_with_period - get :details, :project_id => 1, :period => '7_days' + def test_index_at_project_level_with_period + get :index, :project_id => 1, :period => '7_days' assert_response :success - assert_template 'details' + assert_template 'index' assert_not_nil assigns(:entries) assert_not_nil assigns(:total_hours) assert_equal Date.today - 7, assigns(:from) assert_equal Date.today, assigns(:to) end - def test_details_one_day - get :details, :project_id => 1, :from => "2007-03-23", :to => "2007-03-23" + def test_index_one_day + get :index, :project_id => 1, :from => "2007-03-23", :to => "2007-03-23" assert_response :success - assert_template 'details' + assert_template 'index' assert_not_nil assigns(:total_hours) assert_equal "4.25", "%.2f" % assigns(:total_hours) end - def test_details_at_issue_level - get :details, :issue_id => 1 + def test_index_at_issue_level + get :index, :issue_id => 1 assert_response :success - assert_template 'details' + assert_template 'index' assert_not_nil assigns(:entries) assert_equal 2, assigns(:entries).size assert_not_nil assigns(:total_hours) assert_equal 154.25, assigns(:total_hours) - # display all time by default - assert_equal '2007-03-11'.to_date, assigns(:from) + # display all time based on what's been logged + assert_equal '2007-03-12'.to_date, assigns(:from) assert_equal '2007-04-22'.to_date, assigns(:to) end - def test_details_atom_feed - get :details, :project_id => 1, :format => 'atom' + def test_index_atom_feed + get :index, :project_id => 1, :format => 'atom' assert_response :success assert_equal 'application/atom+xml', @response.content_type assert_not_nil assigns(:items) assert assigns(:items).first.is_a?(TimeEntry) end - def test_details_all_projects_csv_export + def test_index_all_projects_csv_export Setting.date_format = '%m/%d/%Y' - get :details, :format => 'csv' + get :index, :format => 'csv' assert_response :success assert_equal 'text/csv', @response.content_type assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n") assert @response.body.include?("\n04/21/2007,redMine Admin,Design,eCookbook,3,Bug,Error 281 when updating a recipe,1.0,\"\"\n") end - def test_details_csv_export + def test_index_csv_export Setting.date_format = '%m/%d/%Y' - get :details, :project_id => 1, :format => 'csv' + get :index, :project_id => 1, :format => 'csv' assert_response :success assert_equal 'text/csv', @response.content_type assert @response.body.include?("Date,User,Activity,Project,Issue,Tracker,Subject,Hours,Comment\n") diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index d178f8f85..5e288d445 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -24,7 +24,7 @@ class UsersController; def rescue_action(e) raise e end; end class UsersControllerTest < ActionController::TestCase include Redmine::I18n - fixtures :users, :projects, :members, :member_roles, :roles + fixtures :users, :projects, :members, :member_roles, :roles, :auth_sources def setup @controller = UsersController.new @@ -96,15 +96,76 @@ class UsersControllerTest < ActionController::TestCase assert_response 200 assert_not_nil assigns(:user) end + + def test_show_displays_memberships_based_on_project_visibility + @request.session[:user_id] = 1 + get :show, :id => 2 + assert_response :success + memberships = assigns(:memberships) + assert_not_nil memberships + project_ids = memberships.map(&:project_id) + assert project_ids.include?(2) #private project admin can see + end - def test_edit + context "GET :new" do + setup do + get :new + end + + should_assign_to :user + should_respond_with :success + should_render_template :new + end + + context "POST :create" do + context "when successful" do + setup do + post :create, :user => { + :firstname => 'John', + :lastname => 'Doe', + :login => 'jdoe', + :password => 'test', + :password_confirmation => 'test', + :mail => 'jdoe@gmail.com' + }, + :notification_option => 'none' + end + + should_assign_to :user + should_respond_with :redirect + should_redirect_to('user edit') { {:controller => 'users', :action => 'edit', :id => User.find_by_login('jdoe')}} + + should 'set the users mail notification' do + user = User.last + assert_equal 'none', user.mail_notification + end + end + + context "when unsuccessful" do + setup do + post :create, :user => {} + end + + should_assign_to :user + should_respond_with :success + should_render_template :new + end + + end + + def test_update ActionMailer::Base.deliveries.clear - post :edit, :id => 2, :user => {:firstname => 'Changed'} - assert_equal 'Changed', User.find(2).firstname + put :update, :id => 2, :user => {:firstname => 'Changed'}, :notification_option => 'all', :pref => {:hide_mail => '1', :comments_sorting => 'desc'} + + user = User.find(2) + assert_equal 'Changed', user.firstname + assert_equal 'all', user.mail_notification + assert_equal true, user.pref[:hide_mail] + assert_equal 'desc', user.pref[:comments_sorting] assert ActionMailer::Base.deliveries.empty? end - def test_edit_with_activation_should_send_a_notification + def test_update_with_activation_should_send_a_notification u = User.new(:firstname => 'Foo', :lastname => 'Bar', :mail => 'foo.bar@somenet.foo', :language => 'fr') u.login = 'foo' u.status = User::STATUS_REGISTERED @@ -112,7 +173,7 @@ class UsersControllerTest < ActionController::TestCase ActionMailer::Base.deliveries.clear Setting.bcc_recipients = '1' - post :edit, :id => u.id, :user => {:status => User::STATUS_ACTIVE} + put :update, :id => u.id, :user => {:status => User::STATUS_ACTIVE} assert u.reload.active? mail = ActionMailer::Base.deliveries.last assert_not_nil mail @@ -120,12 +181,12 @@ class UsersControllerTest < ActionController::TestCase assert mail.body.include?(ll('fr', :notice_account_activated)) end - def test_edit_with_password_change_should_send_a_notification + def test_updat_with_password_change_should_send_a_notification ActionMailer::Base.deliveries.clear Setting.bcc_recipients = '1' u = User.find(2) - post :edit, :id => u.id, :user => {}, :password => 'newpass', :password_confirmation => 'newpass', :send_information => '1' + put :update, :id => u.id, :user => {}, :password => 'newpass', :password_confirmation => 'newpass', :send_information => '1' assert_equal User.hash_password('newpass'), u.reload.hashed_password mail = ActionMailer::Base.deliveries.last @@ -133,6 +194,18 @@ class UsersControllerTest < ActionController::TestCase assert_equal [u.mail], mail.bcc assert mail.body.include?('newpass') end + + test "put :update with a password change to an AuthSource user switching to Internal authentication" do + # Configure as auth source + u = User.find(2) + u.auth_source = AuthSource.find(1) + u.save! + + put :update, :id => u.id, :user => {:auth_source_id => ''}, :password => 'newpass', :password_confirmation => 'newpass' + + assert_equal nil, u.reload.auth_source + assert_equal User.hash_password('newpass'), u.reload.hashed_password + end def test_edit_membership post :edit_membership, :id => 2, :membership_id => 1, diff --git a/test/functional/versions_controller_test.rb b/test/functional/versions_controller_test.rb index e4864c908..e4ac5c068 100644 --- a/test/functional/versions_controller_test.rb +++ b/test/functional/versions_controller_test.rb @@ -31,6 +31,41 @@ class VersionsControllerTest < ActionController::TestCase User.current = nil end + def test_index + get :index, :project_id => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version doesn't appear + assert !assigns(:versions).include?(Version.find(1)) + # Context menu on issues + assert_select "script", :text => Regexp.new(Regexp.escape("new ContextMenu('/issues/context_menu')")) + end + + def test_index_with_completed_versions + get :index, :project_id => 1, :completed => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + # Version with no date set appears + assert assigns(:versions).include?(Version.find(3)) + # Completed version appears + assert assigns(:versions).include?(Version.find(1)) + end + + def test_index_showing_subprojects_versions + @subproject_version = Version.generate!(:project => Project.find(3)) + get :index, :project_id => 1, :with_subprojects => 1 + assert_response :success + assert_template 'index' + assert_not_nil assigns(:versions) + + assert assigns(:versions).include?(Version.find(4)), "Shared version not found" + assert assigns(:versions).include?(@subproject_version), "Subproject version not found" + end + def test_show get :show, :id => 2 assert_response :success @@ -40,10 +75,10 @@ class VersionsControllerTest < ActionController::TestCase assert_tag :tag => 'h2', :content => /1.0/ end - def test_new + def test_create @request.session[:user_id] = 2 # manager assert_difference 'Version.count' do - post :new, :project_id => '1', :version => {:name => 'test_add_version'} + post :create, :project_id => '1', :version => {:name => 'test_add_version'} end assert_redirected_to '/projects/ecookbook/settings/versions' version = Version.find_by_name('test_add_version') @@ -51,10 +86,10 @@ class VersionsControllerTest < ActionController::TestCase assert_equal 1, version.project_id end - def test_new_from_issue_form + def test_create_from_issue_form @request.session[:user_id] = 2 # manager assert_difference 'Version.count' do - xhr :post, :new, :project_id => '1', :version => {:name => 'test_add_version_from_issue_form'} + xhr :post, :create, :project_id => '1', :version => {:name => 'test_add_version_from_issue_form'} end assert_response :success assert_select_rjs :replace, 'issue_fixed_version_id' @@ -73,14 +108,14 @@ class VersionsControllerTest < ActionController::TestCase def test_close_completed Version.update_all("status = 'open'") @request.session[:user_id] = 2 - post :close_completed, :project_id => 'ecookbook' + put :close_completed, :project_id => 'ecookbook' assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' assert_not_nil Version.find_by_status('closed') end - def test_post_edit + def test_post_update @request.session[:user_id] = 2 - post :edit, :id => 2, + put :update, :id => 2, :version => { :name => 'New version name', :effective_date => Date.today.strftime("%Y-%m-%d")} assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' @@ -91,7 +126,7 @@ class VersionsControllerTest < ActionController::TestCase def test_destroy @request.session[:user_id] = 2 - post :destroy, :id => 3 + delete :destroy, :id => 3 assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => 'ecookbook' assert_nil Version.find_by_id(3) end diff --git a/test/integration/admin_test.rb b/test/integration/admin_test.rb index 9ea9e9809..1600f89bd 100644 --- a/test/integration/admin_test.rb +++ b/test/integration/admin_test.rb @@ -22,10 +22,10 @@ class AdminTest < ActionController::IntegrationTest def test_add_user log_user("admin", "admin") - get "/users/add" + get "/users/new" assert_response :success - assert_template "users/add" - post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09" + assert_template "users/new" + post "/users/create", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09" user = User.find_by_login("psmith") assert_kind_of User, user @@ -35,15 +35,15 @@ class AdminTest < ActionController::IntegrationTest assert_kind_of User, logged_user assert_equal "Paul", logged_user.firstname - post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED } + put "users/#{user.id}", :id => user.id, :user => { :status => User::STATUS_LOCKED } assert_redirected_to "/users/#{ user.id }/edit" locked_user = User.try_to_login("psmith", "psmith09") assert_equal nil, locked_user end test "Add a user as an anonymous user should fail" do - post '/users/add', :user => { :login => 'psmith', :firstname => 'Paul'}, :password => "psmith09", :password_confirmation => "psmith09" + post '/users/create', :user => { :login => 'psmith', :firstname => 'Paul'}, :password => "psmith09", :password_confirmation => "psmith09" assert_response :redirect - assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers%2Fnew" + assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fusers" end end diff --git a/test/integration/issues_api_test.rb b/test/integration/issues_api_test.rb index a1e2cb703..e26cf643b 100644 --- a/test/integration/issues_api_test.rb +++ b/test/integration/issues_api_test.rb @@ -198,7 +198,7 @@ class IssuesApiTest < ActionController::IntegrationTest setup do @issue_count = Issue.count @journal_count = Journal.count - @attributes = {:subject => 'API update'} + @attributes = {:subject => 'API update', :notes => 'A new note'} put '/issues/1.xml', {:issue => @attributes}, :authorization => credentials('jsmith') end @@ -214,10 +214,15 @@ class IssuesApiTest < ActionController::IntegrationTest assert_equal Journal.count, @journal_count + 1 end + should "add the note to the journal" do + journal = Journal.last + assert_equal "A new note", journal.notes + end + should "update the issue" do issue = Issue.find(1) @attributes.each do |attribute, value| - assert_equal value, issue.send(attribute) + assert_equal value, issue.send(attribute) unless attribute == :notes end end @@ -252,7 +257,7 @@ class IssuesApiTest < ActionController::IntegrationTest setup do @issue_count = Issue.count @journal_count = Journal.count - @attributes = {:subject => 'API update'} + @attributes = {:subject => 'API update', :notes => 'A new note'} put '/issues/1.json', {:issue => @attributes}, :authorization => credentials('jsmith') end @@ -268,13 +273,18 @@ class IssuesApiTest < ActionController::IntegrationTest assert_equal Journal.count, @journal_count + 1 end + should "add the note to the journal" do + journal = Journal.last + assert_equal "A new note", journal.notes + end + should "update the issue" do issue = Issue.find(1) @attributes.each do |attribute, value| - assert_equal value, issue.send(attribute) + assert_equal value, issue.send(attribute) unless attribute == :notes end end - + end context "PUT /issues/1.json with failed update" do diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb index 0c1a36d34..c8a151cfd 100644 --- a/test/integration/issues_test.rb +++ b/test/integration/issues_test.rb @@ -69,7 +69,7 @@ class IssuesTest < ActionController::IntegrationTest log_user('jsmith', 'jsmith') set_tmp_attachments_directory - put 'issues/1/edit', + put 'issues/1', :notes => 'Some notes', :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'This is an attachment'}} assert_redirected_to "issues/1" diff --git a/test/integration/layout_test.rb b/test/integration/layout_test.rb new file mode 100644 index 000000000..001bf50d2 --- /dev/null +++ b/test/integration/layout_test.rb @@ -0,0 +1,24 @@ +require "#{File.dirname(__FILE__)}/../test_helper" + +class LayoutTest < ActionController::IntegrationTest + fixtures :all + + test "browsing to a missing page should render the base layout" do + get "/users/100000000" + + assert_response :not_found + + # UsersController uses the admin layout by default + assert_select "#admin-menu", :count => 0 + end + + test "browsing to an unauthorized page should render the base layout" do + change_user_password('miscuser9', 'test') + + log_user('miscuser9','test') + + get "/admin" + assert_response :forbidden + assert_select "#admin-menu", :count => 0 + end +end diff --git a/test/integration/routing_test.rb b/test/integration/routing_test.rb index f8476df56..8f96745bb 100644 --- a/test/integration/routing_test.rb +++ b/test/integration/routing_test.rb @@ -19,8 +19,8 @@ require "#{File.dirname(__FILE__)}/../test_helper" class RoutingTest < ActionController::IntegrationTest context "activities" do - should_route :get, "/activity", :controller => 'projects', :action => 'activity', :id => nil - should_route :get, "/activity.atom", :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom' + should_route :get, "/activity", :controller => 'activities', :action => 'index', :id => nil + should_route :get, "/activity.atom", :controller => 'activities', :action => 'index', :id => nil, :format => 'atom' end context "attachments" do @@ -85,22 +85,32 @@ class RoutingTest < ActionController::IntegrationTest # Extra actions should_route :get, "/projects/23/issues/64/copy", :controller => 'issues', :action => 'new', :project_id => '23', :copy_from => '64' - should_route :get, "/issues/1/move", :controller => 'issues', :action => 'move', :id => '1' - should_route :post, "/issues/1/move", :controller => 'issues', :action => 'move', :id => '1' + should_route :get, "/issues/move/new", :controller => 'issue_moves', :action => 'new' + should_route :post, "/issues/move", :controller => 'issue_moves', :action => 'create' - should_route :post, "/issues/1/quoted", :controller => 'issues', :action => 'reply', :id => '1' + should_route :post, "/issues/1/quoted", :controller => 'journals', :action => 'new', :id => '1' should_route :get, "/issues/calendar", :controller => 'calendars', :action => 'show' - should_route :post, "/issues/calendar", :controller => 'calendars', :action => 'show' + should_route :put, "/issues/calendar", :controller => 'calendars', :action => 'update' should_route :get, "/projects/project-name/issues/calendar", :controller => 'calendars', :action => 'show', :project_id => 'project-name' - should_route :post, "/projects/project-name/issues/calendar", :controller => 'calendars', :action => 'show', :project_id => 'project-name' + should_route :put, "/projects/project-name/issues/calendar", :controller => 'calendars', :action => 'update', :project_id => 'project-name' should_route :get, "/issues/gantt", :controller => 'gantts', :action => 'show' - should_route :post, "/issues/gantt", :controller => 'gantts', :action => 'show' + should_route :put, "/issues/gantt", :controller => 'gantts', :action => 'update' should_route :get, "/projects/project-name/issues/gantt", :controller => 'gantts', :action => 'show', :project_id => 'project-name' - should_route :post, "/projects/project-name/issues/gantt", :controller => 'gantts', :action => 'show', :project_id => 'project-name' + should_route :put, "/projects/project-name/issues/gantt", :controller => 'gantts', :action => 'update', :project_id => 'project-name' - should_route :get, "/issues/auto_complete", :controller => 'issues', :action => 'auto_complete' + should_route :get, "/issues/auto_complete", :controller => 'auto_completes', :action => 'issues' + + should_route :get, "/issues/preview/123", :controller => 'previews', :action => 'issue', :id => '123' + should_route :post, "/issues/preview/123", :controller => 'previews', :action => 'issue', :id => '123' + should_route :get, "/issues/context_menu", :controller => 'context_menus', :action => 'issues' + should_route :post, "/issues/context_menu", :controller => 'context_menus', :action => 'issues' + + should_route :get, "/issues/changes", :controller => 'journals', :action => 'index' + + should_route :get, "/issues/bulk_edit", :controller => 'issues', :action => 'bulk_edit' + should_route :post, "/issues/bulk_edit", :controller => 'issues', :action => 'bulk_update' end context "issue categories" do @@ -146,41 +156,46 @@ class RoutingTest < ActionController::IntegrationTest should_route :get, "/news/2", :controller => 'news', :action => 'show', :id => '2' should_route :get, "/projects/567/news/new", :controller => 'news', :action => 'new', :project_id => '567' should_route :get, "/news/234", :controller => 'news', :action => 'show', :id => '234' + should_route :get, "/news/567/edit", :controller => 'news', :action => 'edit', :id => '567' + should_route :get, "/news/preview", :controller => 'previews', :action => 'news' + + should_route :post, "/projects/567/news", :controller => 'news', :action => 'create', :project_id => '567' + should_route :post, "/news/567/comments", :controller => 'comments', :action => 'create', :id => '567' - should_route :post, "/projects/567/news/new", :controller => 'news', :action => 'new', :project_id => '567' - should_route :post, "/news/567/edit", :controller => 'news', :action => 'edit', :id => '567' - should_route :post, "/news/567/destroy", :controller => 'news', :action => 'destroy', :id => '567' + should_route :put, "/news/567", :controller => 'news', :action => 'update', :id => '567' + + should_route :delete, "/news/567", :controller => 'news', :action => 'destroy', :id => '567' + should_route :delete, "/news/567/comments/15", :controller => 'comments', :action => 'destroy', :id => '567', :comment_id => '15' end context "projects" do should_route :get, "/projects", :controller => 'projects', :action => 'index' should_route :get, "/projects.atom", :controller => 'projects', :action => 'index', :format => 'atom' should_route :get, "/projects.xml", :controller => 'projects', :action => 'index', :format => 'xml' - should_route :get, "/projects/new", :controller => 'projects', :action => 'add' + should_route :get, "/projects/new", :controller => 'projects', :action => 'new' should_route :get, "/projects/test", :controller => 'projects', :action => 'show', :id => 'test' should_route :get, "/projects/1.xml", :controller => 'projects', :action => 'show', :id => '1', :format => 'xml' should_route :get, "/projects/4223/settings", :controller => 'projects', :action => 'settings', :id => '4223' should_route :get, "/projects/4223/settings/members", :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members' - should_route :get, "/projects/567/destroy", :controller => 'projects', :action => 'destroy', :id => '567' - should_route :get, "/projects/33/files", :controller => 'projects', :action => 'list_files', :id => '33' - should_route :get, "/projects/33/files/new", :controller => 'projects', :action => 'add_file', :id => '33' - should_route :get, "/projects/33/roadmap", :controller => 'projects', :action => 'roadmap', :id => '33' - should_route :get, "/projects/33/activity", :controller => 'projects', :action => 'activity', :id => '33' - should_route :get, "/projects/33/activity.atom", :controller => 'projects', :action => 'activity', :id => '33', :format => 'atom' + should_route :get, "/projects/33/files", :controller => 'files', :action => 'index', :project_id => '33' + should_route :get, "/projects/33/files/new", :controller => 'files', :action => 'new', :project_id => '33' + should_route :get, "/projects/33/roadmap", :controller => 'versions', :action => 'index', :project_id => '33' + should_route :get, "/projects/33/activity", :controller => 'activities', :action => 'index', :id => '33' + should_route :get, "/projects/33/activity.atom", :controller => 'activities', :action => 'index', :id => '33', :format => 'atom' - should_route :post, "/projects/new", :controller => 'projects', :action => 'add' - should_route :post, "/projects.xml", :controller => 'projects', :action => 'add', :format => 'xml' - should_route :post, "/projects/4223/edit", :controller => 'projects', :action => 'edit', :id => '4223' - should_route :post, "/projects/64/destroy", :controller => 'projects', :action => 'destroy', :id => '64' - should_route :post, "/projects/33/files/new", :controller => 'projects', :action => 'add_file', :id => '33' + should_route :post, "/projects", :controller => 'projects', :action => 'create' + should_route :post, "/projects.xml", :controller => 'projects', :action => 'create', :format => 'xml' + should_route :post, "/projects/33/files", :controller => 'files', :action => 'create', :project_id => '33' should_route :post, "/projects/64/archive", :controller => 'projects', :action => 'archive', :id => '64' should_route :post, "/projects/64/unarchive", :controller => 'projects', :action => 'unarchive', :id => '64' - should_route :post, "/projects/64/activities/save", :controller => 'projects', :action => 'save_activities', :id => '64' - should_route :put, "/projects/1.xml", :controller => 'projects', :action => 'edit', :id => '1', :format => 'xml' + should_route :put, "/projects/64/enumerations", :controller => 'project_enumerations', :action => 'update', :project_id => '64' + should_route :put, "/projects/4223", :controller => 'projects', :action => 'update', :id => '4223' + should_route :put, "/projects/1.xml", :controller => 'projects', :action => 'update', :id => '1', :format => 'xml' + should_route :delete, "/projects/64", :controller => 'projects', :action => 'destroy', :id => '64' should_route :delete, "/projects/1.xml", :controller => 'projects', :action => 'destroy', :id => '1', :format => 'xml' - should_route :delete, "/projects/64/reset_activities", :controller => 'projects', :action => 'reset_activities', :id => '64' + should_route :delete, "/projects/64/enumerations", :controller => 'project_enumerations', :action => 'destroy', :project_id => '64' end context "repositories" do @@ -207,45 +222,58 @@ class RoutingTest < ActionController::IntegrationTest end context "timelogs" do - should_route :get, "/issues/567/time_entries/new", :controller => 'timelog', :action => 'edit', :issue_id => '567' - should_route :get, "/projects/ecookbook/time_entries/new", :controller => 'timelog', :action => 'edit', :project_id => 'ecookbook' - should_route :get, "/projects/ecookbook/issues/567/time_entries/new", :controller => 'timelog', :action => 'edit', :project_id => 'ecookbook', :issue_id => '567' - should_route :get, "/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22' - should_route :get, "/time_entries/report", :controller => 'timelog', :action => 'report' - should_route :get, "/projects/567/time_entries/report", :controller => 'timelog', :action => 'report', :project_id => '567' - should_route :get, "/projects/567/time_entries/report.csv", :controller => 'timelog', :action => 'report', :project_id => '567', :format => 'csv' - should_route :get, "/time_entries", :controller => 'timelog', :action => 'details' - should_route :get, "/time_entries.csv", :controller => 'timelog', :action => 'details', :format => 'csv' - should_route :get, "/time_entries.atom", :controller => 'timelog', :action => 'details', :format => 'atom' - should_route :get, "/projects/567/time_entries", :controller => 'timelog', :action => 'details', :project_id => '567' - should_route :get, "/projects/567/time_entries.csv", :controller => 'timelog', :action => 'details', :project_id => '567', :format => 'csv' - should_route :get, "/projects/567/time_entries.atom", :controller => 'timelog', :action => 'details', :project_id => '567', :format => 'atom' - should_route :get, "/issues/234/time_entries", :controller => 'timelog', :action => 'details', :issue_id => '234' - should_route :get, "/issues/234/time_entries.csv", :controller => 'timelog', :action => 'details', :issue_id => '234', :format => 'csv' - should_route :get, "/issues/234/time_entries.atom", :controller => 'timelog', :action => 'details', :issue_id => '234', :format => 'atom' - should_route :get, "/projects/ecookbook/issues/123/time_entries", :controller => 'timelog', :action => 'details', :project_id => 'ecookbook', :issue_id => '123' + should_route :get, "/time_entries", :controller => 'timelog', :action => 'index' + should_route :get, "/time_entries.csv", :controller => 'timelog', :action => 'index', :format => 'csv' + should_route :get, "/time_entries.atom", :controller => 'timelog', :action => 'index', :format => 'atom' + should_route :get, "/projects/567/time_entries", :controller => 'timelog', :action => 'index', :project_id => '567' + should_route :get, "/projects/567/time_entries.csv", :controller => 'timelog', :action => 'index', :project_id => '567', :format => 'csv' + should_route :get, "/projects/567/time_entries.atom", :controller => 'timelog', :action => 'index', :project_id => '567', :format => 'atom' + should_route :get, "/issues/234/time_entries", :controller => 'timelog', :action => 'index', :issue_id => '234' + should_route :get, "/issues/234/time_entries.csv", :controller => 'timelog', :action => 'index', :issue_id => '234', :format => 'csv' + should_route :get, "/issues/234/time_entries.atom", :controller => 'timelog', :action => 'index', :issue_id => '234', :format => 'atom' + should_route :get, "/projects/ecookbook/issues/123/time_entries", :controller => 'timelog', :action => 'index', :project_id => 'ecookbook', :issue_id => '123' + should_route :get, "/issues/567/time_entries/new", :controller => 'timelog', :action => 'new', :issue_id => '567' + should_route :get, "/projects/ecookbook/time_entries/new", :controller => 'timelog', :action => 'new', :project_id => 'ecookbook' + should_route :get, "/projects/ecookbook/issues/567/time_entries/new", :controller => 'timelog', :action => 'new', :project_id => 'ecookbook', :issue_id => '567' + should_route :get, "/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22' + + should_route :post, "/projects/ecookbook/timelog/edit", :controller => 'timelog', :action => 'create', :project_id => 'ecookbook' + should_route :post, "/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22' should_route :post, "/time_entries/55/destroy", :controller => 'timelog', :action => 'destroy', :id => '55' end + context "time_entry_reports" do + should_route :get, "/time_entries/report", :controller => 'time_entry_reports', :action => 'report' + should_route :get, "/projects/567/time_entries/report", :controller => 'time_entry_reports', :action => 'report', :project_id => '567' + should_route :get, "/projects/567/time_entries/report.csv", :controller => 'time_entry_reports', :action => 'report', :project_id => '567', :format => 'csv' + end + context "users" do should_route :get, "/users", :controller => 'users', :action => 'index' should_route :get, "/users/44", :controller => 'users', :action => 'show', :id => '44' - should_route :get, "/users/new", :controller => 'users', :action => 'add' + should_route :get, "/users/new", :controller => 'users', :action => 'new' should_route :get, "/users/444/edit", :controller => 'users', :action => 'edit', :id => '444' should_route :get, "/users/222/edit/membership", :controller => 'users', :action => 'edit', :id => '222', :tab => 'membership' - should_route :post, "/users/new", :controller => 'users', :action => 'add' - should_route :post, "/users/444/edit", :controller => 'users', :action => 'edit', :id => '444' + should_route :post, "/users", :controller => 'users', :action => 'create' should_route :post, "/users/123/memberships", :controller => 'users', :action => 'edit_membership', :id => '123' should_route :post, "/users/123/memberships/55", :controller => 'users', :action => 'edit_membership', :id => '123', :membership_id => '55' should_route :post, "/users/567/memberships/12/destroy", :controller => 'users', :action => 'destroy_membership', :id => '567', :membership_id => '12' + + should_route :put, "/users/444", :controller => 'users', :action => 'update', :id => '444' end + # TODO: should they all be scoped under /projects/:project_id ? context "versions" do should_route :get, "/projects/foo/versions/new", :controller => 'versions', :action => 'new', :project_id => 'foo' + should_route :get, "/versions/show/1", :controller => 'versions', :action => 'show', :id => '1' + should_route :get, "/versions/edit/1", :controller => 'versions', :action => 'edit', :id => '1' - should_route :post, "/projects/foo/versions/new", :controller => 'versions', :action => 'new', :project_id => 'foo' + should_route :post, "/projects/foo/versions", :controller => 'versions', :action => 'create', :project_id => 'foo' + should_route :post, "/versions/update/1", :controller => 'versions', :action => 'update', :id => '1' + + should_route :delete, "/versions/destroy/1", :controller => 'versions', :action => 'destroy', :id => '1' end context "wiki (singular, project's pages)" do diff --git a/test/object_daddy_helpers.rb b/test/object_daddy_helpers.rb index 4a2b85a9e..c94ada229 100644 --- a/test/object_daddy_helpers.rb +++ b/test/object_daddy_helpers.rb @@ -13,6 +13,11 @@ module ObjectDaddyHelpers User.spawn(attributes) end + def User.add_to_project(user, project, roles) + roles = [roles] unless roles.is_a?(Array) + Member.generate!(:principal => user, :project => project, :roles => roles) + end + # Generate the default Query def Query.generate_default!(attributes={}) query = Query.spawn(attributes) @@ -25,8 +30,9 @@ module ObjectDaddyHelpers def Issue.generate_for_project!(project, attributes={}) issue = Issue.spawn(attributes) do |issue| issue.project = project + issue.tracker = project.trackers.first unless project.trackers.empty? + yield issue if block_given? end - issue.tracker = project.trackers.first unless project.trackers.empty? issue.save! issue end diff --git a/test/test_helper.rb b/test/test_helper.rb index c934e1bc2..9a2761021 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -63,7 +63,7 @@ class ActiveSupport::TestCase end # Mock out a file - def mock_file + def self.mock_file file = 'a_file.png' file.stubs(:size).returns(32) file.stubs(:original_filename).returns('a_file.png') @@ -71,7 +71,11 @@ class ActiveSupport::TestCase file.stubs(:read).returns(false) file end - + + def mock_file + self.class.mock_file + end + # Use a temporary directory for attachment related tests def set_tmp_attachments_directory Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test") @@ -86,6 +90,12 @@ class ActiveSupport::TestCase saved_settings.each {|k, v| Setting[k] = v} end + def change_user_password(login, new_password) + user = User.first(:conditions => {:login => login}) + user.password, user.password_confirmation = new_password, new_password + user.save! + end + def self.ldap_configured? @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389) return @test_ldap.bind @@ -162,4 +172,13 @@ class ActiveSupport::TestCase end end end + + def self.should_create_a_new_user(&block) + should "create a new user" do + user = instance_eval &block + assert user + assert_kind_of User, user + assert !user.new_record? + end + end end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 90d342898..2906ec6e4 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -17,10 +17,7 @@ require File.dirname(__FILE__) + '/../../test_helper' -class ApplicationHelperTest < HelperTestCase - include ApplicationHelper - include ActionView::Helpers::TextHelper - include ActionView::Helpers::DateHelper +class ApplicationHelperTest < ActionView::TestCase fixtures :projects, :roles, :enabled_modules, :users, :repositories, :changesets, @@ -33,6 +30,35 @@ class ApplicationHelperTest < HelperTestCase def setup super end + + context "#link_to_if_authorized" do + context "authorized user" do + should "be tested" + end + + context "unauthorized user" do + should "be tested" + end + + should "allow using the :controller and :action for the target link" do + User.current = User.find_by_login('admin') + + @project = Issue.first.project # Used by helper + response = link_to_if_authorized("By controller/action", + {:controller => 'issues', :action => 'edit', :id => Issue.first.id}) + assert_match /href/, response + end + + should "allow using the url for the target link" do + User.current = User.find_by_login('admin') + + @project = Issue.first.project # Used by helper + response = link_to_if_authorized("By url", + new_issue_move_path(:id => Issue.first.id)) + assert_match /href/, response + end + + end def test_auto_links to_test = { @@ -575,7 +601,7 @@ EXPECTED # turn off avatars Setting.gravatar_enabled = '0' - assert_nil avatar(User.find_by_mail('jsmith@somenet.foo')) + assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo')) end def test_link_to_user @@ -597,4 +623,16 @@ EXPECTED t = link_to_user(user) assert_equal ::I18n.t(:label_user_anonymous), t end + + def test_link_to_project + project = Project.find(1) + assert_equal %(eCookbook), + link_to_project(project) + assert_equal %(eCookbook), + link_to_project(project, :action => 'settings') + assert_equal %(eCookbook), + link_to_project(project, {:only_path => false, :jump => 'blah'}) + assert_equal %(eCookbook), + link_to_project(project, {:action => 'settings'}, :class => "project") + end end diff --git a/test/unit/helpers/issue_moves_helper_test.rb b/test/unit/helpers/issue_moves_helper_test.rb new file mode 100644 index 000000000..b2ffb6042 --- /dev/null +++ b/test/unit/helpers/issue_moves_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class IssueMovesHelperTest < ActionView::TestCase +end diff --git a/test/unit/issue_status_test.rb b/test/unit/issue_status_test.rb index 2c0685cec..bcc50a480 100644 --- a/test/unit/issue_status_test.rb +++ b/test/unit/issue_status_test.rb @@ -32,10 +32,12 @@ class IssueStatusTest < ActiveSupport::TestCase end def test_destroy - count_before = IssueStatus.count status = IssueStatus.find(3) - assert status.destroy - assert_equal count_before - 1, IssueStatus.count + assert_difference 'IssueStatus.count', -1 do + assert status.destroy + end + assert_nil Workflow.first(:conditions => {:old_status_id => status.id}) + assert_nil Workflow.first(:conditions => {:new_status_id => status.id}) end def test_destroy_status_in_use diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index e0eb479d9..4438d854d 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -510,9 +510,50 @@ class IssueTest < ActiveSupport::TestCase assert !Issue.new(:due_date => nil).overdue? assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue? end - - def test_assignable_users - assert_kind_of User, Issue.find(1).assignable_users.first + + context "#behind_schedule?" do + should "be false if the issue has no start_date" do + assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule? + end + + should "be false if the issue has no end_date" do + assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule? + end + + should "be false if the issue has more done than it's calendar time" do + assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule? + end + + should "be true if the issue hasn't been started at all" do + assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule? + end + + should "be true if the issue has used more calendar time than it's done ratio" do + assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule? + end + end + + context "#assignable_users" do + should "be Users" do + assert_kind_of User, Issue.find(1).assignable_users.first + end + + should "include the issue author" do + project = Project.find(1) + non_project_member = User.generate! + issue = Issue.generate_for_project!(project, :author => non_project_member) + + assert issue.assignable_users.include?(non_project_member) + end + + should "not show the issue author twice" do + assignable_user_ids = Issue.find(1).assignable_users.collect(&:id) + assert_equal 2, assignable_user_ids.length + + assignable_user_ids.each do |user_id| + assert_equal 1, assignable_user_ids.count(user_id), "User #{user_id} appears more or less than once" + end + end end def test_create_should_send_email_notification @@ -522,7 +563,7 @@ class IssueTest < ActiveSupport::TestCase assert issue.save assert_equal 1, ActionMailer::Base.deliveries.size end - + def test_stale_issue_should_not_send_email_notification ActionMailer::Base.deliveries.clear issue = Issue.find(1) @@ -571,6 +612,9 @@ class IssueTest < ActiveSupport::TestCase @issue = Issue.find(1) @issue_status = IssueStatus.find(1) @issue_status.update_attribute(:default_done_ratio, 50) + @issue2 = Issue.find(2) + @issue_status2 = IssueStatus.find(2) + @issue_status2.update_attribute(:default_done_ratio, 0) end context "with Setting.issue_done_ratio using the issue_field" do @@ -580,6 +624,7 @@ class IssueTest < ActiveSupport::TestCase should "read the issue's field" do assert_equal 0, @issue.done_ratio + assert_equal 30, @issue2.done_ratio end end @@ -590,6 +635,7 @@ class IssueTest < ActiveSupport::TestCase should "read the Issue Status's default done ratio" do assert_equal 50, @issue.done_ratio + assert_equal 0, @issue2.done_ratio end end end @@ -599,6 +645,9 @@ class IssueTest < ActiveSupport::TestCase @issue = Issue.find(1) @issue_status = IssueStatus.find(1) @issue_status.update_attribute(:default_done_ratio, 50) + @issue2 = Issue.find(2) + @issue_status2 = IssueStatus.find(2) + @issue_status2.update_attribute(:default_done_ratio, 0) end context "with Setting.issue_done_ratio using the issue_field" do @@ -608,8 +657,10 @@ class IssueTest < ActiveSupport::TestCase should "not change the issue" do @issue.update_done_ratio_from_issue_status + @issue2.update_done_ratio_from_issue_status - assert_equal 0, @issue.done_ratio + assert_equal 0, @issue.read_attribute(:done_ratio) + assert_equal 30, @issue2.read_attribute(:done_ratio) end end @@ -618,10 +669,12 @@ class IssueTest < ActiveSupport::TestCase Setting.issue_done_ratio = 'issue_status' end - should "not change the issue's done ratio" do + should "change the issue's done ratio" do @issue.update_done_ratio_from_issue_status + @issue2.update_done_ratio_from_issue_status - assert_equal 50, @issue.done_ratio + assert_equal 50, @issue.read_attribute(:done_ratio) + assert_equal 0, @issue2.read_attribute(:done_ratio) end end end @@ -704,4 +757,49 @@ class IssueTest < ActiveSupport::TestCase assert issue.save assert_equal before, Issue.on_active_project.length end + + context "Issue#recipients" do + setup do + @project = Project.find(1) + @author = User.generate_with_protected! + @assignee = User.generate_with_protected! + @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author) + end + + should "include project recipients" do + assert @project.recipients.present? + @project.recipients.each do |project_recipient| + assert @issue.recipients.include?(project_recipient) + end + end + + should "include the author if the author is active" do + assert @issue.author, "No author set for Issue" + assert @issue.recipients.include?(@issue.author.mail) + end + + should "include the assigned to user if the assigned to user is active" do + assert @issue.assigned_to, "No assigned_to set for Issue" + assert @issue.recipients.include?(@issue.assigned_to.mail) + end + + should "not include users who opt out of all email" do + @author.update_attribute(:mail_notification, :none) + + assert !@issue.recipients.include?(@issue.author.mail) + end + + should "not include the issue author if they are only notified of assigned issues" do + @author.update_attribute(:mail_notification, :only_assigned) + + assert !@issue.recipients.include?(@issue.author.mail) + end + + should "not include the assigned user if they are only notified of owned issues" do + @assignee.update_attribute(:mail_notification, :only_owner) + + assert !@issue.recipients.include?(@issue.assigned_to.mail) + end + + end end diff --git a/test/unit/journal_observer_test.rb b/test/unit/journal_observer_test.rb new file mode 100644 index 000000000..1b1c1cc3f --- /dev/null +++ b/test/unit/journal_observer_test.rb @@ -0,0 +1,118 @@ +# redMine - project management software +# Copyright (C) 2006-2009 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 File.dirname(__FILE__) + '/../test_helper' + +class JournalObserverTest < ActiveSupport::TestCase + fixtures :issues, :issue_statuses, :journals, :journal_details + + def setup + ActionMailer::Base.deliveries.clear + @journal = Journal.find 1 + end + + # context: issue_updated notified_events + def test_create_should_send_email_notification_with_issue_updated + Setting.notified_events = ['issue_updated'] + issue = Issue.find(:first) + user = User.find(:first) + journal = issue.init_journal(user, issue) + + assert journal.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_should_not_send_email_notification_without_issue_updated + Setting.notified_events = [] + issue = Issue.find(:first) + user = User.find(:first) + journal = issue.init_journal(user, issue) + + assert journal.save + assert_equal 0, ActionMailer::Base.deliveries.size + end + + # context: issue_note_added notified_events + def test_create_should_send_email_notification_with_issue_note_added + Setting.notified_events = ['issue_note_added'] + issue = Issue.find(:first) + user = User.find(:first) + journal = issue.init_journal(user, issue) + journal.notes = 'This update has a note' + + assert journal.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_should_not_send_email_notification_without_issue_note_added + Setting.notified_events = [] + issue = Issue.find(:first) + user = User.find(:first) + journal = issue.init_journal(user, issue) + journal.notes = 'This update has a note' + + assert journal.save + assert_equal 0, ActionMailer::Base.deliveries.size + end + + # context: issue_status_updated notified_events + def test_create_should_send_email_notification_with_issue_status_updated + Setting.notified_events = ['issue_status_updated'] + issue = Issue.find(:first) + user = User.find(:first) + issue.init_journal(user, issue) + issue.status = IssueStatus.last + + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_should_not_send_email_notification_without_issue_status_updated + Setting.notified_events = [] + issue = Issue.find(:first) + user = User.find(:first) + issue.init_journal(user, issue) + issue.status = IssueStatus.last + + assert issue.save + assert_equal 0, ActionMailer::Base.deliveries.size + end + + # context: issue_priority_updated notified_events + def test_create_should_send_email_notification_with_issue_priority_updated + Setting.notified_events = ['issue_priority_updated'] + issue = Issue.find(:first) + user = User.find(:first) + issue.init_journal(user, issue) + issue.priority = IssuePriority.last + + assert issue.save + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def test_create_should_not_send_email_notification_without_issue_priority_updated + Setting.notified_events = [] + issue = Issue.find(:first) + user = User.find(:first) + issue.init_journal(user, issue) + issue.priority = IssuePriority.last + + assert issue.save + assert_equal 0, ActionMailer::Base.deliveries.size + end + +end diff --git a/test/unit/lib/redmine/helpers/gantt_test.rb b/test/unit/lib/redmine/helpers/gantt_test.rb new file mode 100644 index 000000000..6b97b083f --- /dev/null +++ b/test/unit/lib/redmine/helpers/gantt_test.rb @@ -0,0 +1,703 @@ +# 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. + +require File.dirname(__FILE__) + '/../../../../test_helper' + +class Redmine::Helpers::GanttTest < ActiveSupport::TestCase + # Utility methods and classes so assert_select can be used. + class GanttViewTest < ActionView::Base + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TextHelper + include ActionController::UrlWriter + include ApplicationHelper + include ProjectsHelper + include IssuesHelper + + def self.default_url_options + {:only_path => true } + end + + end + + include ActionController::Assertions::SelectorAssertions + + def setup + @response = ActionController::TestResponse.new + # Fixtures + ProjectCustomField.delete_all + Project.destroy_all + + User.current = User.find(1) + end + + def build_view + @view = GanttViewTest.new + end + + def html_document + HTML::Document.new(@response.body) + end + + # Creates a Gantt chart for a 4 week span + def create_gantt(project=Project.generate!) + @project = project + @gantt = Redmine::Helpers::Gantt.new + @gantt.project = @project + @gantt.query = Query.generate_default!(:project => @project) + @gantt.view = build_view + @gantt.instance_variable_set('@date_from', 2.weeks.ago.to_date) + @gantt.instance_variable_set('@date_to', 2.weeks.from_now.to_date) + end + + context "#number_of_rows" do + + context "with one project" do + should "return the number of rows just for that project" + end + + context "with no project" do + should "return the total number of rows for all the projects, resursively" + end + + end + + context "#number_of_rows_on_project" do + setup do + create_gantt + end + + should "clear the @query.project so cross-project issues and versions can be counted" do + assert @gantt.query.project + @gantt.number_of_rows_on_project(@project) + assert_nil @gantt.query.project + end + + should "count 1 for the project itself" do + assert_equal 1, @gantt.number_of_rows_on_project(@project) + end + + should "count the number of issues without a version" do + @project.issues << Issue.generate_for_project!(@project, :fixed_version => nil) + assert_equal 2, @gantt.number_of_rows_on_project(@project) + end + + should "count the number of versions" do + @project.versions << Version.generate! + @project.versions << Version.generate! + assert_equal 3, @gantt.number_of_rows_on_project(@project) + end + + should "count the number of issues on versions, including cross-project" do + version = Version.generate! + @project.versions << version + @project.issues << Issue.generate_for_project!(@project, :fixed_version => version) + + assert_equal 3, @gantt.number_of_rows_on_project(@project) + end + + should "recursive and count the number of rows on each subproject" do + @project.versions << Version.generate! # +1 + + @subproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1 + @subproject.set_parent!(@project) + @subproject.issues << Issue.generate_for_project!(@subproject) # +1 + @subproject.issues << Issue.generate_for_project!(@subproject) # +1 + + @subsubproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1 + @subsubproject.set_parent!(@subproject) + @subsubproject.issues << Issue.generate_for_project!(@subsubproject) # +1 + + assert_equal 7, @gantt.number_of_rows_on_project(@project) # +1 for self + end + end + + # TODO: more of an integration test + context "#subjects" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => 1.week.from_now.to_date, :sharing => 'none') + @project.versions << @version + + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => Date.yesterday, + :due_date => 1.week.from_now.to_date) + @project.issues << @issue + + @response.body = @gantt.subjects + end + + context "project" do + should "be rendered" do + assert_select "div.project-name a", /#{@project.name}/ + end + + should "have an indent of 4" do + assert_select "div.project-name[style*=left:4px]" + end + end + + context "version" do + should "be rendered" do + assert_select "div.version-name a", /#{@version.name}/ + end + + should "be indented 24 (one level)" do + assert_select "div.version-name[style*=left:24px]" + end + end + + context "issue" do + should "be rendered" do + assert_select "div.issue-subject", /#{@issue.subject}/ + end + + should "be indented 44 (two levels)" do + assert_select "div.issue-subject[style*=left:44px]" + end + end + end + + context "#lines" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => 1.week.from_now.to_date) + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => Date.yesterday, + :due_date => 1.week.from_now.to_date) + @project.issues << @issue + + @response.body = @gantt.lines + end + + context "project" do + should "be rendered" do + assert_select "div.project_todo" + assert_select "div.project-line.starting" + assert_select "div.project-line.ending" + assert_select "div.label.project-name", /#{@project.name}/ + end + end + + context "version" do + should "be rendered" do + assert_select "div.milestone_todo" + assert_select "div.milestone.starting" + assert_select "div.milestone.ending" + assert_select "div.label.version-name", /#{@version.name}/ + end + end + + context "issue" do + should "be rendered" do + assert_select "div.task_todo" + assert_select "div.label.issue-name", /#{@issue.done_ratio}/ + assert_select "div.tooltip", /#{@issue.subject}/ + end + end + end + + context "#render_project" do + should "be tested" + end + + context "#render_issues" do + should "be tested" + end + + context "#render_version" do + should "be tested" + end + + context "#subject_for_project" do + setup do + create_gantt + end + + context ":html format" do + should "add an absolute positioned div" do + @response.body = @gantt.subject_for_project(@project, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @response.body = @gantt.subject_for_project(@project, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the project name" do + @response.body = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'div', :text => /#{@project.name}/ + end + + should "include a link to the project" do + @response.body = @gantt.subject_for_project(@project, {:format => :html}) + assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/ + end + + should "style overdue projects" do + @project.enabled_module_names = [:issue_tracking] + @project.versions << Version.generate!(:effective_date => Date.yesterday) + + assert @project.overdue?, "Need an overdue project for this test" + @response.body = @gantt.subject_for_project(@project, {:format => :html}) + + assert_select 'div span.project-overdue' + end + + + end + + should "test the PNG format" + should "test the PDF format" + end + + context "#line_for_project" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => Date.yesterday) + @project.versions << @version + + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => Date.yesterday, + :due_date => 1.week.from_now.to_date) + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project_todo[style*=left:52px]" + end + + should "be the total width of the project" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project_todo[style*=width:31px]" + end + + end + + context "late line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project_late[style*=left:52px]" + end + + should "be the total delayed width of the project" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project_late[style*=width:6px]" + end + end + + context "done line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project_done[style*=left:52px]" + end + + should "Be the total done width of the project" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project_done[style*=left:52px]" + end + end + + context "starting marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_from', Date.today) + + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-line.starting", false + end + + should "appear at the starting point" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-line.starting[style*=left:52px]" + end + end + + context "ending marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) + + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-line.ending", false + + end + + should "appear at the end of the date range" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-line.ending[style*=left:84px]" + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) + + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-name", /#{@project.name}/ + end + + should "show the project name" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-name", /#{@project.name}/ + end + + should "show the percent complete" do + @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4}) + assert_select "div.project-name", /0%/ + end + end + end + + should "test the PNG format" + should "test the PDF format" + end + + context "#subject_for_version" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => Date.yesterday) + @project.versions << @version + + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#subject_for_version", + :tracker => @tracker, + :project => @project, + :start_date => Date.today) + + end + + context ":html format" do + should "add an absolute positioned div" do + @response.body = @gantt.subject_for_version(@version, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @response.body = @gantt.subject_for_version(@version, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the version name" do + @response.body = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'div', :text => /#{@version.name}/ + end + + should "include a link to the version" do + @response.body = @gantt.subject_for_version(@version, {:format => :html}) + assert_select 'a[href=?]', Regexp.escape("/versions/show/#{@version.to_param}"), :text => /#{@version.name}/ + end + + should "style late versions" do + assert @version.overdue?, "Need an overdue version for this test" + @response.body = @gantt.subject_for_version(@version, {:format => :html}) + + assert_select 'div span.version-behind-schedule' + end + + should "style behind schedule versions" do + assert @version.behind_schedule?, "Need a behind schedule version for this test" + @response.body = @gantt.subject_for_version(@version, {:format => :html}) + + assert_select 'div span.version-behind-schedule' + end + end + should "test the PNG format" + should "test the PDF format" + end + + context "#line_for_version" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => 1.week.from_now.to_date) + @project.versions << @version + + @project.issues << Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => Date.yesterday, + :due_date => 1.week.from_now.to_date) + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone_todo[style*=left:52px]" + end + + should "be the total width of the version" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone_todo[style*=width:31px]" + end + + end + + context "late line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone_late[style*=left:52px]" + end + + should "be the total delayed width of the version" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone_late[style*=width:6px]" + end + end + + context "done line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone_done[style*=left:52px]" + end + + should "Be the total done width of the version" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone_done[style*=left:52px]" + end + end + + context "starting marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_from', Date.today) + + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone.starting", false + end + + should "appear at the starting point" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone.starting[style*=left:52px]" + end + end + + context "ending marker" do + should "not appear if the starting point is off the gantt chart" do + # Shift the date range of the chart + @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) + + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone.ending", false + + end + + should "appear at the end of the date range" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.milestone.ending[style*=left:84px]" + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) + + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version-name", /#{@version.name}/ + end + + should "show the version name" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version-name", /#{@version.name}/ + end + + should "show the percent complete" do + @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4}) + assert_select "div.version-name", /30%/ + end + end + end + + should "test the PNG format" + should "test the PDF format" + end + + context "#subject_for_issue" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + + @issue = Issue.generate!(:subject => "gantt#subject_for_issue", + :tracker => @tracker, + :project => @project, + :start_date => 3.days.ago.to_date, + :due_date => Date.yesterday) + @project.issues << @issue + + end + + context ":html format" do + should "add an absolute positioned div" do + @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select "div[style*=absolute]" + end + + should "use the indent option to move the div to the right" do + @response.body = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40}) + assert_select "div[style*=left:40]" + end + + should "include the issue subject" do + @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'div', :text => /#{@issue.subject}/ + end + + should "include a link to the issue" do + @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) + assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/ + end + + should "style overdue issues" do + assert @issue.overdue?, "Need an overdue issue for this test" + @response.body = @gantt.subject_for_issue(@issue, {:format => :html}) + + assert_select 'div span.issue-overdue' + end + + end + should "test the PNG format" + should "test the PDF format" + end + + context "#line_for_issue" do + setup do + create_gantt + @project.enabled_module_names = [:issue_tracking] + @tracker = Tracker.generate! + @project.trackers << @tracker + @version = Version.generate!(:effective_date => 1.week.from_now.to_date) + @project.versions << @version + @issue = Issue.generate!(:fixed_version => @version, + :subject => "gantt#line_for_project", + :tracker => @tracker, + :project => @project, + :done_ratio => 30, + :start_date => Date.yesterday, + :due_date => 1.week.from_now.to_date) + @project.issues << @issue + end + + context ":html format" do + context "todo line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_todo[style*=left:52px]" + end + + should "be the total width of the issue" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_todo[style*=width:34px]" + end + + end + + context "late line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_late[style*=left:52px]" + end + + should "be the total delayed width of the issue" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_late[style*=width:6px]" + end + end + + context "done line" do + should "start from the starting point on the left" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=left:52px]" + end + + should "Be the total done width of the issue" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.task_done[style*=left:52px]" + end + end + + context "status content" do + should "appear at the far left, even if it's far in the past" do + @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date) + + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.issue-name" + end + + should "show the issue status" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.issue-name", /#{@issue.status.name}/ + end + + should "show the percent complete" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.issue-name", /30%/ + end + end + end + + should "have an issue tooltip" do + @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4}) + assert_select "div.tooltip", /#{@issue.subject}/ + end + + should "test the PNG format" + should "test the PDF format" + end + + context "#to_image" do + should "be tested" + end + + context "#to_pdf" do + should "be tested" + end + +end diff --git a/test/unit/lib/redmine/i18n_test.rb b/test/unit/lib/redmine/i18n_test.rb index 666bd7481..df1942e95 100644 --- a/test/unit/lib/redmine/i18n_test.rb +++ b/test/unit/lib/redmine/i18n_test.rb @@ -29,7 +29,7 @@ class Redmine::I18nTest < ActiveSupport::TestCase set_language_if_valid 'en' today = Date.today Setting.date_format = '' - assert_equal I18n.l(today), format_date(today) + assert_equal I18n.l(today, :count => today.strftime('%d')), format_date(today) end def test_date_format @@ -47,7 +47,7 @@ class Redmine::I18nTest < ActiveSupport::TestCase format_date(Date.today) format_time(Time.now) format_time(Time.now, false) - assert_not_equal 'default', ::I18n.l(Date.today, :format => :default), "date.formats.default missing in #{lang}" + assert_not_equal 'default', ::I18n.l(Date.today, :count => Date.today.strftime('%d'), :format => :default), "date.formats.default missing in #{lang}" assert_not_equal 'time', ::I18n.l(Time.now, :format => :time), "time.formats.time missing in #{lang}" end assert l('date.day_names').is_a?(Array) @@ -63,8 +63,8 @@ class Redmine::I18nTest < ActiveSupport::TestCase now = Time.now Setting.date_format = '' Setting.time_format = '' - assert_equal I18n.l(now), format_time(now) - assert_equal I18n.l(now, :format => :time), format_time(now, false) + assert_equal I18n.l(now, :count => now.strftime('%d')), format_time(now) + assert_equal I18n.l(now, :count => now.strftime('%d'), :format => :time), format_time(now, false) end def test_time_format diff --git a/test/unit/lib/redmine/menu_manager/menu_item_test.rb b/test/unit/lib/redmine/menu_manager/menu_item_test.rb index 835e154d8..ab17d4a4b 100644 --- a/test/unit/lib/redmine/menu_manager/menu_item_test.rb +++ b/test/unit/lib/redmine/menu_manager/menu_item_test.rb @@ -24,7 +24,7 @@ module RedmineMenuTestHelper end end -class Redmine::MenuManager::MenuItemTest < Test::Unit::TestCase +class Redmine::MenuManager::MenuItemTest < ActiveSupport::TestCase include RedmineMenuTestHelper Redmine::MenuManager.map :test_menu do |menu| diff --git a/test/unit/lib/redmine/menu_manager_test.rb b/test/unit/lib/redmine/menu_manager_test.rb index 0c01ca323..200ed3976 100644 --- a/test/unit/lib/redmine/menu_manager_test.rb +++ b/test/unit/lib/redmine/menu_manager_test.rb @@ -17,7 +17,7 @@ require File.dirname(__FILE__) + '/../../../test_helper' -class Redmine::MenuManagerTest < Test::Unit::TestCase +class Redmine::MenuManagerTest < ActiveSupport::TestCase context "MenuManager#map" do should "be tested" end @@ -25,8 +25,4 @@ class Redmine::MenuManagerTest < Test::Unit::TestCase context "MenuManager#items" do should "be tested" end - - should "be tested" do - assert true - end end diff --git a/test/unit/lib/redmine/notifiable_test.rb b/test/unit/lib/redmine/notifiable_test.rb new file mode 100644 index 000000000..494d16b95 --- /dev/null +++ b/test/unit/lib/redmine/notifiable_test.rb @@ -0,0 +1,31 @@ +# 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. + +require File.dirname(__FILE__) + '/../../../test_helper' + +class Redmine::NotifiableTest < ActiveSupport::TestCase + def setup + end + + def test_all + assert_equal 11, Redmine::Notifiable.all.length + + %w(issue_added issue_updated issue_note_added issue_status_updated issue_priority_updated news_added document_added file_added message_posted wiki_content_added wiki_content_updated).each do |notifiable| + assert Redmine::Notifiable.all.collect(&:name).include?(notifiable), "missing #{notifiable}" + end + end +end diff --git a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb index 2dc5d3e8e..45e3a5adb 100644 --- a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb +++ b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb @@ -13,7 +13,23 @@ class GitAdapterTest < ActiveSupport::TestCase end def test_getting_all_revisions - assert_equal 13, @adapter.revisions('',nil,nil,:all => true).length + assert_equal 15, @adapter.revisions('',nil,nil,:all => true).length + end + + def test_getting_certain_revisions + assert_equal 1, @adapter.revisions('','899a15d^','899a15d').length + end + + def test_getting_revisions_with_spaces_in_filename + assert_equal 1, @adapter.revisions("filemane with spaces.txt", nil, nil, :all => true).length + end + + def test_getting_revisions_with_leading_and_trailing_spaces_in_filename + assert_equal " filename with a leading space.txt ", @adapter.revisions(" filename with a leading space.txt ", nil, nil, :all => true)[0].paths[0][:path] + end + + def test_getting_entries_with_leading_and_trailing_spaces_in_filename + assert_equal " filename with a leading space.txt ", @adapter.entries('', '83ca5fd546063a3c7dc2e568ba3355661a9e2b2c')[3].name end def test_annotate @@ -30,6 +46,22 @@ class GitAdapterTest < ActiveSupport::TestCase assert_kind_of Redmine::Scm::Adapters::Annotate, annotate assert_equal 2, annotate.lines.size end + + def test_last_rev + last_rev = @adapter.lastrev("README", "4f26664364207fa8b1af9f8722647ab2d4ac5d43") + assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", last_rev.scmid + assert_equal "4a07fe31bffcf2888791f3e6cbc9c4545cefe3e8", last_rev.identifier + assert_equal "Adam Soltys ", last_rev.author + assert_equal "2009-06-24 05:27:38".to_time, last_rev.time + end + + def test_last_rev_with_spaces_in_filename + last_rev = @adapter.lastrev("filemane with spaces.txt", "ed5bb786bbda2dee66a2d50faf51429dbc043a7b") + assert_equal "ed5bb786bbda2dee66a2d50faf51429dbc043a7b", last_rev.scmid + assert_equal "ed5bb786bbda2dee66a2d50faf51429dbc043a7b", last_rev.identifier + assert_equal "Felix Schäfer ", last_rev.author + assert_equal "2010-09-18 19:59:46".to_time, last_rev.time + end else puts "Git test repository NOT FOUND. Skipping unit tests !!!" def test_fake; assert true end diff --git a/test/unit/lib/redmine_test.rb b/test/unit/lib/redmine_test.rb index 5150da1f2..5fdf05540 100644 --- a/test/unit/lib/redmine_test.rb +++ b/test/unit/lib/redmine_test.rb @@ -33,7 +33,7 @@ module RedmineMenuTestHelper end end -class RedmineTest < Test::Unit::TestCase +class RedmineTest < ActiveSupport::TestCase include RedmineMenuTestHelper def test_top_menu @@ -62,12 +62,14 @@ class RedmineTest < Test::Unit::TestCase end def test_project_menu - assert_number_of_items_in_menu :project_menu, 12 + assert_number_of_items_in_menu :project_menu, 14 assert_menu_contains_item_named :project_menu, :overview assert_menu_contains_item_named :project_menu, :activity assert_menu_contains_item_named :project_menu, :roadmap assert_menu_contains_item_named :project_menu, :issues assert_menu_contains_item_named :project_menu, :new_issue + assert_menu_contains_item_named :project_menu, :calendar + assert_menu_contains_item_named :project_menu, :gantt assert_menu_contains_item_named :project_menu, :news assert_menu_contains_item_named :project_menu, :documents assert_menu_contains_item_named :project_menu, :wiki diff --git a/test/unit/mail_handler_test.rb b/test/unit/mail_handler_test.rb index e0ff8a2a7..947845a58 100644 --- a/test/unit/mail_handler_test.rb +++ b/test/unit/mail_handler_test.rb @@ -41,6 +41,7 @@ class MailHandlerTest < ActiveSupport::TestCase def setup ActionMailer::Base.deliveries.clear + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) end def test_add_issue @@ -152,7 +153,7 @@ class MailHandlerTest < ActiveSupport::TestCase assert !issue.new_record? issue.reload assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo')) - assert_equal 1, issue.watchers.size + assert_equal 1, issue.watcher_user_ids.size end def test_add_issue_by_unknown_user @@ -240,6 +241,7 @@ class MailHandlerTest < ActiveSupport::TestCase end def test_add_issue_should_send_email_notification + Setting.notified_events = ['issue_added'] ActionMailer::Base.deliveries.clear # This email contains: 'Project: onlinestore' issue = submit_email('ticket_on_given_project.eml') diff --git a/test/unit/mailer_test.rb b/test/unit/mailer_test.rb index f1fc2502f..77bcb36f6 100644 --- a/test/unit/mailer_test.rb +++ b/test/unit/mailer_test.rb @@ -352,6 +352,17 @@ class MailerTest < ActiveSupport::TestCase mail = ActionMailer::Base.deliveries.last assert mail.bcc.include?('dlopper@somenet.foo') assert mail.body.include?('Bug #3: Error 281 when updating a recipe') + assert_equal '1 issue(s) due in the next 42 days', mail.subject + end + + def test_reminders_for_users + Mailer.reminders(:days => 42, :users => ['5']) + assert_equal 0, ActionMailer::Base.deliveries.size # No mail for dlopper + Mailer.reminders(:days => 42, :users => ['3']) + assert_equal 1, ActionMailer::Base.deliveries.size # No mail for dlopper + mail = ActionMailer::Base.deliveries.last + assert mail.bcc.include?('dlopper@somenet.foo') + assert mail.body.include?('Bug #3: Error 281 when updating a recipe') end def last_email diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index 7870dc2a5..9b8809c2b 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -842,4 +842,126 @@ class ProjectTest < ActiveSupport::TestCase end + context "#start_date" do + setup do + ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests + @project = Project.generate!(:identifier => 'test0') + @project.trackers << Tracker.generate! + end + + should "be nil if there are no issues on the project" do + assert_nil @project.start_date + end + + should "be nil if issue tracking is disabled" do + Issue.generate_for_project!(@project, :start_date => Date.today) + @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy} + @project.reload + + assert_nil @project.start_date + end + + should "be tested when issues have no start date" + + should "be the earliest start date of it's issues" do + early = 7.days.ago.to_date + Issue.generate_for_project!(@project, :start_date => Date.today) + Issue.generate_for_project!(@project, :start_date => early) + + assert_equal early, @project.start_date + end + + end + + context "#due_date" do + setup do + ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests + @project = Project.generate!(:identifier => 'test0') + @project.trackers << Tracker.generate! + end + + should "be nil if there are no issues on the project" do + assert_nil @project.due_date + end + + should "be nil if issue tracking is disabled" do + Issue.generate_for_project!(@project, :due_date => Date.today) + @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy} + @project.reload + + assert_nil @project.due_date + end + + should "be tested when issues have no due date" + + should "be the latest due date of it's issues" do + future = 7.days.from_now.to_date + Issue.generate_for_project!(@project, :due_date => future) + Issue.generate_for_project!(@project, :due_date => Date.today) + + assert_equal future, @project.due_date + end + + should "be the latest due date of it's versions" do + future = 7.days.from_now.to_date + @project.versions << Version.generate!(:effective_date => future) + @project.versions << Version.generate!(:effective_date => Date.today) + + + assert_equal future, @project.due_date + + end + + should "pick the latest date from it's issues and versions" do + future = 7.days.from_now.to_date + far_future = 14.days.from_now.to_date + Issue.generate_for_project!(@project, :due_date => far_future) + @project.versions << Version.generate!(:effective_date => future) + + assert_equal far_future, @project.due_date + end + + end + + context "Project#completed_percent" do + setup do + ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests + @project = Project.generate!(:identifier => 'test0') + @project.trackers << Tracker.generate! + end + + context "no versions" do + should "be 100" do + assert_equal 100, @project.completed_percent + end + end + + context "with versions" do + should "return 0 if the versions have no issues" do + Version.generate!(:project => @project) + Version.generate!(:project => @project) + + assert_equal 0, @project.completed_percent + end + + should "return 100 if the version has only closed issues" do + v1 = Version.generate!(:project => @project) + Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1) + v2 = Version.generate!(:project => @project) + Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2) + + assert_equal 100, @project.completed_percent + end + + should "return the averaged completed percent of the versions (not weighted)" do + v1 = Version.generate!(:project => @project) + Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1) + v2 = Version.generate!(:project => @project) + Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2) + + assert_equal 50, @project.completed_percent + end + + end + end end diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 26cba2a5e..db6173bae 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -33,12 +33,31 @@ class QueryTest < ActiveSupport::TestCase assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'} end + def test_project_filter_in_global_queries + query = Query.new(:project => nil, :name => '_') + project_filter = query.available_filters["project_id"] + assert_not_nil project_filter + project_ids = project_filter[:values].map{|p| p[1]} + assert project_ids.include?("1") #public project + assert !project_ids.include?("2") #private project user cannot see + end + def find_issues_with_query(query) Issue.find :all, :include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement end + def assert_find_issues_with_query_is_successful(query) + assert_nothing_raised do + find_issues_with_query(query) + end + end + + def assert_query_statement_includes(query, condition) + assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}" + end + def test_query_should_allow_shared_versions_for_a_project_query subproject_version = Version.find(4) query = Query.new(:project => Project.find(1), :name => '_') @@ -351,4 +370,155 @@ class QueryTest < ActiveSupport::TestCase assert !q.editable_by?(manager) assert !q.editable_by?(developer) end + + context "#available_filters" do + setup do + @query = Query.new(:name => "_") + end + + should "include users of visible projects in cross-project view" do + users = @query.available_filters["assigned_to_id"] + assert_not_nil users + assert users[:values].map{|u|u[1]}.include?("3") + end + + context "'member_of_group' filter" do + should "be present" do + assert @query.available_filters.keys.include?("member_of_group") + end + + should "be an optional list" do + assert_equal :list_optional, @query.available_filters["member_of_group"][:type] + end + + should "have a list of the groups as values" do + Group.destroy_all # No fixtures + group1 = Group.generate!.reload + group2 = Group.generate!.reload + + expected_group_list = [ + [group1.name, group1.id], + [group2.name, group2.id] + ] + assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort + end + + end + + context "'assigned_to_role' filter" do + should "be present" do + assert @query.available_filters.keys.include?("assigned_to_role") + end + + should "be an optional list" do + assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type] + end + + should "have a list of the Roles as values" do + assert @query.available_filters["assigned_to_role"][:values].include?(['Manager',1]) + assert @query.available_filters["assigned_to_role"][:values].include?(['Developer',2]) + assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter',3]) + end + + should "not include the built in Roles as values" do + assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member',4]) + assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous',5]) + end + + end + + end + + context "#statement" do + context "with 'member_of_group' filter" do + setup do + Group.destroy_all # No fixtures + @user_in_group = User.generate! + @second_user_in_group = User.generate! + @user_in_group2 = User.generate! + @user_not_in_group = User.generate! + + @group = Group.generate!.reload + @group.users << @user_in_group + @group.users << @second_user_in_group + + @group2 = Group.generate!.reload + @group2.users << @user_in_group2 + + end + + should "search assigned to for users in the group" do + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '=', [@group.id.to_s]) + + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search not assigned to any group member (none)" do + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '!*', ['']) + + # Users not in a group + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')" + assert_find_issues_with_query_is_successful @query + + end + + should "search assigned to any group member (all)" do + @query = Query.new(:name => '_') + @query.add_filter('member_of_group', '*', ['']) + + # Only users in a group + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')" + assert_find_issues_with_query_is_successful @query + + end + end + + context "with 'assigned_to_role' filter" do + setup do + # No fixtures + MemberRole.delete_all + Member.delete_all + Role.delete_all + + @manager_role = Role.generate!(:name => 'Manager') + @developer_role = Role.generate!(:name => 'Developer') + + @project = Project.generate! + @manager = User.generate! + @developer = User.generate! + @boss = User.generate! + User.add_to_project(@manager, @project, @manager_role) + User.add_to_project(@developer, @project, @developer_role) + User.add_to_project(@boss, @project, [@manager_role, @developer_role]) + end + + should "search assigned to for users with the Role" do + @query = Query.new(:name => '_') + @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s]) + + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search assigned to for users not assigned to any Role (none)" do + @query = Query.new(:name => '_') + @query.add_filter('assigned_to_role', '!*', ['']) + + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')" + assert_find_issues_with_query_is_successful @query + end + + should "search assigned to for users assigned to any Role (all)" do + @query = Query.new(:name => '_') + @query.add_filter('assigned_to_role', '*', ['']) + + assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')" + assert_find_issues_with_query_is_successful @query + end + end + end + end diff --git a/test/unit/repository_git_test.rb b/test/unit/repository_git_test.rb index dad5610ae..5ae889492 100644 --- a/test/unit/repository_git_test.rb +++ b/test/unit/repository_git_test.rb @@ -34,8 +34,8 @@ class RepositoryGitTest < ActiveSupport::TestCase @repository.fetch_changesets @repository.reload - assert_equal 13, @repository.changesets.count - assert_equal 22, @repository.changes.count + assert_equal 15, @repository.changesets.count + assert_equal 24, @repository.changes.count commit = @repository.changesets.find(:first, :order => 'committed_on ASC') assert_equal "Initial import.\nThe repository contains 3 files.", commit.comments @@ -57,10 +57,10 @@ class RepositoryGitTest < ActiveSupport::TestCase # Remove the 3 latest changesets @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy) @repository.reload - assert_equal 10, @repository.changesets.count + assert_equal 12, @repository.changesets.count @repository.fetch_changesets - assert_equal 13, @repository.changesets.count + assert_equal 15, @repository.changesets.count end else puts "Git test repository NOT FOUND. Skipping unit tests !!!" diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb index 6512c067a..5299dc9d9 100644 --- a/test/unit/repository_test.rb +++ b/test/unit/repository_test.rb @@ -67,6 +67,7 @@ class RepositoryTest < ActiveSupport::TestCase def test_scan_changesets_for_issue_ids Setting.default_language = 'en' + Setting.notified_events = ['issue_added','issue_updated'] # choosing a status to apply to fix issues Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id diff --git a/test/unit/time_entry_test.rb b/test/unit/time_entry_test.rb index 3c135510a..3069f9368 100644 --- a/test/unit/time_entry_test.rb +++ b/test/unit/time_entry_test.rb @@ -48,4 +48,52 @@ class TimeEntryTest < ActiveSupport::TestCase def test_hours_should_default_to_nil assert_nil TimeEntry.new.hours end + + context "#earilest_date_for_project" do + setup do + User.current = nil + @public_project = Project.generate!(:is_public => true) + @issue = Issue.generate_for_project!(@public_project) + TimeEntry.generate!(:spent_on => '2010-01-01', + :issue => @issue, + :project => @public_project) + end + + context "without a project" do + should "return the lowest spent_on value that is visible to the current user" do + assert_equal "2007-03-12", TimeEntry.earilest_date_for_project.to_s + end + end + + context "with a project" do + should "return the lowest spent_on value that is visible to the current user for that project and it's subprojects only" do + assert_equal "2010-01-01", TimeEntry.earilest_date_for_project(@public_project).to_s + end + end + + end + + context "#latest_date_for_project" do + setup do + User.current = nil + @public_project = Project.generate!(:is_public => true) + @issue = Issue.generate_for_project!(@public_project) + TimeEntry.generate!(:spent_on => '2010-01-01', + :issue => @issue, + :project => @public_project) + end + + context "without a project" do + should "return the highest spent_on value that is visible to the current user" do + assert_equal "2010-01-01", TimeEntry.latest_date_for_project.to_s + end + end + + context "with a project" do + should "return the highest spent_on value that is visible to the current user for that project and it's subprojects only" do + project = Project.find(1) + assert_equal "2007-04-22", TimeEntry.latest_date_for_project(project).to_s + end + end + end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 5a0c9f87e..3f824f9fe 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -35,6 +35,12 @@ class UserTest < ActiveSupport::TestCase def test_truth assert_kind_of User, @jsmith end + + def test_mail_should_be_stripped + u = User.new + u.mail = " foo@bar.com " + assert_equal "foo@bar.com", u.mail + end def test_create user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo") @@ -54,6 +60,18 @@ class UserTest < ActiveSupport::TestCase user.password, user.password_confirmation = "password", "password" assert user.save end + + context "User#before_create" do + should "set the mail_notification to the default Setting" do + @user1 = User.generate_with_protected! + assert_equal 'only_my_events', @user1.mail_notification + + with_settings :default_notification_option => 'all' do + @user2 = User.generate_with_protected! + assert_equal 'all', @user2.mail_notification + end + end + end context "User.login" do should "be case-insensitive." do @@ -279,7 +297,7 @@ class UserTest < ActiveSupport::TestCase end def test_mail_notification_all - @jsmith.mail_notification = true + @jsmith.mail_notification = 'all' @jsmith.notified_project_ids = [] @jsmith.save @jsmith.reload @@ -287,15 +305,15 @@ class UserTest < ActiveSupport::TestCase end def test_mail_notification_selected - @jsmith.mail_notification = false + @jsmith.mail_notification = 'selected' @jsmith.notified_project_ids = [1] @jsmith.save @jsmith.reload assert Project.find(1).recipients.include?(@jsmith.mail) end - def test_mail_notification_none - @jsmith.mail_notification = false + def test_mail_notification_only_my_events + @jsmith.mail_notification = 'only_my_events' @jsmith.notified_project_ids = [] @jsmith.save @jsmith.reload @@ -349,6 +367,132 @@ class UserTest < ActiveSupport::TestCase end + context "#allowed_to?" do + context "with a unique project" do + should "return false if project is archived" do + project = Project.find(1) + Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED) + assert ! @admin.allowed_to?(:view_issues, Project.find(1)) + end + + should "return false if related module is disabled" do + project = Project.find(1) + project.enabled_module_names = ["issue_tracking"] + assert @admin.allowed_to?(:add_issues, project) + assert ! @admin.allowed_to?(:view_wiki_pages, project) + end + + should "authorize nearly everything for admin users" do + project = Project.find(1) + assert ! @admin.member_of?(project) + %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p| + assert @admin.allowed_to?(p.to_sym, project) + end + end + + should "authorize normal users depending on their roles" do + project = Project.find(1) + assert @jsmith.allowed_to?(:delete_messages, project) #Manager + assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper + end + end + + context "with multiple projects" do + should "return false if array is empty" do + assert ! @admin.allowed_to?(:view_project, []) + end + + should "return true only if user has permission on all these projects" do + assert @admin.allowed_to?(:view_project, Project.all) + assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2) + assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere + assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers + end + + should "behave correctly with arrays of 1 project" do + assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first]) + end + end + + context "with options[:global]" do + should "authorize if user has at least one role that has this permission" do + @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere + @anonymous = User.find(6) + assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true) + assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true) + assert @dlopper2.allowed_to?(:add_issues, nil, :global => true) + assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true) + assert @anonymous.allowed_to?(:view_issues, nil, :global => true) + end + end + end + + context "User#notify_about?" do + context "Issues" do + setup do + @project = Project.find(1) + @author = User.generate_with_protected! + @assignee = User.generate_with_protected! + @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author) + end + + should "be true for a user with :all" do + @author.update_attribute(:mail_notification, :all) + assert @author.notify_about?(@issue) + end + + should "be false for a user with :none" do + @author.update_attribute(:mail_notification, :none) + assert ! @author.notify_about?(@issue) + end + + should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do + @user = User.generate_with_protected!(:mail_notification => :only_my_events) + assert ! @user.notify_about?(@issue) + end + + should "be true for a user with :only_my_events and is the author" do + @author.update_attribute(:mail_notification, :only_my_events) + assert @author.notify_about?(@issue) + end + + should "be true for a user with :only_my_events and is the assignee" do + @assignee.update_attribute(:mail_notification, :only_my_events) + assert @assignee.notify_about?(@issue) + end + + should "be true for a user with :only_assigned and is the assignee" do + @assignee.update_attribute(:mail_notification, :only_assigned) + assert @assignee.notify_about?(@issue) + end + + should "be false for a user with :only_assigned and is not the assignee" do + @author.update_attribute(:mail_notification, :only_assigned) + assert ! @author.notify_about?(@issue) + end + + should "be true for a user with :only_owner and is the author" do + @author.update_attribute(:mail_notification, :only_owner) + assert @author.notify_about?(@issue) + end + + should "be false for a user with :only_owner and is not the author" do + @assignee.update_attribute(:mail_notification, :only_owner) + assert ! @assignee.notify_about?(@issue) + end + + should "be false if the mail_notification is anything else" do + @assignee.update_attribute(:mail_notification, :somthing_else) + assert ! @assignee.notify_about?(@issue) + end + + end + + context "other events" do + should 'be added and tested' + end + end + if Object.const_defined?(:OpenID) def test_setting_identity_url diff --git a/test/unit/version_test.rb b/test/unit/version_test.rb index 1abb4a272..b30eedaea 100644 --- a/test/unit/version_test.rb +++ b/test/unit/version_test.rb @@ -104,7 +104,57 @@ class VersionTest < ActiveSupport::TestCase assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent assert_progress_equal 25.0/100.0*100, v.closed_pourcent end - + + context "#behind_schedule?" do + setup do + ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests + @project = Project.generate!(:identifier => 'test0') + @project.trackers << Tracker.generate! + + @version = Version.generate!(:project => @project, :effective_date => nil) + end + + should "be false if there are no issues assigned" do + @version.update_attribute(:effective_date, Date.yesterday) + assert_equal false, @version.behind_schedule? + end + + should "be false if there is no effective_date" do + assert_equal false, @version.behind_schedule? + end + + should "be false if all of the issues are ahead of schedule" do + @version.update_attribute(:effective_date, 7.days.from_now.to_date) + @version.fixed_issues = [ + Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left + Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left + ] + assert_equal 60, @version.completed_pourcent + assert_equal false, @version.behind_schedule? + end + + should "be true if any of the issues are behind schedule" do + @version.update_attribute(:effective_date, 7.days.from_now.to_date) + @version.fixed_issues = [ + Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left + Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left + ] + assert_equal 40, @version.completed_pourcent + assert_equal true, @version.behind_schedule? + end + + should "be false if all of the issues are complete" do + @version.update_attribute(:effective_date, 7.days.from_now.to_date) + @version.fixed_issues = [ + Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)), # 7 day span + Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span + ] + assert_equal 100, @version.completed_pourcent + assert_equal false, @version.behind_schedule? + + end + end + context "#estimated_hours" do setup do @version = Version.create!(:project_id => 1, :name => '#estimated_hours') diff --git a/vendor/plugins/engines/lib/engines/rails_extensions/asset_helpers.rb b/vendor/plugins/engines/lib/engines/rails_extensions/asset_helpers.rb index 9fd1742c4..a4a9266f2 100644 --- a/vendor/plugins/engines/lib/engines/rails_extensions/asset_helpers.rb +++ b/vendor/plugins/engines/lib/engines/rails_extensions/asset_helpers.rb @@ -109,11 +109,11 @@ module Engines::RailsExtensions::AssetHelpers # Returns the publicly-addressable relative URI for the given asset, type and plugin def self.plugin_asset_path(plugin_name, type, asset) raise "No plugin called '#{plugin_name}' - please use the full name of a loaded plugin." if Engines.plugins[plugin_name].nil? - "/#{Engines.plugins[plugin_name].public_asset_directory}/#{type}/#{asset}" + "#{ActionController::Base.relative_url_root}/#{Engines.plugins[plugin_name].public_asset_directory}/#{type}/#{asset}" end end module ::ActionView::Helpers::AssetTagHelper #:nodoc: include Engines::RailsExtensions::AssetHelpers -end \ No newline at end of file +end diff --git a/vendor/plugins/gravatar/Rakefile b/vendor/plugins/gravatar/Rakefile index 9e4854916..e67e5e7f9 100644 --- a/vendor/plugins/gravatar/Rakefile +++ b/vendor/plugins/gravatar/Rakefile @@ -6,7 +6,7 @@ task :default => :spec desc 'Run all application-specific specs' Spec::Rake::SpecTask.new(:spec) do |t| - t.rcov = true + # t.rcov = true end desc "Report code statistics (KLOCs, etc) from the application" diff --git a/vendor/plugins/gravatar/lib/gravatar.rb b/vendor/plugins/gravatar/lib/gravatar.rb index 6246645bc..9af1fed16 100644 --- a/vendor/plugins/gravatar/lib/gravatar.rb +++ b/vendor/plugins/gravatar/lib/gravatar.rb @@ -26,6 +26,9 @@ module GravatarHelper # decorational picture, the alt text should be empty according to the # XHTML specs. :alt => '', + + # The title text to use for the img tag for the gravatar. + :title => '', # The class to assign to the img tag for the gravatar. :class => 'gravatar', @@ -48,8 +51,8 @@ module GravatarHelper def gravatar(email, options={}) src = h(gravatar_url(email, options)) options = DEFAULT_OPTIONS.merge(options) - [:class, :alt, :size].each { |opt| options[opt] = h(options[opt]) } - "\"#{options[:alt]}\"" + [:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) } + "\"#{options[:alt]}\"" end # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true, @@ -82,4 +85,4 @@ module GravatarHelper end -end \ No newline at end of file +end diff --git a/vendor/plugins/gravatar/spec/gravatar_spec.rb b/vendor/plugins/gravatar/spec/gravatar_spec.rb index a11d2683a..6f78d79ad 100644 --- a/vendor/plugins/gravatar/spec/gravatar_spec.rb +++ b/vendor/plugins/gravatar/spec/gravatar_spec.rb @@ -4,34 +4,40 @@ require 'active_support' # to get "returning" require File.dirname(__FILE__) + '/../lib/gravatar' include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util -context "gravatar_url with a custom default URL" do - setup do +describe "gravatar_url with a custom default URL" do + before(:each) do @original_options = DEFAULT_OPTIONS.dup DEFAULT_OPTIONS[:default] = "no_avatar.png" @url = gravatar_url("somewhere") end - specify "should include the \"default\" argument in the result" do + it "should include the \"default\" argument in the result" do @url.should match(/&default=no_avatar.png/) end - teardown do + after(:each) do DEFAULT_OPTIONS.merge!(@original_options) end end -context "gravatar_url with default settings" do - setup do +describe "gravatar_url with default settings" do + before(:each) do @url = gravatar_url("somewhere") end - specify "should have a nil default URL" do + it "should have a nil default URL" do DEFAULT_OPTIONS[:default].should be_nil end - specify "should not include the \"default\" argument in the result" do + it "should not include the \"default\" argument in the result" do @url.should_not match(/&default=/) end -end \ No newline at end of file +end + +describe "gravatar with a custom title option" do + it "should include the title in the result" do + gravatar('example@example.com', :title => "This is a title attribute").should match(/This is a title attribute/) + end +end diff --git a/vendor/plugins/open_id_authentication/lib/open_id_authentication.rb b/vendor/plugins/open_id_authentication/lib/open_id_authentication.rb index 22481136e..70418fde7 100644 --- a/vendor/plugins/open_id_authentication/lib/open_id_authentication.rb +++ b/vendor/plugins/open_id_authentication/lib/open_id_authentication.rb @@ -89,7 +89,7 @@ module OpenIdAuthentication begin uri = URI.parse(identifier) - uri.scheme = uri.scheme.downcase # URI should do this + uri.scheme = uri.scheme.downcase if uri.scheme # URI should do this identifier = uri.normalize.to_s rescue URI::InvalidURIError raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")