subissues-v1.8.diff

Subissues v1.8 (using awesome_nested_set). - Aleksei Gusev, 2009-04-08 11:06

Download (103 KB)

View differences:

app/controllers/issues_controller.rb
18 18
class IssuesController < ApplicationController
19 19
  menu_item :new_issue, :only => :new
20 20
  
21
  before_filter :find_issue, :only => [:show, :edit, :reply]
21
  before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment ]
22 22
  before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23
  before_filter :find_project, :only => [:new, :update_form, :preview]
24
  before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25
  before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
23
  before_filter :find_parent_issue, :only => [:add_subissue]
24
  before_filter :find_optional_parent_issue, :only => [:new]
25
  before_filter :find_project, :only => [:new, :auto_complete_for_issue_parent, :add_subissue, :update_form, :preview ]
26
  before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar ]
27
  before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu, :auto_complete_for_issue_parent ]
26 28
  accept_key_auth :index, :changes
27 29

  
28 30
  helper :journals
......
41 43
  include SortHelper
42 44
  include IssuesHelper
43 45
  helper :timelog
46
  include ActionView::Helpers::PrototypeHelper
44 47
  include Redmine::Export::PDF
45 48

  
49
  def auto_complete_for_issue_parent
50
    @phrase = params[:issue_parent]
51
    @candidates = []
52

  
53
    # If cross project issue relations is allowed we should get
54
    # candidates from every project
55
    if Setting.cross_project_issue_relations?
56
      projects_to_search = nil
57
    else
58
      projects_to_search = [ @project ] + @project.active_children
59
    end
60

  
61
    # Try to find issue by id.
62
    if @phrase.match(/^#?(\d+)$/)
63
      if Setting.cross_project_issue_relations?
64
        issue = Issue.find_by_id( $1)
65
      else
66
        issue = Issue.find_by_id_and_project_id( $1, projects_to_search.collect { |i| i.id})
67
      end
68
      @candidates = [ issue ] if issue
69
    end
70

  
71
    # If finding by id is fail, try to find by searching in subject
72
    # and description.
73
    if @candidates.empty?
74
      # extract tokens from the question
75
      # eg. hello "bye bye" => ["hello", "bye bye"]
76
      tokens = @phrase.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
77
      # tokens must be at least 3 character long
78
      tokens = tokens.uniq.select {|w| w.length > 2 }
79
      like_tokens = tokens.collect {|w| "%#{w.downcase}%"}      
80

  
81
      @candidates, count = Issue.search( like_tokens, projects_to_search, :before => true)
82
    end
83

  
84
    render :inline => "<%= auto_complete_result_parent_issue( @candidates, @phrase) %>"
85
  end
86

  
46 87
  def index
47 88
    retrieve_query
48 89
    sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
......
58 99
      end
59 100
      @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 101
      @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61
      @issues = Issue.find :all, :order => sort_clause,
62
                           :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63
                           :conditions => @query.statement,
64
                           :limit  =>  limit,
65
                           :offset =>  @issue_pages.current.offset
102
      @issues = Issue.find( :all, :order => sort_clause,
103
                            :include => [ :assigned_to,
104
                                          :status,
105
                                          :tracker,
106
                                          :project,
107
                                          :priority,
108
                                          :category,
109
                                          :fixed_version ],
110
                            :conditions => @query.statement,
111
                            :limit  => limit,
112
                            :offset => @issue_pages.current.offset)
113
      
66 114
      respond_to do |format|
67 115
        format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 116
        format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
......
136 184
    end    
137 185
    @issue.status = default_status
138 186
    @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
139
    
187

  
188

  
140 189
    if request.get? || request.xhr?
141 190
      @issue.start_date ||= Date.today
142 191
    else
......
147 196
        attach_files(@issue, params[:attachments])
148 197
        flash[:notice] = l(:notice_successful_create)
149 198
        call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
199
        @issue.move_to_child_of @parent_issue if @parent_issue
150 200
        redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
151 201
                                        { :action => 'show', :id => @issue })
152 202
        return
......
155 205
    @priorities = Enumeration.priorities
156 206
    render :layout => !request.xhr?
157 207
  end
208

  
209
  def add_subissue
210
    redirect_to :action => 'new', :issue => { :parent_issue_id => @parent_issue.id }
211
  end
158 212
  
159 213
  # Attributes that can be updated on workflow transition (without :edit permission)
160 214
  # TODO: make it configurable (at least per role)
......
449 503
  rescue ActiveRecord::RecordNotFound
450 504
    render_404
451 505
  end
452
  
506

  
453 507
  def find_optional_project
454 508
    @project = Project.find(params[:project_id]) unless params[:project_id].blank?
455 509
    allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
......
458 512
    render_404
459 513
  end
460 514
  
515
  def find_parent_issue
516
    @parent_issue = Issue.find( params[:parent_issue_id])
517
  rescue ActiveRecord::RecordNotFound
518
    render_404
519
  end
520
  
521
  def find_optional_parent_issue
522
    if params[:issue] && !params[:issue][:parent_id].blank?
523
      @parent_issue = Issue.find( params[:issue][:parent_id])
524
    end
525
  rescue ActiveRecord::RecordNotFound
526
    render_404
527
  end
528
  
461 529
  # Retrieve query from session or build a new query
462 530
  def retrieve_query
463 531
    if !params[:query_id].blank?
......
481 549
            @query.add_short_filter(field, params[field]) if params[field]
482 550
          end
483 551
        end
484
        session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
552
        if params[:view_options] and params[:view_options].is_a? Hash
553
          params[:view_options].each_pair do |name, value|
554
            @query.set_view_option( name, value)
555
          end
556
        end
557
        session[:query] = {
558
          :project_id => @query.project_id,
559
          :filters => @query.filters,
560
          :view_options => @query.view_options
561
        }
485 562
      else
486 563
        @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
487
        @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
564
        @query ||= Query.new(:name => "_",
565
                             :project => @project,
566
                             :filters => session[:query][:filters],
567
                             :view_options => session[:query][:view_options])
488 568
        @query.project = @project
489 569
      end
490 570
    end
app/controllers/projects_controller.rb
46 46
  helper :repositories
47 47
  include RepositoriesHelper
48 48
  include ProjectsHelper
