diff --git redmine-original/app/controllers/issues_controller.rb suissues-patch/app/controllers/issues_controller.rb index fc02794..850c604 100644 --- redmine-original/app/controllers/issues_controller.rb +++ suissues-patch/app/controllers/issues_controller.rb @@ -18,11 +18,11 @@ class IssuesController < ApplicationController menu_item :new_issue, :only => :new - before_filter :find_issue, :only => [:show, :edit, :reply] + before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment ] before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] - before_filter :find_project, :only => [:new, :update_form, :preview] - before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] - before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] + before_filter :find_project, :only => [:new, :auto_complete_for_issue_parent, :add_subissue, :update_form, :preview ] + before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu, :auto_complete_for_issue_parent ] + before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar ] accept_key_auth :index, :changes helper :journals @@ -41,8 +41,47 @@ class IssuesController < ApplicationController include SortHelper include IssuesHelper helper :timelog + include ActionView::Helpers::PrototypeHelper include Redmine::Export::PDF + def auto_complete_for_issue_parent + @phrase = params[:issue_parent] + @candidates = [] + + # If cross project issue relations is allowed we should get + # candidates from every project + if Setting.cross_project_issue_relations? + projects_to_search = nil + else + projects_to_search = [ @project ] + @project.active_children + end + + # Try to find issue by id. + if @phrase.match(/^#?(\d+)$/) + if Setting.cross_project_issue_relations? + issue = Issue.find_by_id( $1) + else + issue = Issue.find_by_id_and_project_id( $1, projects_to_search.collect { |i| i.id}) + end + @candidates = [ issue ] if issue + end + + # If finding by id is fail, try to find by searching in subject + # and description. + if @candidates.empty? + # extract tokens from the question + # eg. hello "bye bye" => ["hello", "bye bye"] + tokens = @phrase.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')} + # tokens must be at least 3 character long + tokens = tokens.uniq.select {|w| w.length > 2 } + like_tokens = tokens.collect {|w| "%#{w.downcase}%"} + + @candidates, count = Issue.search( like_tokens, projects_to_search, :before => true) + end + + render :inline => "<%= auto_complete_result_parent_issue( @candidates, @phrase) %>" + end + def index retrieve_query sort_init 'id', 'desc' @@ -58,11 +97,18 @@ class IssuesController < ApplicationController end @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) @issue_pages = Paginator.new self, @issue_count, limit, params['page'] - @issues = Issue.find :all, :order => sort_clause, - :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], - :conditions => @query.statement, - :limit => limit, - :offset => @issue_pages.current.offset + @issues = Issue.find( :all, :order => sort_clause, + :include => [ :assigned_to, + :status, + :tracker, + :project, + :priority, + :category, + :fixed_version ], + :conditions => @query.statement, + :limit => limit, + :offset => @issue_pages.current.offset) + respond_to do |format| format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } @@ -136,7 +182,8 @@ class IssuesController < ApplicationController end @issue.status = default_status @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq - + @parent_issue = Issue.find( params[:issue_parent_issue_id]) unless params[:issue_parent_issue_id].blank? + if request.get? || request.xhr? @issue.start_date ||= Date.today else @@ -144,6 +191,7 @@ class IssuesController < ApplicationController # Check that the user is allowed to apply the requested status @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status if @issue.save + set_parent( @issue, params[:issue_parent_issue_id]) if params[:issue_parent_issue_id] attach_files(@issue, params[:attachments]) flash[:notice] = l(:notice_successful_create) Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') @@ -156,6 +204,18 @@ class IssuesController < ApplicationController @priorities = Enumeration::get_values('IPRI') render :layout => !request.xhr? end + + def add_subissue + if params[:issue_parent_issue_id].nil? + flash.now[:error] = 'No parent issue specified.' + render :nothing => true, :layout => true + return + else + redirect_to( :controller => 'issues', :action => 'new', + :issue_parent_issue_id => params[:issue_parent_issue_id]) + return + end + end # Attributes that can be updated on workflow transition (without :edit permission) # TODO: make it configurable (at least per role) @@ -186,6 +246,7 @@ class IssuesController < ApplicationController call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal}) if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save + set_parent(@issue, params[:issue_parent_issue_id]) if params[:issue_parent_issue_id] # Log spend time if current_role.allowed_to?(:log_time) @time_entry.save @@ -248,6 +309,7 @@ class IssuesController < ApplicationController call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) # Don't save any change to the issue if the user is not authorized to apply the requested status if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save + set_parent(issue, params[:issue_parent_issue_id]) unless params[:issue_parent_issue_id].blank? # Send notification for each issue (if changed) Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') else @@ -412,6 +474,7 @@ class IssuesController < ApplicationController def update_form @issue = Issue.new(params[:issue]) + @parent_issue = @issue.parent render :action => :new, :layout => false end @@ -425,6 +488,7 @@ class IssuesController < ApplicationController private def find_issue @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) + @parent_issue = @issue.parent @project = @issue.project rescue ActiveRecord::RecordNotFound render_404 @@ -481,10 +545,22 @@ private @query.add_short_filter(field, params[field]) if params[field] end end - session[:query] = {:project_id => @query.project_id, :filters => @query.filters} + if params[:view_options] and params[:view_options].is_a? Hash + params[:view_options].each_pair do |name, value| + @query.set_view_option( name, value) + end + end + session[:query] = { + :project_id => @query.project_id, + :filters => @query.filters, + :view_options => @query.view_options + } else @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] - @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) + @query ||= Query.new(:name => "_", + :project => @project, + :filters => session[:query][:filters], + :view_options => session[:query][:view_options]) @query.project = @project end end diff --git redmine-original/app/controllers/projects_controller.rb suissues-patch/app/controllers/projects_controller.rb index a75e412..8ab1d51 100644 --- redmine-original/app/controllers/projects_controller.rb +++ suissues-patch/app/controllers/projects_controller.rb @@ -46,6 +46,8 @@ class ProjectsController < ApplicationController helper :repositories include RepositoriesHelper include ProjectsHelper + helper :versions + include VersionsHelper # Lists visible projects def index @@ -293,4 +295,48 @@ private @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s } end end + + + def sort_as_tree(issues) + issues.sort!{|a,b| a.hierarchical_level <=> b.hierarchical_level} + @sorted_issues = [] + issues.each do |issue| + if @sorted_issues.empty? + @sorted_issues << issue + next + end + @time_to_stop = false #indicates when this task reaches its parent task (important because it has to stop between its parent task and the next aunt task + @sorted_issues.each do |sorted_issue| + #if same parent and smaller date, stop; if same parent, same date and smaller id, stop; after parent and before next parent, stop; + if ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date > issue.start_date)) || + ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date == issue.start_date) && (sorted_issue.id > issue.id)) || + (@time_to_stop && (sorted_issue.hierarchical_level < issue.hierarchical_level)) + @sorted_issues.insert(@sorted_issues.index(sorted_issue), issue) + break + end + @time_to_stop = true if sorted_issue == issue.parent + end + #if this issue's parent is the last element + @sorted_issues << issue if @time_to_stop + end + @sorted_issues + end + + #assumes that first level issues are ordered by date (sort_as_tree) + def integrate_versions_with_issues_tree(issues, versions) + versions.sort! {|x,y| x.start_date <=> y.start_date } + versions.each do |version| + issues << version if issues.empty? + issues.each do |issue| + if ((issue.is_a? Issue && issue.root?) || (issue.is_a? Version)) && version.start_date < issue.start_date + #insert version before a root task or another version whose date is immediately after this task's one + issues.insert(issues.index(issue), version) + elsif issue == issues.last + issues << version + end + end + end + issues + end + end diff --git redmine-original/app/controllers/queries_controller.rb suissues-patch/app/controllers/queries_controller.rb index 8500e85..7ab2b44 100644 --- redmine-original/app/controllers/queries_controller.rb +++ suissues-patch/app/controllers/queries_controller.rb @@ -30,7 +30,11 @@ class QueriesController < ApplicationController params[:fields].each do |field| @query.add_filter(field, params[:operators][field], params[:values][field]) end if params[:fields] - + + params[:view_options].each_pair do |name, value| + @query.set_view_option( name, value) + end if params[:view_options] + if request.post? && params[:confirm] && @query.save flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query @@ -45,6 +49,9 @@ class QueriesController < ApplicationController params[:fields].each do |field| @query.add_filter(field, params[:operators][field], params[:values][field]) end if params[:fields] + params[:view_options].each_pair do |name, value| + @query.set_view_option( name, value) + end if params[:view_options] @query.attributes = params[:query] @query.project = nil if params[:query_is_for_all] @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin? diff --git redmine-original/app/controllers/versions_controller.rb suissues-patch/app/controllers/versions_controller.rb index c269432..73a4b9b 100644 --- redmine-original/app/controllers/versions_controller.rb +++ suissues-patch/app/controllers/versions_controller.rb @@ -20,6 +20,10 @@ class VersionsController < ApplicationController before_filter :find_project, :authorize def show + @issues = @version.fixed_issues.find(:all, + :include => [:status, :tracker], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") + @issues = Issue.find_with_parents( @issues.collect { |i| i.id}) end def edit diff --git redmine-original/app/helpers/issues_helper.rb suissues-patch/app/helpers/issues_helper.rb index b2b85ee..293743b 100644 --- redmine-original/app/helpers/issues_helper.rb +++ suissues-patch/app/helpers/issues_helper.rb @@ -19,6 +19,12 @@ require 'csv' module IssuesHelper include ApplicationHelper + + def issue_ancestors(issue=@issue) + ancestors = "" + return "" if issue.parent == nil + ancestors += "issue-#{issue.parent.id}-child " + issue_ancestors(issue.parent) + end def render_issue_tooltip(issue) @cached_label_start_date ||= l(:field_start_date) @@ -193,4 +199,31 @@ module IssuesHelper export.rewind export end + + def set_parent(issue, parent_id) + if (issue && parent_id.to_s.size > 0) + issue.relations_from.each do |relation| + if relation.relation_type == IssueRelation::TYPE_PARENTS + relation.destroy + end + end + issue.reload + IssueRelation.create do |relation| + relation.issue_from = issue + relation.issue_to = Issue.find(parent_id) + relation.relation_type = IssueRelation::TYPE_PARENTS + unless relation.save + flash[:error] = "Can't set ##{parent_id} as parent for the issue." + end + end unless parent_id == '0' + end + end + + def auto_complete_result_parent_issue(candidates, phrase) + return "" if candidates.empty? + candidates.map! do |c| + content_tag("li", highlight( c.to_s, phrase), :id => String( c[:id])) + end + content_tag("ul", candidates.uniq) + end end diff --git redmine-original/app/helpers/queries_helper.rb suissues-patch/app/helpers/queries_helper.rb index 63d6a53..a27bca9 100644 --- redmine-original/app/helpers/queries_helper.rb +++ suissues-patch/app/helpers/queries_helper.rb @@ -1,3 +1,4 @@ +# -*- coding: mule-utf-8 -*- # redMine - project management software # Copyright (C) 2006-2007 Jean-Philippe Lang # @@ -27,7 +28,7 @@ module QueriesHelper content_tag('th', column.caption) end - def column_content(column, issue) + def column_content(column, issue, query) if column.is_a?(QueryCustomFieldColumn) cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id} show_value(cv) @@ -40,8 +41,7 @@ module QueriesHelper else case column.name when :subject - h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + - link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) + subject_in_tree(issue, value, query) when :done_ratio progress_bar(value, :width => '80px') when :fixed_version @@ -52,4 +52,47 @@ module QueriesHelper end end end + + def subject_in_tree(issue, value, query) + case query.view_options['show_parents'] + when Query::VIEW_OPTIONS_SHOW_PARENTS_NEVER + content_tag('div', subject_text(issue, value), :class=>'issue-subject') + else + content_tag('span', content_tag('div', subject_text(issue, value), :class=>'issue-subject'), :class=>"issue-subject-level-#{issue.hierarchical_level}") + end + end + + def subject_text(issue, value) + subject_text = link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) + h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + subject_text + end + + def issue_content(issue, query, options = { }) + html = "" + html << "' + html << '' + check_box_tag( "ids[]", issue.id, false, :id => nil) + '' + html << '' + link_to( issue.id, :controller => 'issues', :action => 'show', :id => issue) + '' + query.columns.each do |column| + html << content_tag( 'td', column_content(column, issue, query), :class => column.name) + end + html << "" + html + end + + def issues_family_content( parent, issues_to_show, query) + html = "" + html << issue_content( parent, query, :unfiltered => !( issues_to_show.include? parent)) + unless parent.children.empty? + parent.children.each do |child| + if issues_to_show.include?( child) || issues_to_show.detect { |i| i.parents_hierarchy.include? child } + html << issues_family_content( child, issues_to_show, query) + end + end + end + html + end + end diff --git redmine-original/app/helpers/versions_helper.rb suissues-patch/app/helpers/versions_helper.rb index 0fcc640..35edff3 100644 --- redmine-original/app/helpers/versions_helper.rb +++ suissues-patch/app/helpers/versions_helper.rb @@ -44,4 +44,31 @@ module VersionsHelper def status_by_options_for_select(value) options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value) end + + def render_list_of_related_issues( issues, version, current_level = 1) + issues_on_current_level = issues.select { |i| i.hierarchical_level == current_level } + issues -= issues_on_current_level + content_tag( 'ul') do + html = '' + issues_on_current_level.each do |issue| + opts_for_issue_li = { } + if !issue.fixed_version or issue.fixed_version != version + opts_for_issue_li[:class] = 'issue-unfiltered' + end + html << content_tag( 'li', opts_for_issue_li) do + opts = { } + if issue.done_ratio == 100 + opts[:style] = 'font-weight: bold' + end + link_to_issue(issue, opts) + ": " + h(issue.subject) + end + children_to_print = issues & issue.children + children_to_print += issues.select { |i| i.hierarchical_level >= current_level + 2} + unless children_to_print.empty? + html << render_list_of_related_issues( children_to_print, version, current_level + 1) + end + end + html + end + end end diff --git redmine-original/app/models/issue.rb suissues-patch/app/models/issue.rb index 618c559..eee2f35 100644 --- redmine-original/app/models/issue.rb +++ suissues-patch/app/models/issue.rb @@ -18,19 +18,19 @@ class Issue < ActiveRecord::Base belongs_to :project belongs_to :tracker - belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' - belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' - belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' - belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' - belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id' - belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' + belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' + belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' + belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id' + belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' has_many :journals, :as => :journalized, :dependent => :destroy has_many :time_entries, :dependent => :delete_all has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all - has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all + has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all acts_as_attachable :after_remove => :attachment_removed acts_as_customizable @@ -118,11 +118,6 @@ class Issue < ActiveRecord::Base return issue end - def priority_id=(pid) - self.priority = nil - write_attribute(:priority_id, pid) - end - def estimated_hours=(h) write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) end @@ -139,6 +134,19 @@ class Issue < ActiveRecord::Base if start_date && soonest_start && start_date < soonest_start errors.add :start_date, :activerecord_error_invalid end + + if IssueStatus.find_by_id( @attributes['status_id']).is_closed? && children.find { |i| !i.closed? } + errors.add :status, "Can't close parent issue while on of the children still is open." + end + + unless children.empty? + children_max_fixed_version = children.select { |i| i.fixed_version } .max { |a,b| a.fixed_version <=> b.fixed_version } + if @attributes['fixed_version_id'] && children_max_fixed_version + if Version.find_by_id( @attributes['fixed_version_id']) < children_max_fixed_version.fixed_version + errors.add :fixed_version, "Can't set target version of parent issue lower than any of the children." + end + end + end end def validate_on_create @@ -159,7 +167,7 @@ class Issue < ActiveRecord::Base @current_journal.details << JournalDetail.new(:property => 'attr', :prop_key => c, :old_value => @issue_before_change.send(c), - :value => send(c)) unless send(c)==@issue_before_change.send(c) + :value => send(c)) unless send(c)==@issue_before_change.send(c) || (!self.edge? && %w(status_id priority_id fixed_version_id start_date due_date done_ratio estimated_hours).include?(c)) } # custom fields changes custom_values.each {|c| @@ -172,6 +180,13 @@ class Issue < ActiveRecord::Base } @current_journal.save end + + # If target version is set, but "Due to" date is not, set it as + # the same as the date of target version. + if self.fixed_version && self.due_date.nil? + self.due_date = self.fixed_version.due_date if self.fixed_version.due_date + end + # Save the issue even if the journal is not saved (because empty) true end @@ -183,6 +198,14 @@ class Issue < ActiveRecord::Base # Update start/due dates of following issues relations_from.each(&:set_issue_to_dates) + # Set default status of parent if new status openes the issue. + relations_from.each do |relation| + if relation.relation_type == IssueRelation::TYPE_PARENTS + relation.set_issue_to_default_status + relation.set_issue_to_target_version + end + end + # Close duplicates if the issue was closed if @issue_before_change && !@issue_before_change.closed? && self.closed? duplicates.each do |duplicate| @@ -195,8 +218,9 @@ class Issue < ActiveRecord::Base duplicate.update_attribute :status, self.status end end + end - + def init_journal(user, notes = "") @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) @issue_before_change = self.clone @@ -208,6 +232,11 @@ class Issue < ActiveRecord::Base @current_journal end + def priority_id=(pid) + self.priority = nil + write_attribute(:priority_id, pid) + end + # Return true if the issue is closed, otherwise false def closed? self.status.is_closed? @@ -264,7 +293,11 @@ class Issue < ActiveRecord::Base # Returns the due date or the target due date if any # Used on gantt chart def due_before - due_date || (fixed_version ? fixed_version.effective_date : nil) + due_date || (fixed_version ? fixed_version.effective_date : 0) + end + + def duration1 + (start_date && due_date) ? (due_date - start_date + 1) : 0 end def duration @@ -275,6 +308,152 @@ class Issue < ActiveRecord::Base @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min end + def self.visible_by(usr) + with_scope(:find => { :conditions => Project.visible_by(usr) }) do + yield + end + end + + def done_ratio + if children? + @total_planned_days ||= 0 + @total_actual_days ||= 0 + children.each do |child| # from every subtask get the total number of days and the number of days already "worked" + planned_days = child.duration1 + actual_days = child.done_ratio ? (planned_days * child.done_ratio / 100).floor : 0 + @total_planned_days += planned_days + @total_actual_days += actual_days + end + @total_done_ratio = @total_planned_days != 0 ? (@total_actual_days * 100 / @total_planned_days).floor : 0 + else + read_attribute(:done_ratio) + end + end + + def estimated_hours + if children? + is_set = false + children.each do |child| + if child.estimated_hours + if is_set + @est_hours += child.estimated_hours + else + @est_hours = child.estimated_hours + is_set = true + end + end + end + @est_hours + else + read_attribute(:estimated_hours) + end + end + + def start_date + calculate 'start_date' + end + + def due_date + calculate 'due_date' + end + + def calculate(field) + if children? + @value = eval "children.first.#{field}" + children.each do |child| + case field + when 'start_date' + if child.start_date && (!@value || @value > child.start_date) + @value = child.start_date + end + when 'due_date' + if child.due_date && (!@value || @value < child.due_date) + @value = child.due_date + end + end + end + @value + else + read_attribute(eval(":#{field}")) + end + end + + def children? + children != [] + end + + def children + children = [] + relations_to.each do |relation| + if relation.relation_type == IssueRelation::TYPE_PARENTS + children << relation.other_issue(self) + end + end + children + end + + def parent + relations_from.each do |relation| + if relation.relation_type == IssueRelation::TYPE_PARENTS + return parent = relation.other_issue(self) + end + end + return nil + end + + def parents_hierarchy + return [] unless p = parent + parents = [ p ] + while p = p.parent + parents += [ p ] + end + parents + end + + def parent? + parent != nil + end + + def root? + !parent? + end + + def ancestors(issue=self) + a = [] + return a if ! issue.parent? + (a << issue.parent) | ancestors(issue.parent) + end + + #First level tasks have hierarchical level = 1 and so on + def hierarchical_level(issue=self) + issue.parent? ? (1 + hierarchical_level(issue.parent)) : 1 + end + + def edge? + relations_to.each do |relation| + if relation.relation_type == IssueRelation::TYPE_PARENTS + return false + end + end + return true + end + + def orphan? + root? && edge? + end + + def self.find_with_parents( *args) + issues = find( *args) + return [] if issues.empty? + issues.each do |i| + while not i.root? + issues += [ i.parent ] + i = i.parent + end + end + issues.uniq + end + def to_s "#{tracker} ##{id}: #{subject}" end diff --git redmine-original/app/models/issue_relation.rb suissues-patch/app/models/issue_relation.rb index 13e14cc..9a34ded 100644 --- redmine-original/app/models/issue_relation.rb +++ suissues-patch/app/models/issue_relation.rb @@ -17,23 +17,31 @@ class IssueRelation < ActiveRecord::Base belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' - belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' + belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' TYPE_RELATES = "relates" TYPE_DUPLICATES = "duplicates" TYPE_BLOCKS = "blocks" TYPE_PRECEDES = "precedes" + TYPE_PARENTS = "parents" - TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, - TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, - TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, - TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, + TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, + TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, + TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 }, + TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 }, + TYPE_PARENTS => { :name => :label_parents, :sym_name => :label_children, :order => 5 }, }.freeze - validates_presence_of :issue_from, :issue_to, :relation_type - validates_inclusion_of :relation_type, :in => TYPES.keys - validates_numericality_of :delay, :allow_nil => true - validates_uniqueness_of :issue_to_id, :scope => :issue_from_id + validates_presence_of :issue_from, :issue_to, :relation_type + validates_inclusion_of :relation_type, :in => TYPES.keys + validates_numericality_of :delay, :allow_nil => true + validates_uniqueness_of :issue_to_id, :scope => :issue_from_id + + # Only one parent allowed. + validates_uniqueness_of( :relation_type, + :scope => [:issue_from_id, :relation_type], + :message => l(:error_issue_can_have_only_one_parent), + :if => Proc.new { |issue_relation| issue_relation.relation_type == TYPE_PARENTS }) attr_protected :issue_from_id, :issue_to_id @@ -59,8 +67,30 @@ class IssueRelation < ActiveRecord::Base else self.delay = nil end + + # Set new status to parent if new status openes the issue. + if TYPE_PARENTS == relation_type + set_issue_to_default_status + set_issue_to_target_version + end + set_issue_to_dates end + + def set_issue_to_default_status + if issue_to.closed? && !issue_from.closed? + issue_to.update_attribute :status, IssueStatus.default + end + end + + def set_issue_to_target_version + if issue_to.fixed_version.nil? && issue_from.fixed_version or + ( issue_to.fixed_version && issue_from.fixed_version and + issue_to.fixed_version.project == issue_from.fixed_version.project and + issue_to.fixed_version < issue_from.fixed_version ) + issue_to.update_attribute :fixed_version, issue_from.fixed_version + end + end def set_issue_to_dates soonest_start = self.successor_soonest_start diff --git redmine-original/app/models/query.rb suissues-patch/app/models/query.rb index 4689f5f..149271b 100644 --- redmine-original/app/models/query.rb +++ suissues-patch/app/models/query.rb @@ -48,11 +48,27 @@ class QueryCustomFieldColumn < QueryColumn end end +class ViewOption + attr_accessor :name, :available_values + include GLoc + + def initialize( name, available_values) + self.name = name + self.available_values = available_values + end + + def caption + set_language_if_valid( User.current.language) + l("label_view_option_#{name}") + end +end + class Query < ActiveRecord::Base belongs_to :project belongs_to :user serialize :filters serialize :column_names + serialize :view_options attr_protected :project_id, :user_id @@ -109,10 +125,22 @@ class Query < ActiveRecord::Base QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), ] cattr_reader :available_columns + + VIEW_OPTIONS_SHOW_PARENTS_NEVER = 'do_not_show' + VIEW_OPTIONS_SHOW_PARENTS_ALWAYS = 'show_always' + VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT = 'organize_by_parent' + + @@available_view_options = [ + ViewOption.new( 'show_parents', [ [ l(:label_view_option_parents_do_not_show), VIEW_OPTIONS_SHOW_PARENTS_NEVER ], + [ l(:label_view_option_parents_show_always), VIEW_OPTIONS_SHOW_PARENTS_ALWAYS ], + [ l(:label_view_option_parents_show_and_group), VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT ] ]) + ] + cattr_reader :available_view_options def initialize(attributes = nil) super attributes self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } + self.view_options ||= { 'show_parents' => 'do_not_show' } set_language_if_valid(User.current.language) end @@ -317,7 +345,19 @@ class Query < ActiveRecord::Base (filters_clauses << project_statement).join(' AND ') end - + + def set_view_option( option, value) + self.view_options[option] = value + end + + def values_for_view_option( option) + @@available_view_options.find { |vo| vo.name == option }.available_values + end + + def caption_for_view_option( option) + @@available_view_options.find { |vo| vo.name == option }.caption + end + private # Helper method to generate the WHERE sql for a +field+ with a +value+ diff --git redmine-original/app/models/version.rb suissues-patch/app/models/version.rb index 7f96cea..3f3a8f2 100644 --- redmine-original/app/models/version.rb +++ suissues-patch/app/models/version.rb @@ -27,6 +27,8 @@ class Version < ActiveRecord::Base validates_length_of :name, :maximum => 60 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true + include Comparable + def start_date effective_date end diff --git redmine-original/app/views/issues/_edit.rhtml suissues-patch/app/views/issues/_edit.rhtml index 413f217..6c7423e 100644 --- redmine-original/app/views/issues/_edit.rhtml +++ suissues-patch/app/views/issues/_edit.rhtml @@ -15,7 +15,7 @@ <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %> <% end %> - <% if authorize_for('timelog', 'edit') %> + <% if authorize_for('timelog', 'edit') && @issue.edge? %>
<%= l(:button_log_time) %> <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %>
diff --git redmine-original/app/views/issues/_form.rhtml suissues-patch/app/views/issues/_form.rhtml index fac2d6a..1e71755 100644 --- redmine-original/app/views/issues/_form.rhtml +++ suissues-patch/app/views/issues/_form.rhtml @@ -1,3 +1,11 @@ + + <% if @issue.new_record? %>

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