49
  helper :versions
50
  include VersionsHelper
49 51
  
50 52
  # Lists visible projects
51 53
  def index
......
292 294
      @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
293 295
    end
294 296
  end
297
  
298
  
299
  def sort_as_tree(issues)
300
    issues.sort!{|a,b| a.hierarchical_level <=> b.hierarchical_level}
301
    @sorted_issues = []
302
    issues.each do |issue|
303
      if @sorted_issues.empty?
304
        @sorted_issues << issue
305
        next
306
      end
307
      @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
308
      @sorted_issues.each do |sorted_issue|
309
        #if same parent and smaller date, stop; if same parent, same date and smaller id, stop; after parent and before next parent, stop; 
310
        if ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date > issue.start_date)) ||
311
         ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date == issue.start_date) && (sorted_issue.id > issue.id)) ||
312
         (@time_to_stop && (sorted_issue.hierarchical_level < issue.hierarchical_level))
313
          @sorted_issues.insert(@sorted_issues.index(sorted_issue), issue)
314
          break
315
        end
316
        @time_to_stop = true if sorted_issue == issue.parent      
317
      end
318
      #if this issue's parent is the last element
319
      @sorted_issues << issue if @time_to_stop
320
    end
321
    @sorted_issues
322
  end
323
  
324
  #assumes that first level issues are ordered by date (sort_as_tree)
325
  def integrate_versions_with_issues_tree(issues, versions)
326
    versions.sort! {|x,y| x.start_date <=> y.start_date }    
327
    versions.each do |version|
328
      issues << version if issues.empty?
329
      issues.each do |issue|
330
        if ((issue.is_a? Issue && issue.root?) || (issue.is_a? Version)) && version.start_date < issue.start_date
331
          #insert version before a root task or another version whose date is immediately after this task's one 
332
          issues.insert(issues.index(issue), version)
333
        elsif issue == issues.last
334
          issues << version        
335
        end
336
      end
337
    end
338
    issues
339
  end  
340
  
295 341
end
app/controllers/queries_controller.rb
30 30
    params[:fields].each do |field|
31 31
      @query.add_filter(field, params[:operators][field], params[:values][field])
32 32
    end if params[:fields]
33
    
33

  
34
    params[:view_options].each_pair do |name, value|
35
      @query.set_view_option( name, value)
36
    end if params[:view_options]
37

  
34 38
    if request.post? && params[:confirm] && @query.save
35 39
      flash[:notice] = l(:notice_successful_create)
36 40
      redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
......
45 49
      params[:fields].each do |field|
46 50
        @query.add_filter(field, params[:operators][field], params[:values][field])
47 51
      end if params[:fields]
52
      params[:view_options].each_pair do |name, value|
53
        @query.set_view_option( name, value)
54
      end if params[:view_options]
48 55
      @query.attributes = params[:query]
49 56
      @query.project = nil if params[:query_is_for_all]
50 57
      @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
app/controllers/versions_controller.rb
20 20
  before_filter :find_project, :authorize
21 21

  
22 22
  def show