<%= observe_field :issue_tracker_id, :url => { :action => :new }, @@ -17,13 +25,14 @@
-<% if @issue.new_record? || @allowed_statuses.any? %> +<% if (@issue.new_record? || @allowed_statuses.any?) %>

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

<% else %>

<%= @issue.status.name %>

<% end %>

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

+

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

<% unless @project.issue_categories.empty? %>

<%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> @@ -38,10 +47,30 @@

+<% if @issue.edge? %>

<%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %>

<%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %>

<%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %>

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

+<% else %> +

<%= format_date(@issue.start_date) %>

+

<%= format_date(@issue.due_date) %>

+

<%= "#{@issue.done_ratio}%" %>

+<% end %> +<%= content_tag( :input, {}, + :id => :issue_parent_issue_id, + :type => :hidden, + :name => 'issue_parent_issue_id', :value => @parent_issue ? @parent_issue.id : "") %> +

+<% if authorize_for( 'issues', 'add_subissue') %> + <%= text_field_with_auto_complete( :issue, :parent, + { :name => 'issue_parent', :value => @parent_issue || "" }, + :url => { :action => 'auto_complete_for_issue_parent', :project_id => @project}, + :after_update_element => 'setParentIssueValue') %> +<% else %> + <%= @parent_issue || "-" %> +<% end %> +