23
    @issues = @version.fixed_issues.find(:all,
24
                                         :include => [:status, :tracker],
25
                                         :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
26
    @issues = Issue.find_with_parents( @issues.collect { |i| i.id})
23 27
  end
24 28
  
25 29
  def edit
app/helpers/issues_helper.rb
19 19

  
20 20
module IssuesHelper
21 21
  include ApplicationHelper
22
  
23
  def issue_ancestors(issue=@issue)
24
    ancestors = ""
25
    return "" if issue.parent == nil
26
    ancestors += "issue-#{issue.parent.id}-child " + issue_ancestors(issue.parent)        
27
  end
22 28

  
23 29
  def render_issue_tooltip(issue)
24 30
    @cached_label_start_date ||= l(:field_start_date)
......
196 202
    export.rewind
197 203
    export
198 204
  end
205
  
206
  def auto_complete_result_parent_issue(candidates, phrase)
207
    return "" if candidates.empty?
208
    candidates.map! do |c|
209
      content_tag("li", highlight( c.to_s, phrase), :id => String( c[:id]))
210
    end
211
    content_tag("ul", candidates.uniq)
212
  end
199 213
end
app/helpers/queries_helper.rb
1
# -*- coding: mule-utf-8 -*-
1 2
# redMine - project management software
2 3
# Copyright (C) 2006-2007  Jean-Philippe Lang
3 4
#
......
27 28
                      content_tag('th', column.caption)
28 29
  end
29 30
  
30
  def column_content(column, issue)
31
  def column_content(column, issue, query)
31 32
    if column.is_a?(QueryCustomFieldColumn)
32 33
      cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 34
      show_value(cv)
......
40 41
      else
41 42
        case column.name
42 43
        when :subject
43
        h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
44
          link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
44
          subject_in_tree(issue, value, query)
45 45
        when :done_ratio
46 46
          progress_bar(value, :width => '80px')
47 47
        when :fixed_version
......
52 52
      end
53 53
    end
54 54
  end
55
  
56
  def subject_in_tree(issue, value, query)
57
    case query.view_options['show_parents']
58
    when Query::VIEW_OPTIONS_SHOW_PARENTS_NEVER
59
      content_tag('div', subject_text(issue, value), :class=>'issue-subject')
60
    else
61
      content_tag('span', content_tag('div', subject_text(issue, value), :class=>'issue-subject'), :class=>"issue-subject-level-#{issue.hierarchical_level}")
62
    end
63
  end
64
  
65
  def subject_text(issue, value)
66
    subject_text = link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
67
    h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + subject_text
68
  end
69

  
70
  def issue_content(issue, query, options = { })
71
    html = ""
72
    html << "<tr id=\"issue-#{issue.id}\" class=\"issue hascontextmenu " +
73
      ( options[:unfiltered] ? 'issue-unfiltered ' : '') +
74
      "status-#{issue.status.position} priority-#{issue.priority.position} " +
75
      cycle('odd', 'even') + '">'
76
    html << '<td class="checkbox">' + check_box_tag( "ids[]", issue.id, false, :id => nil) + '</td>'
77
    html << '<td>' + link_to( issue.id, :controller => 'issues', :action => 'show', :id => issue) + '</td>'
78
    query.columns.each do |column|
79
      html << content_tag( 'td', column_content(column, issue, query), :class => column.name)
80
    end
81
    html << "</tr>"
82
    html
83
  end
84

  
85
  def issues_family_content( parent, issues_to_show, query)
86
    html = ""
87
    html << issue_content( parent, query, :unfiltered => !( issues_to_show.include? parent))
88
    unless  parent.children.empty?
89
      parent.children.each do |child|
90
        if issues_to_show.include?( child) || issues_to_show.detect { |i| i.ancestors.include? child }
91
          html << issues_family_content( child, issues_to_show, query)
92
        end
93
      end
94
    end
95
    html
96
  end
97
  
55 98
end
app/helpers/versions_helper.rb
44 44
  def status_by_options_for_select(value)
45 45
    options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
46 46
  end
47

  
48
  def render_list_of_related_issues( issues, version, current_level = 1)
49
    issues_on_current_level = issues.select { |i| i.hierarchical_level == current_level }
50
    issues -= issues_on_current_level
51
    content_tag( 'ul') do
52
      html = ''
53
      issues_on_current_level.each do |issue|
54
        opts_for_issue_li = { }
55
        if !issue.fixed_version or issue.fixed_version != version
56
          opts_for_issue_li[:class] = 'issue-unfiltered'
57
        end
58
        html << content_tag( 'li', opts_for_issue_li) do
59
          opts = { }
60
          if issue.done_ratio == 100
61
            opts[:style] = 'font-weight: bold'
62
          end
63
          link_to_issue(issue, opts)  + ": " + h(issue.subject)
64
        end
65
        children_to_print = issues & issue.children
66
        children_to_print += issues.select { |i| i.hierarchical_level >= current_level + 2}
67
        unless children_to_print.empty?
68
          html << render_list_of_related_issues( children_to_print, version, current_level + 1)
69
        end
70
      end
71
      html
72
    end
73
  end
47 74
end
app/models/issue.rb
18 18
class Issue < ActiveRecord::Base
19 19
  belongs_to :project
20 20
  belongs_to :tracker
21
  belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22
  belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23
  belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24
  belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25
  belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26
  belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
21
  belongs_to :status,        :class_name => 'IssueStatus',   :foreign_key => 'status_id'
22
  belongs_to :author,        :class_name => 'User',          :foreign_key => 'author_id'
23
  belongs_to :assigned_to,   :class_name => 'User',          :foreign_key => 'assigned_to_id'
24
  belongs_to :fixed_version, :class_name => 'Version',       :foreign_key => 'fixed_version_id'
25
  belongs_to :priority,      :class_name => 'Enumeration',   :foreign_key => 'priority_id'
26
  belongs_to :category,      :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27

  
28 28
  has_many :journals, :as => :journalized, :dependent => :destroy
29 29
  has_many :time_entries, :dependent => :delete_all
30 30
  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
  
32 32
  has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33
  has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33
  has_many :relations_to,   :class_name => 'IssueRelation', :foreign_key => 'issue_to_id',   :dependent => :delete_all
34 34
  
35 35
  acts_as_attachable :after_remove => :attachment_removed
36 36
  acts_as_customizable
......
46 46
  acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47
                            :author_key => :author_id
48 48
  
49
  acts_as_nested_set
50

  
51
  alias_method :nested_set_move_to, :move_to
52

  
53
  # Move the node to the left of another node (you can pass id only)
54
  def move_to_left_of(node)
55
    nested_set_move_to node, :left
56
  end
57

  
58
  # Move the node to the left of another node (you can pass id only)
59
  def move_to_right_of(node)
60
    nested_set_move_to node, :right
61
  end
62

  
63
  # Move the node to the child of another node (you can pass id only)
64
  def move_to_child_of(node)
65
    nested_set_move_to node, :child
66
  end
67
  
68
  # Move the node to root nodes
69
  def move_to_root
70
    nested_set_move_to nil, :root
71
  end
72

  
49 73
  validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 74
  validates_length_of :subject, :maximum => 255
51 75
  validates_inclusion_of :done_ratio, :in => 0..100
......
55 79
                                          :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56 80
  
57 81
  named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
58
  
59
  # Returns true if usr or current user is allowed to view the issue
60
  def visible?(usr=nil)
61
    (usr || User.current).allowed_to?(:view_issues, self.project)
62
  end
63
  
64
  def after_initialize
65
    if new_record?
66
      # set default values for new records only
67
      self.status ||= IssueStatus.default
68
      self.priority ||= Enumeration.priorities.default
69
    end
70
  end
71
  
72
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
73
  def available_custom_fields
74
    (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
75
  end
76
  
77
  def copy_from(arg)
78
    issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
79
    self.attributes = issue.attributes.dup
80
    self.custom_values = issue.custom_values.collect {|v| v.clone}
81
    self
82
  end
83
  
84
  # Moves/copies an issue to a new project and tracker
85
  # Returns the moved/copied issue on success, false on failure
86
  def move_to(new_project, new_tracker = nil, options = {})
87
    options ||= {}
88
    issue = options[:copy] ? self.clone : self
89
    transaction do
90
      if new_project && issue.project_id != new_project.id
91
        # delete issue relations
92
        unless Setting.cross_project_issue_relations?
93
          issue.relations_from.clear
94
          issue.relations_to.clear
95
        end
96
        # issue is moved to another project
97
        # reassign to the category with same name if any
98
        new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
99
        issue.category = new_category
100
        issue.fixed_version = nil
101
        issue.project = new_project
102
      end
103
      if new_tracker
104
        issue.tracker = new_tracker
105
      end
106
      if options[:copy]
107
        issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
108
        issue.status = self.status
109
      end
110
      if issue.save
111
        unless options[:copy]
112
          # Manually update project_id on related time entries
113
          TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
114
        end
115
      else
116
        Issue.connection.rollback_db_transaction
117
        return false
118
      end
119
    end
120
    return issue
121
  end
122
  
123
  def priority_id=(pid)
124
    self.priority = nil
125
    write_attribute(:priority_id, pid)
126
  end
127
  
128
  def estimated_hours=(h)
129
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
130
  end
131
  
82

  
132 83
  def validate
84
    # FIXME: I do not know what actually this should do, but this
85
    # validation does not allow me to change due_date in hook when
86
    # fixed_version is set.
133 87
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
134 88
      errors.add :due_date, :not_a_date
135 89
    end
......
141 95
    if start_date && soonest_start && start_date < soonest_start
142 96
      errors.add :start_date, :invalid
143 97
    end
98

  
99
    unless children.empty?
100
      if IssueStatus.find_by_id( @attributes['status_id']).is_closed? &&
101
          children.detect { |i| !i.closed? }
102
        errors.add( :status,
103
                    "Can't close parent issue " +
104
                    "while one of the children is still open.")
105
      end
106

  
107
      children_max_fixed_version = children.select { |i| i.fixed_version } .max { |a,b| a.fixed_version <=> b.fixed_version }
108
      if @attributes['fixed_version_id'] && children_max_fixed_version
109
        if Version.find_by_id( @attributes['fixed_version_id']) < children_max_fixed_version.fixed_version
110
          errors.add :fixed_version, "Can't set target version of parent issue lower than any of the children."
111
        end
112
      end
113
    end
144 114
  end
145 115
  
146 116
  def validate_on_create
......
161 131
        @current_journal.details << JournalDetail.new(:property => 'attr',
162 132
                                                      :prop_key => c,
163 133
                                                      :old_value => @issue_before_change.send(c),
164
                                                      :value => send(c)) unless send(c)==@issue_before_change.send(c)
134
                                                      :value => send(c)) unless send(c)==@issue_before_change.send(c) || (!self.leaf? && %w(status_id priority_id fixed_version_id start_date due_date done_ratio estimated_hours).include?(c))
165 135
      }
166 136
      # custom fields changes
167 137
      custom_values.each {|c|
......
174 144
      }      
175 145
      @current_journal.save
176 146
    end
147

  
177 148
    # Save the issue even if the journal is not saved (because empty)
178 149
    true
150

  
179 151
  end
180 152
  
181 153
  def after_save
182 154
    # Reload is needed in order to get the right status
183 155
    reload
184
    
156

  
185 157
    # Update start/due dates of following issues
186 158
    relations_from.each(&:set_issue_to_dates)
187
    
159

  
160
    if parent
161
      # Set default status of parent if new status opened the issue.
162
      if !status.is_closed? && parent.status.is_closed?
163
        parent.update_attribute :status, IssueStatus.default
164
      end
165

  
166
      # Set 'Target version' of parent if one was set on one of the
167
      # children issue and parent have no 'Target version'. Do the same
168
      # if 'Target version of the parent issue lower (by the release
169
      # date or by the version number).
170
      if parent.fixed_version.nil? && fixed_version or
171
          ( parent.fixed_version && fixed_version and
172
            parent.fixed_version.project == fixed_version.project and
173
            parent.fixed_version < fixed_version )
174
        parent.update_attribute :fixed_version, fixed_version
175
      end
176
    end
177

  
178
    # If target version is set, but "Due to" date is not, set it as
179
    # the same as the date of target version.
180
    if due_date.nil? && fixed_version && fixed_version.due_date
181
      self.update_attribute :due_date, fixed_version.due_date
182
    end
183

  
188 184
    # Close duplicates if the issue was closed
189 185
    if @issue_before_change && !@issue_before_change.closed? && self.closed?
190 186
      duplicates.each do |duplicate|
......
198 194
      end
199 195
    end
200 196
  end
197

  
198
  # Returns true if usr or current user is allowed to view the issue
199
  def visible?(usr=nil)
200
    (usr || User.current).allowed_to?(:view_issues, self.project)
201
  end
202
  
203
  def after_initialize
204
    if new_record?
205
      # set default values for new records only
206
      self.status ||= IssueStatus.default
207
      self.priority ||= Enumeration.priorities.default
208
    end
209
  end
210
  
211
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
212
  def available_custom_fields