diff --git redmine-original/app/views/issues/_form_update.rhtml suissues-patch/app/views/issues/_form_update.rhtml index 3f17a03..c460913 100644 --- redmine-original/app/views/issues/_form_update.rhtml +++ suissues-patch/app/views/issues/_form_update.rhtml @@ -4,7 +4,11 @@

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

+<% if @issue.edge? %>

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

+<% else %> +

<%= "#{@issue.done_ratio}%" %>

+<% end %> <%= content_tag('p', f.select(:fixed_version_id, (@project.versions.sort.collect {|v| [v.name, v.id]}), { :include_blank => true })) unless @project.versions.empty? %> diff --git redmine-original/app/views/issues/_list.rhtml suissues-patch/app/views/issues/_list.rhtml index 9326760..4d8300b 100644 --- redmine-original/app/views/issues/_list.rhtml +++ suissues-patch/app/views/issues/_list.rhtml @@ -1,22 +1,40 @@ -<% form_tag({}) do -%> - +<% form_tag({}) do -%> +
- <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> + <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> <% query.columns.each do |column| %> <%= column_header(column) %> <% end %> - - + + + <% if query.view_options['show_parents'] == Query::VIEW_OPTIONS_SHOW_PARENTS_NEVER -%> <% issues.each do |issue| -%> - - - - <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %> - + <%= issue_content( issue, query) %> <% end -%> - -
<%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', - :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
<%= check_box_tag("ids[]", issue.id, false, :id => nil) %><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %>
+ <% elsif query.view_options['show_parents'] == Query::VIEW_OPTIONS_SHOW_PARENTS_ALWAYS -%> + <% issues.each do |issue| -%> + <% issue.parents_hierarchy.reverse.each do |parent_issue| -%> + <%= issue_content( parent_issue, query, :unfiltered => true) %> + <% end -%> + <%= issue_content( issue, query) %> + <% end -%> + <% elsif query.view_options['show_parents'] == Query::VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT -%> + <% parents_on_first_lvl = [] + issues.each do |i| + if i.parent + first_parent = i.parents_hierarchy.last + else + first_parent = i + end + parents_on_first_lvl += [ first_parent ] unless parents_on_first_lvl.include?( first_parent) + end -%> + <% parents_on_first_lvl.each do |parent| -%> + <%= issues_family_content( parent, issues, query) %> + <% end -%> + <% end -%> + + <% end -%> diff --git redmine-original/app/views/issues/context_menu.rhtml suissues-patch/app/views/issues/context_menu.rhtml index 6856568..2c12b43 100644 --- redmine-original/app/views/issues/context_menu.rhtml +++ suissues-patch/app/views/issues/context_menu.rhtml @@ -15,7 +15,6 @@
  • <%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)}, :class => 'icon-edit', :disabled => !@can[:edit] %>
  • <% end %> -
  • <%= l(:field_priority) %>
      @@ -64,6 +63,7 @@
  • <% end -%> + <% if @issue && @issue.edge? %>
  • <%= l(:field_done_ratio) %>
      @@ -73,7 +73,7 @@ <% end -%>
  • - + <% end %> <% if !@issue.nil? %>
  • <%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon-copy', :disabled => !@can[:copy] %>
  • diff --git redmine-original/app/views/issues/index.rhtml suissues-patch/app/views/issues/index.rhtml index 64592a5..74e0d58 100644 --- redmine-original/app/views/issues/index.rhtml +++ suissues-patch/app/views/issues/index.rhtml @@ -4,8 +4,12 @@ <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %> <%= hidden_field_tag('project_id', @project.id) if @project %> +
    <%= l(:label_view) %> + <%= render :partial => 'queries/view_options', :locals => {:query => @query } %> +
    <%= l(:label_filter_plural) %> - <%= render :partial => 'queries/filters', :locals => {:query => @query} %> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +

    <%= link_to_remote l(:button_apply), { :url => { :set_filter => 1 }, @@ -23,7 +27,6 @@ <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %> <% end %>

    -
    <% end %> <% else %>
    diff --git redmine-original/app/views/issues/show.rhtml suissues-patch/app/views/issues/show.rhtml index 8742ce6..e87c02d 100644 --- redmine-original/app/views/issues/show.rhtml +++ suissues-patch/app/views/issues/show.rhtml @@ -1,4 +1,8 @@
    +<%= link_to_if_authorized(l(:button_add_subissue), + { :controller => 'issues', :action => 'add_subissue', + :project_id => @project.id, :issue_parent_issue_id => @issue.id }, + :class => 'icon icon-add') %> <%= 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' %> <%= watcher_tag(@issue, User.current) %> @@ -51,8 +55,8 @@ if (n > 1) n = 0 %> - <%end -end %> + <% end %> +<% end %> <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> diff --git redmine-original/app/views/projects/roadmap.rhtml suissues-patch/app/views/projects/roadmap.rhtml index 0778d81..cd95e4a 100644 --- redmine-original/app/views/projects/roadmap.rhtml +++ suissues-patch/app/views/projects/roadmap.rhtml @@ -10,19 +10,19 @@ <%= render :partial => 'versions/overview', :locals => {:version => version} %> <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %> - <% issues = version.fixed_issues.find(:all, - :include => [:status, :tracker], - :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], - :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty? + <% + unless @selected_tracker_ids.empty? + issues = version.fixed_issues.find(:all, + :include => [:status, :tracker], + :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"], + :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") + issues = Issue.find_with_parents( issues.collect { |i| i.id }) + end issues ||= [] %> <% if issues.size > 0 %> <% end %> <% end %> diff --git redmine-original/app/views/queries/_form.rhtml suissues-patch/app/views/queries/_form.rhtml index 8da2640..1c4104b 100644 --- redmine-original/app/views/queries/_form.rhtml +++ suissues-patch/app/views/queries/_form.rhtml @@ -21,6 +21,10 @@ :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %>

    +
    <%= l(:label_view) %> + <%= render :partial => 'queries/view_options', :locals => {:query => query } %> +
    +
    <%= l(:label_filter_plural) %> <%= render :partial => 'queries/filters', :locals => {:query => query}%>
    diff --git redmine-original/app/views/queries/_view_options.rhtml suissues-patch/app/views/queries/_view_options.rhtml new file mode 100644 index 0000000..dea7db8 --- /dev/null +++ suissues-patch/app/views/queries/_view_options.rhtml @@ -0,0 +1,6 @@ +<% query.view_options.each_key do |voption| -%> + <%= query.caption_for_view_option( voption) %>: + <%= select_tag( "view_options[#{voption}]", + options_for_select( query.values_for_view_option( voption), + query.view_options[voption])) %> +<% end %> diff --git redmine-original/app/views/versions/show.rhtml suissues-patch/app/views/versions/show.rhtml index 7e52645..ef91b89 100644 --- redmine-original/app/views/versions/show.rhtml +++ suissues-patch/app/views/versions/show.rhtml @@ -31,16 +31,9 @@ <%= 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], - :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %> -<% if issues.size > 0 %> +<% if @issues.size > 0 %> <% end %>
    diff --git redmine-original/db/migrate/20090115162651_add_queries_view_options.rb suissues-patch/db/migrate/20090115162651_add_queries_view_options.rb new file mode 100644 index 0000000..cfd0377 --- /dev/null +++ suissues-patch/db/migrate/20090115162651_add_queries_view_options.rb @@ -0,0 +1,9 @@ +class AddQueriesViewOptions < ActiveRecord::Migration + def self.up + add_column :queries, :view_options, :text + end + + def self.down + remove_column :queries, :view_options + end +end diff --git redmine-original/db/migrate/20090121172432_add_default_value_of_view_option_queries.rb suissues-patch/db/migrate/20090121172432_add_default_value_of_view_option_queries.rb new file mode 100644 index 0000000..8c40200 --- /dev/null +++ suissues-patch/db/migrate/20090121172432_add_default_value_of_view_option_queries.rb @@ -0,0 +1,11 @@ +class AddDefaultValueOfViewOptionQueries < ActiveRecord::Migration + def self.up + Query.find(:all).each do |q| + q.view_options ||= { 'show_parents' => 'do_not_show' } + q.save! + end + end + + def self.down + end +end diff --git redmine-original/lang/en.yml suissues-patch/lang/en.yml index 246bfb1..973a75c 100644 --- redmine-original/lang/en.yml +++ suissues-patch/lang/en.yml @@ -84,6 +84,7 @@ error_scm_not_found: "The entry or revision was not found in the repository." error_scm_command_failed: "An error occurred when trying to access the repository: %s" error_scm_annotate: "The entry does not exist or can not be annotated." error_issue_not_found_in_project: 'The issue was not found or does not belong to this project' +error_issue_can_have_only_one_parent: allows only one relation (a task can have only one parent). warning_attachments_not_saved: "%d file(s) could not be saved." @@ -187,6 +188,10 @@ field_default_value: Default value field_comments_sorting: Display comments field_parent_title: Parent page field_editable: Editable +field_calendar_firstday: First day of week +field_parent: Subproject of +field_parent_issue: Child of +field_parent_title: Parent page setting_app_title: Application title setting_app_subtitle: Application subtitle @@ -587,6 +592,12 @@ label_generate_key: Generate a key label_issue_watchers: Watchers label_example: Example label_display: Display +label_children: parent of +label_parents: child of +label_view_option_parents_do_not_show: Never +label_view_option_parents_show_always: Always +label_view_option_parents_show_and_group: Organize by parent +label_view_option_show_parents: Show parents button_login: Login button_submit: Submit @@ -627,6 +638,7 @@ button_annotate: Annotate button_update: Update button_configure: Configure button_quote: Quote +button_add_subissue: Add sub-issue status_active: active status_registered: registered diff --git redmine-original/lib/redmine.rb suissues-patch/lib/redmine.rb index c8d64b8..957149b 100644 --- redmine-original/lib/redmine.rb +++ suissues-patch/lib/redmine.rb @@ -35,8 +35,8 @@ Redmine::AccessControl.map do |map| :queries => :index, :reports => :issue_report}, :public => true map.permission :add_issues, {:issues => :new} - map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]} - map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]} + map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :update_subject]} + map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy], :issues => :add_subissue} map.permission :add_issue_notes, {:issues => [:edit, :reply]} map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin diff --git redmine-original/lib/redmine/version.rb suissues-patch/lib/redmine/version.rb index f51d174..41bdd68 100644 --- redmine-original/lib/redmine/version.rb +++ suissues-patch/lib/redmine/version.rb @@ -23,19 +23,30 @@ module Redmine if entries.match(%r{^\d+}) revision = $1.to_i if entries.match(%r{^\d+\s+dir\s+(\d+)\s}) else - xml = REXML::Document.new(entries) - revision = xml.elements['wc-entries'].elements[1].attributes['revision'].to_i - end - rescue - # Could not find the current revision - end - end - revision + xml = REXML::Document.new(entries) + revision = xml.elements['wc-entries'].elements[1].attributes['revision'].to_i + end + rescue + # Could not find the current revision + end + end + revision + end + + def self.warecorp_revision + begin + tag = %x{ git name-rev --tags `git log -1 --pretty=format:'%H'` }.split[1] + tag = ( tag.match %r{tags/(.*)})[1] + rescue + tag = "undefined" + end + tag end REVISION = self.revision + WARECORP_REVISION = self.warecorp_revision ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact - STRING = ARRAY.join('.') + STRING = ARRAY.join('.') + " / #{WARECORP_REVISION}" def self.to_a; ARRAY end def self.to_s; STRING end diff --git redmine-original/public/stylesheets/application.css suissues-patch/public/stylesheets/application.css index 02c4a8b..cd15476 100644 --- redmine-original/public/stylesheets/application.css +++ suissues-patch/public/stylesheets/application.css @@ -704,3 +704,59 @@ h2 img { vertical-align:middle; } #main { background: #fff; } #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} } + +/***** Subtasks *****/ +/*blocks composed of icon (+, - or none), project identification (if there is one) and link to the task*/ +.issue-subject-level-1, .issue-subject-level-1 .issue-subject { + margin-left: 0em; +} + +.issue-subject-level-2, .issue-subject-level-2 .issue-subject { + margin-left: 1em; +} + +.issue-subject-level-3, .issue-subject-level-3 .issue-subject { + margin-left: 2em; +} + +.issue-subject-level-4, .issue-subject-level-4 .issue-subject { + margin-left: 3em; +} + +.issue-subject-level-5, .issue-subject-level-5 .issue-subject { + margin-left: 4em; +} + +.issue-subject-level-2, .issue-subject-level-2 .issue-subject, .issue-subject-level-3, .issue-subject-level-3 .issue-subject, .issue-subject-level-4, .issue-subject-level-4 .issue-subject, .issue-subject-level-5, .issue-subject-level-5 .issue-subject { + background-image: url(../images/corner-dots.gif); + background-repeat: no-repeat; + background-position: -3px center; +} + +/* Used to show issues which is not pass by filter, but should by + shown as parent for other issue. */ +.issue-unfiltered { + opacity: 0.5; + filter: alpha(opacity=50); +} + +.expanded-issue { + background-image: url(contract.png); + background-repeat: no-repeat; +} + +.contracted-issue { + background-image: url(expand.png); + background-repeat: no-repeat; +} + +.expand-icon, .contract-icon{ +/* position: absolute; */ + vertical-align: middle; +} + +/*text after the icon, which needs to be indented so that all its lines stay completely after the icon*/ +.issue-subject{ + padding-left: 1em; +} + diff --git redmine-original/test/fixtures/queries.yml suissues-patch/test/fixtures/queries.yml index f120227..228f6f7 100644 --- redmine-original/test/fixtures/queries.yml +++ suissues-patch/test/fixtures/queries.yml @@ -19,6 +19,10 @@ queries_001: - "125" :operator: "=" + view_options: | + --- + show_parents: "do_not_show" + user_id: 1 column_names: queries_002: @@ -37,6 +41,10 @@ queries_002: - "1" :operator: o + view_options: | + --- + show_parents: "do_not_show" + user_id: 3 column_names: queries_003: @@ -51,6 +59,10 @@ queries_003: - "3" :operator: "=" + view_options: | + --- + show_parents: "do_not_show" + user_id: 3 column_names: queries_004: @@ -65,5 +77,9 @@ queries_004: - "3" :operator: "=" + view_options: | + --- + show_parents: "do_not_show" + user_id: 2 column_names: diff --git redmine-original/test/fixtures/versions.yml suissues-patch/test/fixtures/versions.yml index 62c5e6f..cad63a6 100644 --- redmine-original/test/fixtures/versions.yml +++ suissues-patch/test/fixtures/versions.yml @@ -23,4 +23,12 @@ versions_003: id: 3 description: Future version effective_date: +onlinestore_1_0: + created_on: 2006-07-19 21:00:33 +02:00 + name: "1.0" + project_id: 1 + updated_on: 2006-07-19 21:00:33 +02:00 + id: 4 + description: Future version + effective_date: \ No newline at end of file diff --git redmine-original/test/functional/issues_controller_test.rb suissues-patch/test/functional/issues_controller_test.rb index cc1c774..7f4217d 100644 --- redmine-original/test/functional/issues_controller_test.rb +++ suissues-patch/test/functional/issues_controller_test.rb @@ -22,10 +22,12 @@ require 'issues_controller' class IssuesController; def rescue_action(e) raise e end; end class IssuesControllerTest < Test::Unit::TestCase + fixtures :projects, :users, :roles, :members, + :queries, :issues, :issue_statuses, :versions, @@ -960,4 +962,23 @@ class IssuesControllerTest < Test::Unit::TestCase assert_equal 2, TimeEntry.find(1).issue_id assert_equal 2, TimeEntry.find(2).issue_id end + + def test_new_child_issue + child_issue_subject = 'This is the test_new child issue' + parent_issue = Issue.find(1) + @request.session[:user_id] = 2 + post :new, :project_id => 1, + :issue_parent_issue_id => parent_issue.id, + :issue => {:tracker_id => 3, + :subject => 'This is the test_new child issue', + :description => 'This is the description', + :priority_id => 5, + :estimated_hours => '', + :custom_field_values => {'2' => 'Value for field 2'} } + + assert_redirected_to 'issues/show' + follow_redirect + assert_tag :tag => 'p', :content => "Related issues" + assert_tag :tag => 'a', :content => "#{parent_issue.tracker.name} ##{parent_issue.id}" + end end diff --git redmine-original/test/unit/issue_test.rb suissues-patch/test/unit/issue_test.rb index 89ec3c6..44a6277 100644 --- redmine-original/test/unit/issue_test.rb +++ suissues-patch/test/unit/issue_test.rb @@ -22,7 +22,7 @@ class IssueTest < Test::Unit::TestCase :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, - :issues, + :issues, :versions, :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values, :time_entries @@ -220,7 +220,162 @@ class IssueTest < Test::Unit::TestCase assert_nil Issue.find_by_id(1) assert_nil TimeEntry.find_by_issue_id(1) end + + def test_should_update_target_version_of_parent_issue + create_family_of_issues 'Target version of parent updates test' + + version_0_1 = Version.find( versions( :versions_001).id) + version_1_0 = Version.find( versions( :versions_002).id) + + # set target version for child + @issue2.fixed_version = version_0_1 + assert @issue2.save + assert @issue1.reload.fixed_version == @issue2.fixed_version + + # set target version for child of child larger than target version + # of child. so, target version of parents should be updated. + @issue3.fixed_version = version_1_0 + assert @issue3.save + assert @issue1.reload.fixed_version == @issue3.fixed_version + end + + def test_should_not_allowed_close_parent_issue_while_one_of_children_open + create_family_of_issues 'Closing parent issue when some children is open test.' + + closed_status = issue_statuses( :issue_statuses_005) + @issue3.status = closed_status + assert @issue3.save + + assert_raise ActiveRecord::RecordInvalid do + @issue1.reload.status = closed_status + @issue1.save! + end + + end + + def test_should_change_status_of_parent_when_some_children_is_open + create_family_of_issues 'Changing status of parent from closed to open when some of children is open.' + + open_status = issue_statuses( :issue_statuses_001) + closed_status = issue_statuses( :issue_statuses_005) + + @issue3.status = closed_status + @issue2.status = closed_status + @issue1.status = closed_status + assert @issue3.save + assert @issue2.save + assert @issue1.save + assert @issue1.reload.closed? + assert @issue2.reload.closed? + assert @issue3.reload.closed? + + # set status of children to open status. this should update status + # of parent and set it to open state. + @issue2.status = open_status + assert @issue2.save + assert !@issue2.reload.closed? + assert !@issue1.reload.closed? + end + + def test_should_update_targetversion_of_parent_if_children_have_bigger_targetversion + create_family_of_issues 'Update target version of parent if children have bigger target version.' + + # set parent version to 1. + @issue1.fixed_version = versions( :versions_001) + assert @issue1.save + assert @issue1.reload.fixed_version == versions( :versions_001) + + # set children to version higher that parent. + @issue2.fixed_version = versions( :versions_002) + assert @issue2.save + assert @issue1.reload.fixed_version == versions( :versions_002) + end + + def test_should_set_target_version_of_parent_if_children_have_a_target_version + create_family_of_issues 'Update target version of parent if children have a target version.' + + @issue2.fixed_version = versions( :versions_001) + assert @issue2.save + assert @issue2.reload.fixed_version == versions( :versions_001) + assert @issue1.reload.fixed_version == @issue2.fixed_version + end + + def test_should_not_allow_to_set_targetversion_of_parent_lower_than_any_of_the_children + create_family_of_issues 'Not allowing to set target version of parent lower than any of the children.' + + [ @issue1, @issue2, @issue3 ].each do |issue| + issue.update_attribute :fixed_version, versions( :versions_002) + end + + assert_raise ActiveRecord::RecordInvalid do + @issue1.fixed_version = versions( :versions_001) + @issue1.save! + end + end + + def test_should_not_set_target_version_of_parent_if_child_on_another_project + create_family_of_issues 'Should not set target version of parnet if child on another project.' + + @issue2.fixed_version = versions( :versions_001) + assert @issue2.save + assert @issue1.reload.fixed_version = versions( :versions_001) + + online_store = projects( :projects_002) + assert @issue2.move_to( online_store) + @issue2.reload.fixed_version = versions( :onlinestore_1_0) + assert @issue2.save + assert @issue1.reload.fixed_version = versions( :versions_001) + assert @issue2.reload.fixed_version = versions( :onlinestore_1_0) + end + + def test_should_update_due_to_date_if_target_version_is_set_but_due_to_is_not + @issue = Issue.new( :project_id => 1, :tracker_id => 1, + :author_id => 1, :status_id => 1, + :priority => Enumeration.get_values('IPRI').first, + :subject => 'issue for test hook which set due_to when sets target version.', + :description => 'issue for test hook which set due_to when sets target version.') + + assert @issue.save! + assert @issue.reload.due_date == nil + @issue.fixed_version = versions( :versions_001) + assert @issue.save! + assert @issue.reload.due_date == @issue.reload.fixed_version.effective_date + end + private + + def create_family_of_issues( subject) + # Create 3 issues + @issue1 = Issue.new( :project_id => 1, :tracker_id => 1, + :author_id => 1, :status_id => 1, + :priority => Enumeration.get_values('IPRI').first, + :subject => subject, + :description => subject) + assert @issue1.save + @issue2 = @issue1.clone + assert @issue2.save + @issue3 = @issue1.clone + assert @issue3.save + + # 2 is a child of 1 + IssueRelation.create( :issue_from => @issue2, :issue_to => @issue1, + :relation_type => IssueRelation::TYPE_PARENTS) + # And 3 is a child of 2 + IssueRelation.create( :issue_from => @issue3, :issue_to => @issue2, + :relation_type => IssueRelation::TYPE_PARENTS) + + assert @issue2.reload.parent == @issue1 + assert @issue2.reload.children.include?( @issue3) + + assert @issue1.save + assert @issue2.save + assert @issue3.save + + @issue1.reload + @issue2.reload + @issue3.reload + end + def test_overdue assert Issue.new(:due_date => 1.day.ago.to_date).overdue? assert !Issue.new(:due_date => Date.today).overdue? @@ -228,4 +383,5 @@ class IssueTest < Test::Unit::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 + end