213
    (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
214
  end
215
  
216
  def copy_from(arg)
217
    issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
218
    self.attributes = issue.attributes.dup
219
    self.custom_values = issue.custom_values.collect {|v| v.clone}
220
    self
221
  end
222
  
223
  # Moves/copies an issue to a new project and tracker
224
  # Returns the moved/copied issue on success, false on failure
225
  def move_to(new_project, new_tracker = nil, options = {})
226
    options ||= {}
227
    issue = if options[:copy]
228
              Issue.new( self.attributes.reject { |k,v| k == 'parent_id' })
229
            else
230
              self
231
            end
232
    transaction do
233
      if new_project && issue.project_id != new_project.id
234
        unless Setting.cross_project_issue_relations?
235
          # delete issue relations
236
          issue.relations_from.clear
237
          issue.relations_to.clear
238

  
239
          issue.children.each(&:move_to_root) unless options[:copy]
240
        end
241
        # issue is moved to another project
242
        # reassign to the category with same name if any
243
        new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
244
        issue.category = new_category
245
        issue.fixed_version = nil
246
        issue.project = new_project
247
      end
248
      if new_tracker
249
        issue.tracker = new_tracker
250
      end
251
      if options[:copy]
252
        issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
253
        issue.status = self.status
254
      end
255
      if issue.save
256
        unless options[:copy]
257
          # Manually update project_id on related time entries
258
          TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
259
        end
260
        if new_project && issue.project_id != new_project.id &&
261
            !Setting.cross_project_issue_relations?
262
          issue.move_to_root
263
        end
264
      else
265
        Issue.connection.rollback_db_transaction
266
        return false
267
      end
268
    end
269
    return issue
270
  end
271
  
272
  def estimated_hours=(h)
273
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
274
  end
201 275
  
202 276
  def init_journal(user, notes = "")
203 277
    @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
......
210 284
    @current_journal
211 285
  end
212 286
  
287
  def priority_id=(pid)
288
    self.priority = nil
289
    write_attribute(:priority_id, pid)
290
  end
291

  
213 292
  # Return true if the issue is closed, otherwise false
214 293
  def closed?
215 294
    self.status.is_closed?
......
274 353
    due_date || (fixed_version ? fixed_version.effective_date : nil)
275 354
  end
276 355
  
356
  def duration1
357
    (start_date && due_date) ? (due_date - start_date + 1) : 0
358
  end
359
  
277 360
  # Returns the time scheduled for this issue.
278 361
  # 
279 362
  # Example:
......
287 370
    @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
288 371
  end
289 372
  
373
  def self.visible_by(usr)
374
    with_scope(:find => { :conditions => Project.visible_by(usr) }) do
375
      yield
376
    end
377
  end
378
  
379
  def done_ratio
380
    if children?
381
      @total_planned_days ||= 0
382
      @total_actual_days ||= 0
383
      children.each do |child| # from every subtask get the total number of days and the number of days already "worked"
384
        planned_days = child.duration1
385
        actual_days = child.done_ratio ?  (planned_days * child.done_ratio / 100).floor : 0
386
        @total_planned_days += planned_days
387
        @total_actual_days += actual_days
388
      end
389
      @total_done_ratio = @total_planned_days != 0 ? (@total_actual_days * 100 / @total_planned_days).floor : 0
390
    else
391
      read_attribute(:done_ratio)
392
    end
393
  end
394
  
395
  def estimated_hours
396
    if children?
397
      is_set = false
398
      children.each do |child|
399
        if child.estimated_hours
400
          if is_set
401
            @est_hours += child.estimated_hours
402
          else
403
            @est_hours = child.estimated_hours
404
            is_set = true
405
          end
406
        end     
407
      end
408
      @est_hours
409
    else
410
      read_attribute(:estimated_hours)
411
    end
412
  end
413
  
414
  def due_date
415
    if children?
416
      children_date = children.find_all { |i| i.due_date } 
417
      unless children_date.empty?
418
        children_date.sort { |a,b| a.due_date <=> b.due_date} .max
419
      else
420
        read_attribute(:due_date)
421
      end
422
    else
423
      read_attribute(:due_date)
424
    end
425
  end  
426

  
427
  def children?
428
    children != []
429
  end
430
  
431
  #First level tasks have hierarchical level = 1 and so on
432
  def hierarchical_level(issue=self)
433
    1 + level
434
  end
435
  
436
  # FIXME: remove this method.
437
  def self.find_with_parents( *args)
438
    issues = find( *args)
439
    return [] if issues.empty?
440
    issues.each do |i|
441
      while not i.root?
442
        issues += [ i.parent ]
443
        i = i.parent
444
      end
445
    end
446
    issues.uniq
447
  end
448
  
290 449
  def to_s
291 450
    "#{tracker} ##{id}: #{subject}"
292 451
  end
app/models/issue_relation.rb
17 17

  
18 18
class IssueRelation < ActiveRecord::Base
19 19
  belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
20
  belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
20
  belongs_to :issue_to,   :class_name => 'Issue', :foreign_key => 'issue_to_id'
21 21
  
22 22
  TYPE_RELATES      = "relates"
23 23
  TYPE_DUPLICATES   = "duplicates"
24 24
  TYPE_BLOCKS       = "blocks"
25 25
  TYPE_PRECEDES     = "precedes"
26 26
  
27
  TYPES = { TYPE_RELATES =>     { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
28
            TYPE_DUPLICATES =>  { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
29
            TYPE_BLOCKS =>      { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
30
            TYPE_PRECEDES =>    { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
27
  TYPES = { TYPE_RELATES    => { :name => :label_relates_to, :sym_name => :label_relates_to,    :order => 1 },
28
            TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
29
            TYPE_BLOCKS     => { :name => :label_blocks,     :sym_name => :label_blocked_by,    :order => 3 },
30
            TYPE_PRECEDES   => { :name => :label_precedes,   :sym_name => :label_follows,       :order => 4 },
31 31
          }.freeze
32 32
  
33
  validates_presence_of :issue_from, :issue_to, :relation_type
34
  validates_inclusion_of :relation_type, :in => TYPES.keys
35
  validates_numericality_of :delay, :allow_nil => true
36
  validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
37
  
33
  validates_presence_of     :issue_from,    :issue_to, :relation_type 
34
  validates_inclusion_of    :relation_type, :in        => TYPES.keys
35
  validates_numericality_of :delay,         :allow_nil => true
36
  validates_uniqueness_of   :issue_to_id,   :scope     => :issue_from_id
37

  
38 38
  attr_protected :issue_from_id, :issue_to_id
39 39
  
40 40
  def validate
......
59 59
    else
60 60
      self.delay = nil
61 61
    end
62

  
62 63
    set_issue_to_dates
63 64
  end
64
  
65

  
65 66
  def set_issue_to_dates
66 67
    soonest_start = self.successor_soonest_start
67 68
    if soonest_start && (!issue_to.start_date || issue_to.start_date < soonest_start)
app/models/query.rb
52 52
  end
53 53
end
54 54

  
55
class ViewOption
56
  attr_accessor :name, :available_values
57
  include Redmine::I18n
58
  
59
  def initialize( name, available_values)
60
    self.name = name
61
    self.available_values = available_values
62
  end
63

  
64
  def caption
65
    l("label_view_option_#{name}")
66
  end
67
end
68

  
55 69
class Query < ActiveRecord::Base
56 70
  belongs_to :project
57 71
  belongs_to :user
58 72
  serialize :filters
59 73
  serialize :column_names
74
  serialize :view_options
60 75
  serialize :sort_criteria, Array
61 76
  
62 77
  attr_protected :project_id, :user_id
......
115 130
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
116 131
  ]
117 132
  cattr_reader :available_columns
133

  
134
  VIEW_OPTIONS_SHOW_PARENTS_NEVER              = 'do_not_show'
135
  VIEW_OPTIONS_SHOW_PARENTS_ALWAYS             = 'show_always'
136
  VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT = 'organize_by_parent'
137

  
138
  @@available_view_options = [
139
    ViewOption.new( 'show_parents', [ [ l(:label_view_option_parents_do_not_show), VIEW_OPTIONS_SHOW_PARENTS_NEVER ],
140
                                      [ l(:label_view_option_parents_show_always), VIEW_OPTIONS_SHOW_PARENTS_ALWAYS ],
141
                                      [ l(:label_view_option_parents_show_and_group), VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT ] ])
142
  ]
143
  cattr_reader :available_view_options
118 144
  
119 145
  def initialize(attributes = nil)
120 146
    super attributes
121 147
    self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
148
    self.view_options ||=  { 'show_parents' => 'do_not_show' }
122 149
  end
123 150
  
124 151
  def after_initialize
......
353 380
    
354 381
    (filters_clauses << project_statement).join(' AND ')
355 382
  end
356
  
383

  
384
  def set_view_option( option, value)
385
    self.view_options[option] = value
386
  end
387

  
388
  def values_for_view_option( option)
389
    @@available_view_options.find { |vo| vo.name == option }.available_values
390
  end
391

  
392
  def caption_for_view_option( option)
393
    @@available_view_options.find { |vo| vo.name == option }.caption
394
  end
395

  
357 396
  private
358 397
  
359 398
  # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
app/models/version.rb
27 27
  validates_length_of :name, :maximum => 60
28 28
  validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
29 29
  
30
  include Comparable
31
  
30 32
  def start_date
31 33
    effective_date
32 34
  end
app/views/issues/_edit.rhtml
15 15
        <%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %>
16 16
        </fieldset>
17 17
    <% end %>
18
    <% if authorize_for('timelog', 'edit') %>
18
    <% if authorize_for('timelog', 'edit') && @issue.leaf? %>
19 19
        <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
20 20
        <% fields_for :time_entry, @time_entry, { :builder => TabularFormBuilder, :lang => current_language} do |time_entry| %>
21 21
        <div class="splitcontentleft">
app/views/issues/_form.rhtml
1
<!-- This function is needed for get ID from the text field with parent issue selection. -->
2
<script type="text/javascript">
3
  //<![CDATA[
4
  function setParentIssueValue(element, value) {
5
          document.getElementById('parent_issue_id').value = value.id;
6
      }
7
//]]>
8
</script>
1 9
<% if @issue.new_record? %>
2 10
<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p>
3 11
<%= observe_field :issue_tracker_id, :url => { :action => :new },
......
17 25

  
18 26
<div class="attributes">
19 27
<div class="splitcontentleft">
20
<% if @issue.new_record? || @allowed_statuses.any? %>
28
<% if (@issue.new_record? || @allowed_statuses.any?) %>
21 29
<p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
22 30
<% else %>
23 31
<p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
24 32
<% end %>
25 33

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

  
27 36
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
28 37
<% unless @project.issue_categories.empty? %>
29 38
<p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
......
38 47
</div>
39 48

  
40 49
<div class="splitcontentright">
50
<% if @issue.new_record? ||  @issue.leaf? %>	
41 51
<p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
42 52
<p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
43 53
<p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
44 54
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
55
<% else %>
56
<p><label><%= l(:field_start_date) %></label> <%= format_date(@issue.start_date) %></p>
57
<p><label><%= l(:field_due_date) %></label> <%= format_date(@issue.due_date) %></p>
58
<p><label><%= l(:field_done_ratio) %></label> <%= "#{@issue.done_ratio}%" %></p>
59
<% end %>
60
<%= content_tag( :input, {}, 
61
		 :id => :parent_issue_id,
62
		 :type => :hidden,
63
		 :name => 'parent_issue_id', :value => @parent_issue ? @parent_issue.id : "") %>
64
<p><label><%= l(:field_parent_issue) %></label>
65
<% if authorize_for( 'issues', 'add_subissue') %>
66
  <%= text_field_with_auto_complete( :issue, :parent,
67
				     { :name => 'issue_parent', :value => @parent_issue || "" },
68
				     :url => { :action => 'auto_complete_for_issue_parent', :project_id => @project},
69
				     :after_update_element => 'setParentIssueValue') %>
70
<% else %>
71
  <%= @parent_issue || "-" %>
72
<% end %>
73
</p>
45 74
</div>
46 75

  
47 76
<div style="clear:both;"> </div>
app/views/issues/_form_update.rhtml
4 4
<p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
5 5
</div>
6 6
<div class="splitcontentright">
7
<% if @issue.leaf? %>
7 8
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
9
<% else %>
10
<p><label><%= l(:field_done_ratio) %></label> <%= "#{@issue.done_ratio}%" %></p>
11
<% end %>
8 12
<%= content_tag('p', f.select(:fixed_version_id, 
9 13
                          (@project.versions.sort.collect {|v| [v.name, v.id]}),
10 14
                          { :include_blank => true })) unless @project.versions.empty? %>
app/views/issues/_list.rhtml
1
<% form_tag({}) do -%>	
2
<table class="list issues">
1
<% form_tag({}) do -%>
2
  <table class="list issues">
3 3
    <thead><tr>
4 4
        <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
5
                                                           :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
5
                :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
6 6
        </th>
7
		<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
7
	<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
8 8
        <% query.columns.each do |column| %>
9 9
          <%= column_header(column) %>
10 10
        <% end %>
11
	</tr></thead>
12
	<tbody>
11
      </tr></thead>
12
    <tbody>
13
      <% if query.view_options['show_parents'] == Query::VIEW_OPTIONS_SHOW_PARENTS_NEVER -%>
13 14
	<% issues.each do |issue| -%>
14
	<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>">
15
	    <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
16
		<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
17
        <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
18
	</tr>
15
	  <%= issue_content( issue, query) %>
19 16
	<% end -%>
20
	</tbody>
21
</table>
17
      <% elsif query.view_options['show_parents'] == Query::VIEW_OPTIONS_SHOW_PARENTS_ALWAYS -%>
18
	<% issues.each do |issue| -%>
19
	  <% issue.ancestors.reverse.each do |parent_issue| -%>
20
	    <%= issue_content( parent_issue, query, :unfiltered => true) %>
21
	  <% end -%>
22
	  <%= issue_content( issue, query) %>
23
	<% end -%>
24
      <% elsif query.view_options['show_parents'] == Query::VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT -%>
25
	<% parents_on_first_lvl = []
26
	   issues.each do |i|
27
	     if i.parent
28
	       first_parent = i.root
29
	     else
30
	       first_parent = i
31
	     end
32
	     parents_on_first_lvl += [ first_parent ] unless parents_on_first_lvl.include?( first_parent)
33
	   end -%>
34
	<% parents_on_first_lvl.each do |parent| -%>
35
	  <%= issues_family_content( parent, issues, query) %>
36
	<% end -%>
37
      <% end -%>
38
    </tbody>
39
  </table>
22 40
<% end -%>
app/views/issues/context_menu.rhtml
17 17
	<li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
18 18
	        :class => 'icon-edit', :disabled => !@can[:edit] %></li>
19 19
<% end %>
20

  
21 20
	<li class="folder">			
22 21
		<a href="#" class="submenu"><%= l(:field_priority) %></a>
23 22
		<ul>
......
66 65
		</ul>
67 66
	</li>
68 67
	<% end -%>
68
	<% if @issue && @issue.leaf? %>
69 69
	<li class="folder">
70 70
		<a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
71 71
		<ul>
......
75 75
		<% end -%>
76 76
		</ul>
77 77
	</li>
78
	
78
	<% end %>
79 79
<% if !@issue.nil? %>
80 80
	<li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
81 81
	        :class => 'icon-copy', :disabled => !@can[:copy] %></li>
app/views/issues/index.rhtml
4 4
    
5 5
    <% form_tag({ :controller => 'queries', :action => 'new' }, :id => 'query_form') do %>
6 6
    <%= hidden_field_tag('project_id', @project.to_param) if @project %>
7
    <fieldset id="view"><legend><%= l(:label_view) %></legend>
8
      <%= render :partial => 'queries/view_options', :locals => {:query => @query } %>
9
    </fieldset>
7 10
    <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
8
    <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
11
      <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
12
    </fieldset>
9 13
    <p class="buttons">
10 14
    <%= link_to_remote l(:button_apply), 
11 15
                       { :url => { :set_filter => 1 },
......
23 27
    <%= link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save' %>
24 28
    <% end %>
25 29
    </p>
26
    </fieldset>
27 30
    <% end %>
28 31
<% else %>
29 32
    <div class="contextual">
app/views/issues/show.rhtml
1 1
<div class="contextual">
2
<%= link_to_if_authorized(l(:button_add_subissue),
3
			  { :controller => 'issues', :action => 'add_subissue',
4
			    :project_id => @project.id, :issue => { :parent_id => @issue.id }},
5
			  :class => 'icon icon-add') %>
2 6
<%= 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)) %>
3 7
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
4 8
<%= watcher_tag(@issue, User.current) %>
......
51 55
   if (n > 1) 
52 56
        n = 0 %>
53 57
        </tr><tr>
54
 <%end
55
end %>
58
   <% end %>
59
<% end %>
56 60
</tr>
57 61
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
58 62
</table>
app/views/projects/roadmap.rhtml
10 10
    <%= render :partial => 'versions/overview', :locals => {:version => version} %>
11 11
    <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
12 12

  
13
    <% issues = version.fixed_issues.find(:all,
14
                                          :include => [:status, :tracker],
15
                                          :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"],
16
                                          :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty?
13
    <%
14
       unless @selected_tracker_ids.empty?
15
	 issues = version.fixed_issues.find(:all,
16
                                           :include => [:status, :tracker],
17
                                           :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"],
18
                                           :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") 
19
         issues = Issue.find_with_parents( issues.collect { |i| i.id })
20
       end
17 21
       issues ||= []
18 22
    %>
19 23
    <% if issues.size > 0 %>
20 24
    <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
21
    <ul>
22
    <%- issues.each do |issue| -%>
23
        <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
24
    <%- end -%>
25
    </ul>
25
      <%= render_list_of_related_issues( issues, version) %>
26 26
    </fieldset>
27 27
    <% end %>
28 28
    <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
app/views/queries/_form.rhtml
21 21
      :onclick => 'if (this.checked) {Element.hide("columns")} else {Element.show("columns")}' %></p>
22 22
</div>
23 23

  
24
<fieldset><legend><%= l(:label_view) %></legend>
25
  <%= render :partial => 'queries/view_options', :locals => {:query => query } %>
26
</fieldset>
27

  
24 28
<fieldset><legend><%= l(:label_filter_plural) %></legend>
25 29
<%= render :partial => 'queries/filters', :locals => {:query => query}%>
26 30
</fieldset>
app/views/queries/_view_options.rhtml
1
<% query.view_options.each_key do |voption| -%>
2
  <%= query.caption_for_view_option( voption) %>:
3
  <%= select_tag( "view_options[#{voption}]",
4
		  options_for_select( query.values_for_view_option( voption),
5
				      query.view_options[voption])) %>
6
<% end %>
app/views/versions/show.rhtml
31 31
<%= render :partial => 'versions/overview', :locals => {:version => @version} %>
32 32
<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
33 33

  
34
<% issues = @version.fixed_issues.find(:all,
35
                                       :include => [:status, :tracker],
36
                                       :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
37
<% if issues.size > 0 %>
34
<% if @issues.size > 0 %>
38 35
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
39
<ul>
40
<% issues.each do |issue| -%>
41
    <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
42
<% end -%>
43
</ul>
36
<%= render_list_of_related_issues( @issues, @version) %>
44 37
</fieldset>
45 38
<% end %>
46 39
</div>
config/locales/en.yml
136 136
  error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
137 137
  error_scm_annotate: "The entry does not exist or can not be annotated."
138 138
  error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
139
  
139
  error_issue_can_have_only_one_parent: allows only one relation (a task can have only one parent).
140

  
140 141
  warning_attachments_not_saved: "{{count}} file(s) could not be saved."
141 142
  
142 143
  mail_subject_lost_password: "Your {{value}} password"
......
242 243
  field_watcher: Watcher
243 244
  field_identity_url: OpenID URL
244 245
  field_content: Content
246
  field_calendar_firstday: First day of week
247
  field_parent: Subproject of
248
  field_parent_issue: Child of
249
  field_parent_title: Parent page
245 250
  
246 251
  setting_app_title: Application title
247 252
  setting_app_subtitle: Application subtitle
......
664 669
  label_issue_watchers: Watchers
665 670
  label_example: Example
666 671
  label_display: Display
672
  label_children: parent of
673
  label_parents: child of
674
  label_view_option_parents_do_not_show: Never
675
  label_view_option_parents_show_always: Always
676
  label_view_option_parents_show_and_group: Organize by parent
677
  label_view_option_show_parents: Show parents
667 678
  label_sort: Sort
668 679
  label_ascending: Ascending
669 680
  label_descending: Descending
......
708 719
  button_update: Update
709 720
  button_configure: Configure
710 721
  button_quote: Quote
711
  
722
  button_add_subissue: Add sub-issue
723

  
712 724
  status_active: active
713 725
  status_registered: registered
714 726
  status_locked: locked
db/migrate/20090115162651_add_queries_view_options.rb
1
class AddQueriesViewOptions < ActiveRecord::Migration
2
  def self.up
3
    add_column :queries, :view_options, :text
4
  end
5

  
6
  def self.down
7
    remove_column :queries, :view_options
8
  end
9
end
db/migrate/20090121172432_add_default_value_of_view_option_queries.rb
1
class AddDefaultValueOfViewOptionQueries < ActiveRecord::Migration
2
  def self.up
3
    Query.find(:all).each do |q|
4
      q.view_options ||= { 'show_parents' => 'do_not_show' }
5
      q.save!
6
    end
7
  end
8

  
9
  def self.down
10
  end
11
end
db/migrate/20090406213813_add_issues_nested_sets.rb
1
class AddIssuesNestedSets < ActiveRecord::Migration
2
  # IssueRelation::TYPE_PARENTS was deleted
3
  TYPE_PARENTS = "parents"
4
  
5
  def self.up
6
    add_column :issues, :parent_id, :integer, :default => nil
7
    add_column :issues, :lft, :integer
8
    add_column :issues, :rgt, :integer
9
    Issue.reset_column_information
10

  
11
    say_with_time "fixing invalid issues" do
12
      Issue.find( :all).each do |issue|
13
        if !issue.valid? && issue.errors.on( :due_date)
14
          issue.due_date = issue.start_date
15
          issue.save!
16
        end
17
      end
18
    end
19
    
20
    say_with_time "rebuilding left & right indexes" do
21
      Issue.rebuild!
22
    end
23

  
24
    say_with_time(
25
      "converting subissues for using parent_id instead of IssueRelation") do 
26
      IssueRelation.find_all_by_relation_type( TYPE_PARENTS).each do |rel|
27
        rel.issue_from.move_to_child_of rel.issue_to.id
28
        rel.delete
29
      end
30
    end
31
  end
32

  
33
  def self.down
34
    raise ActiveRecord::IrreversibleMigration
35
  end
36
end
lang/en.yml
1
_gloc_rule_default: '|n| n==1 ? "" : "_plural" '
2

  
3
actionview_datehelper_select_day_prefix:
4
actionview_datehelper_select_month_names: January,February,March,April,May,June,July,August,September,October,November,December
5
actionview_datehelper_select_month_names_abbr: Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec
6
actionview_datehelper_select_month_prefix:
7
actionview_datehelper_select_year_prefix:
8
actionview_datehelper_time_in_words_day: 1 day
9
actionview_datehelper_time_in_words_day_plural: %d days
10
actionview_datehelper_time_in_words_hour_about: about an hour
11
actionview_datehelper_time_in_words_hour_about_plural: about %d hours
12
actionview_datehelper_time_in_words_hour_about_single: about an hour
13
actionview_datehelper_time_in_words_minute: 1 minute
14
actionview_datehelper_time_in_words_minute_half: half a minute
15
actionview_datehelper_time_in_words_minute_less_than: less than a minute
16
actionview_datehelper_time_in_words_minute_plural: %d minutes
17
actionview_datehelper_time_in_words_minute_single: 1 minute
18
actionview_datehelper_time_in_words_second_less_than: less than a second
19
actionview_datehelper_time_in_words_second_less_than_plural: less than %d seconds
20
actionview_instancetag_blank_option: Please select
21

  
22
activerecord_error_inclusion: is not included in the list
23
activerecord_error_exclusion: is reserved
24
activerecord_error_invalid: is invalid
25
activerecord_error_confirmation: doesn't match confirmation
26
activerecord_error_accepted: must be accepted
27
activerecord_error_empty: can't be empty
28
activerecord_error_blank: can't be blank
29
activerecord_error_too_long: is too long
30
activerecord_error_too_short: is too short
31
activerecord_error_wrong_length: is the wrong length
32
activerecord_error_taken: has already been taken
33
activerecord_error_not_a_number: is not a number
34
activerecord_error_not_a_date: is not a valid date
35
activerecord_error_greater_than_start_date: must be greater than start date
36
activerecord_error_not_same_project: doesn't belong to the same project
37
activerecord_error_circular_dependency: This relation would create a circular dependency
38

  
39
general_fmt_age: %d yr
40
general_fmt_age_plural: %d yrs
41
general_fmt_date: %%m/%%d/%%Y
42
general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p
43
general_fmt_datetime_short: %%b %%d, %%I:%%M %%p
44
general_fmt_time: %%I:%%M %%p
45
general_text_No: 'No'
46
general_text_Yes: 'Yes'
47
general_text_no: 'no'
48
general_text_yes: 'yes'
49
general_lang_name: 'English'
50
general_csv_separator: ','
51
general_csv_decimal_separator: '.'
52
general_csv_encoding: ISO-8859-1
53
general_pdf_encoding: ISO-8859-1
54
general_day_names: Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
55
general_first_day_of_week: '7'
56

  
57
notice_account_updated: Account was successfully updated.
58
notice_account_invalid_creditentials: Invalid user or password
59
notice_account_password_updated: Password was successfully updated.
60
notice_account_wrong_password: Wrong password
61
notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
62
notice_account_unknown_email: Unknown user.
63
notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
64
notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
65
notice_account_activated: Your account has been activated. You can now log in.
66
notice_successful_create: Successful creation.
67
notice_successful_update: Successful update.
68
notice_successful_delete: Successful deletion.
69
notice_successful_connection: Successful connection.
70
notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
71
notice_locking_conflict: Data has been updated by another user.
72
notice_not_authorized: You are not authorized to access this page.
73
notice_email_sent: An email was sent to %s
74
notice_email_error: An error occurred while sending mail (%s)
75
notice_feeds_access_key_reseted: Your RSS access key was reset.
76
notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
77
notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
78
notice_account_pending: "Your account was created and is now pending administrator approval."
79
notice_default_data_loaded: Default configuration successfully loaded.
80
notice_unable_delete_version: Unable to delete version.
81

  
... This diff was truncated because it exceeds the maximum size that can be displayed.