0001-Ported-the-redmine_subtasks-plugin-from-Aleksei-Guse.patch

Port of the redmine_subtasks plugin against trunk r3285 - Eric Davis, 2010-01-08 20:16

Download (110 KB)

View differences:

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

  
27 30
  accept_key_auth :index, :show, :changes
28 31

  
29 32
  rescue_from Query::StatementInvalid, :with => :query_statement_invalid
......
45 48
  include IssuesHelper
46 49
  helper :timelog
47 50
  include Redmine::Export::PDF
51
  include ActionView::Helpers::PrototypeHelper
48 52

  
49 53
  verify :method => :post,
50 54
         :only => :destroy,
......
102 106
  end
103 107
  
104 108
  def show
109
    retrieve_query_for_subissues
105 110
    @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
106 111
    @journals.each_with_index {|j,i| j.indice = i+1}
107 112
    @journals.reverse! if User.current.wants_comments_in_reverse_order?
......
151 156
      # Check that the user is allowed to apply the requested status
152 157
      @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
153 158
      call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
159
      @issue.parent_id = params[:issue][:parent_id] if params[:issue]
154 160
      if @issue.save
155 161
        attach_files(@issue, params[:attachments])
156 162
        flash[:notice] = l(:notice_successful_create)
......
185 191
    end
186 192

  
187 193
    if request.post?
194
      @issue.parent_id = params[:issue][:parent_id] if params[:issue]
195

  
188 196
      @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
189 197
      @time_entry.attributes = params[:time_entry]
190 198
      attachments = attach_files(@issue, params[:attachments])
......
212 220
    attachments.each(&:destroy)
213 221
  end
214 222

  
223
  def add_subissue
224
    redirect_to :action => 'new',
225
                :project_id => @parent_issue.project,
226
                :issue => { :parent_id => @parent_issue.id }
227
  end
228
  
215 229
  def reply
216 230
    journal = Journal.find(params[:journal_id]) if params[:journal_id]
217 231
    if journal
......
369 383
                              :order => "start_date, effective_date",
370 384
                              :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
371 385
                              )
386
      # Parent issues that might not have the due_date set but do have
387
      # child issues that do have due_date set should be included.
388
      events.each do |issue|
389
        if issue.leaf?
390
          # Can't use the Issue#visible named_scope because it causes
391
          # a SQL error with the awesome_nested_set
392
          ancestors = issue.ancestors.all(:include => [:tracker, :assigned_to, :priority, :project],
393
                                          :order => "start_date",
394
                                          :conditions => 'start_date IS NOT NULL')
395
          ancestors.map! {|i| i.visible? ? i : nil }.compact!
396

  
397
          events += ancestors.flatten if ancestors.present?
398
        end
399
      end
400
      
372 401
      # Versions
373 402
      events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
374
                                   
375
      @gantt.events = events
403

  
404
      @gantt.events = events.uniq
376 405
    end
377 406
    
378 407
    basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
......
458 487
    @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
459 488
    render :partial => 'common/preview'
460 489
  end
490

  
491
  def auto_complete_for_issue_parent
492
    @phrase = params[:issue_parent]
493
    @candidates = []
494

  
495
    # If cross project issue relations is allowed we should get
496
    # candidates from every project
497
    if Setting.cross_project_issue_relations?
498
      projects_to_search = nil
499
    else
500
      projects_to_search = [ @project ] + @project.children
501
    end
502

  
503
    if @phrase.present?
504
      # Try to find issue by id.
505
      if @phrase.match(/^#?(\d+)$/)
506
        if Setting.cross_project_issue_relations?
507
          issue = Issue.visible.find_by_id( $1)
508
        else
509
          issue = Issue.visible.find_by_id_and_project_id( $1, projects_to_search.collect { |i| i.id})
510
        end
511
        @candidates << issue if issue
512
      end
513

  
514
      # Search by subject and description
515
      # extract tokens from the question
516
      # eg. hello "bye bye" => ["hello", "bye bye"]
517
      tokens = @phrase.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
518
      # tokens must be at least 3 character long
519
      tokens = tokens.uniq.select {|w| w.length > 2 }
520
      like_tokens = tokens.collect {|w| "%#{w.downcase}%"}
521

  
522
      search_results, count = Issue.search( like_tokens, projects_to_search, :before => true, :limit => 10)
523
      @candidates += search_results unless search_results.empty?
524
    end
525

  
526
    # Remove the current issue if it's a result
527
    if params[:id].present?
528
      @issue = Issue.visible.find_by_id(params[:id]) 
529
      @candidates.delete(@issue)
530
    end
531

  
532
    render :inline => "<%= auto_complete_result_parent_issue( @candidates, @phrase) %>"
533
  end
461 534
  
462 535
private
463 536
  def find_issue
464 537
    @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
465 538
    @project = @issue.project
539
    @parent_issue = @issue.parent if @issue
540
  rescue ActiveRecord::RecordNotFound
541
    render_404
542
  end
543

  
544
  def find_parent_issue
545
    @parent_issue = Issue.find( params[:parent_issue_id])
546
    render_404 unless @parent_issue.visible?(User.current)
466 547
  rescue ActiveRecord::RecordNotFound
467 548
    render_404
468 549
  end
550

  
551
  def find_optional_parent_issue
552
    if params[:issue] && !params[:issue][:parent_id].blank?
553
      @parent_issue = Issue.visible.find_by_id( params[:issue][:parent_id])
554
    end
555
  end
469 556
  
470 557
  # Filter for bulk operations
471 558
  def find_issues
......
522 609
        end
523 610
        @query.group_by = params[:group_by]
524 611
        @query.column_names = params[:query] && params[:query][:column_names]
525
        session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
612
        if params[:view_options] and params[:view_options].is_a? Hash
613
          params[:view_options].each_pair do |name, value|
614
            @query.set_view_option( name, value)
615
          end
616
        end
617
        
618
        session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :view_options => @query.view_options}
526 619
      else
527 620
        @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
528 621
        @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
622
        if session[:query][:view_options]
623
          session[:query][:view_options].each_pair do |name, value|
624
            @query.set_view_option( name, value)
625
          end
626
        end
529 627
        @query.project = @project
530 628
      end
531 629
    end
532 630
  end
631

  
632
  # Retrive and build a query for the subissues
633
  def retrieve_query_for_subissues
634
    retrieve_query
635
    @query.project = @project
636
    @query.set_view_option('show_parents', ViewOption::SHOW_PARENTS[:organize_by])
637
    @query.column_names = Setting.subissues_list_columns
638
    sort_init( @query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
639
    sort_update({'id' => "#{Issue.table_name}.id"}.merge( @query.available_columns.inject({}) { |h, c| h[c.name.to_s] = c.sortable; h}))
640

  
641
  end
533 642
  
534 643
  # Rescues an invalid query statement. Just in case...
535 644
  def query_statement_invalid(exception)
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
app/controllers/queries_controller.rb
31 31
      @query.add_filter(field, params[:operators][field], params[:values][field])
32 32
    end if params[:fields]
33 33
    @query.group_by ||= params[:group_by]
34
    @query.view_options = params[:view_options] if params[:view_options]
34 35
    
35 36
    if request.post? && params[:confirm] && @query.save
36 37
      flash[:notice] = l(:notice_successful_create)
app/helpers/issues_helper.rb
96 96
      when 'estimated_hours'
97 97
        value = "%0.02f" % detail.value.to_f unless detail.value.blank?
98 98
        old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
99
      when 'parent_id'
100
        if detail.value && i = Issue.visible.find_by_id(detail.value)
101
          value = i.subject
102
        end
103

  
104
        if detail.old_value && i = Issue.visible.find_by_id(detail.old_value)
105
          old_value = i.subject
106
        end
107
        label = l(:field_parent_issue)
99 108
      end
100 109
    when 'cf'
101 110
      custom_field = CustomField.find_by_id(detail.prop_key)
......
196 205
    end
197 206
    export
198 207
  end
208

  
209
  def auto_complete_result_parent_issue(candidates, phrase)
210
    if candidates.present?
211
      candidates.map! do |c|
212
        content_tag("li",
213
                    highlight( c.to_s, phrase),
214
                    :id => String( c[:id]))
215
      end
216
    else
217
      candidates = [content_tag(:li, l(:label_none), :style => 'display:none')]
218
    end
219
    content_tag("ul", candidates.uniq)
220
  end
199 221
end
app/helpers/queries_helper.rb
27 27
                      content_tag('th', column.caption)
28 28
  end
29 29
  
30
  def column_content(column, issue)
30
  def column_content(column, issue, query = nil)
31 31
    value = column.value(issue)
32 32
    
33 33
    case value.class.name
34 34
    when 'String'
35 35
      if column.name == :subject
36
        link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
36
        subject_in_tree( issue, issue.subject, query)
37 37
      else
38 38
        h(value)
39 39
      end
......
61 61
      h(value)
62 62
    end
63 63
  end
64

  
65
  def subject_in_tree(issue, value, query)
66
    if query.view_options['show_parents'] == ViewOption::SHOW_PARENTS[:never]
67
      content_tag('div', subject_text(issue, value), :class=>'issue-subject')
68
    else
69
      css_style = "margin-left: #{issue.level}em;" # Used to indent
70
      content_tag('span',
71
                  content_tag('div',
72
                              subject_text(issue, value),
73
                              :class=>'issue-subject',
74
                              :style => css_style),
75
                  :class => issue.level > 0 ? "issue-subject-in-tree issue-level-#{issue.level}" : '',
76
                  :style => css_style)
77
    end
78
  end
79
  
80
  def subject_text(issue, value)
81
    if issue.visible?
82
      subject_text = link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
83
      h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + subject_text
84
    else
85
      h(value)
86
    end
87
  end
88

  
89
  def issue_content(issue, query, options = { })
90
    row_classes = ['issue','hascontextmenu', issue.css_classes, cycle('odd', 'even')]
91
    row_classes << 'issue-unfiltered' if options[:unfiltered]
92
    row_classes << 'issue-emphasis' if options[:emphasis]
93

  
94
    inner_content = returning '' do |content|
95
      content << content_tag(:td, check_box_tag("ids[]", issue.id, false, :id => nil), :class => 'checkbox')
96
      content << content_tag(:td, link_to(issue.id, :controller => 'issues', :action => 'show', :id => issue))
97

  
98
      query.columns.each do |column|
99
        content << content_tag( 'td', column_content(column, issue, query), :class => column.name)
100
      end
101
    end
102
    
103
    content_tag(:tr,
104
                inner_content,
105
                :id => "issue-#{issue.id}",
106
                :class => row_classes.join(' '))
107
  end
108

  
109
  def private_issue_content(issue, query, options = { })
110
    row_classes = ['issue', 'private-issue',cycle('odd', 'even')]
111
    row_classes << 'issue-unfiltered' if options[:unfiltered]
112
    row_classes << 'issue-emphasis' if options[:emphasis]
113

  
114
    inner_content = returning '' do |content|
115
      content << content_tag(:td, check_box_tag("ids[]", '', false, :id => nil), :class => 'checkbox')
116
      content << content_tag(:td, l(:text_private))
117

  
118
      query.columns.each do |column|
119
        if column.name == :subject
120
          # Need to indent
121
          content << content_tag('td', subject_in_tree(issue, l(:text_private), query), :class => column.name)
122
        else
123
          content << content_tag( 'td', l(:text_private), :class => column.name)
124
        end
125
      end
126
    end
127
    
128
    content_tag(:tr,
129
                inner_content,
130
                :id => "",
131
                :class => row_classes.join(' '))
132
    
133
  end
134

  
135
  def issues_family_content( parent, issues_to_show, query, emphasis_issues)
136
    html = ""
137
    if parent.visible?
138
      html << issue_content( parent, query, :unfiltered => !( issues_to_show.include? parent),
139
                             :emphasis => ( emphasis_issues ? emphasis_issues.include?( parent) : false))
140
    else
141
      html << private_issue_content( parent, query, :unfiltered => !( issues_to_show.include? parent),
142
                                     :emphasis => ( emphasis_issues ? emphasis_issues.include?( parent) : false))
143
    end
144
    unless  parent.children.empty?
145
      parent.children.each do |child|
146
        if issues_to_show.include?( child) || issues_to_show.detect { |i| i.ancestors.include? child }
147
          html << issues_family_content( child, issues_to_show, query, emphasis_issues)
148
        end
149
      end
150
    end
151
    html
152
  end
153

  
64 154
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 = 0)
49
    issues_on_current_level = issues.select { |i| i.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)
64
        end
65
        children_to_print = issues & issue.children
66
        children_to_print += issues.select { |i| i.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
74

  
47 75
end
app/models/issue.rb
36 36
  acts_as_customizable
37 37
  acts_as_watchable
38 38
  acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39
                     :include => [:project, :journals],
39
                     :include => [:project, :journals, :tracker],
40 40
                     # sort by id so that limited eager loading doesn't break with postgresql
41 41
                     :order_column => "#{table_name}.id"
42 42
  acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
......
46 46
  acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47
                            :author_key => :author_id
48 48

  
49
  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
49
  # Needs to be registered before any before_destroy in acts_as_nested_set
50
  before_destroy :move_children_to_root_before_destroy
51

  
52
  acts_as_nested_set
53

  
54
  # Patches to acts_as_nested_set since Issue already defines #move_to
55
  def move_to_left_of(node)
56
    nested_set_move_to node, :left
57
  end
58

  
59
  def move_to_right_of(node)
60
    nested_set_move_to node, :right
61
  end
62

  
63
  def move_to_child_of(node)
64
    nested_set_move_to node, :child
65
  end
66
  
67
  def move_to_root
68
    nested_set_move_to nil, :root
69
  end
70

  
71
  alias_method :nested_set_move_to, :move_to
50 72
  
73
  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
74

  
51 75
  validates_presence_of :subject, :priority, :project, :tracker, :author, :status
52 76
  validates_length_of :subject, :maximum => 255
53 77
  validates_inclusion_of :done_ratio, :in => 0..100
54 78
  validates_numericality_of :estimated_hours, :allow_nil => true
79
  validate :subtasks_validation
55 80

  
56 81
  named_scope :visible, lambda {|*args| { :include => :project,
57 82
                                          :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
......
60 85

  
61 86
  before_save :update_done_ratio_from_issue_status
62 87
  after_save :create_journal
88
  after_save :set_parent
89
  after_save :do_subtasks_hooks
63 90
  
64 91
  # Returns true if usr or current user is allowed to view the issue
65 92
  def visible?(usr=nil)
......
91 118
  # Returns the moved/copied issue on success, false on failure
92 119
  def move_to(new_project, new_tracker = nil, options = {})
93 120
    options ||= {}
94
    issue = options[:copy] ? self.clone : self
121
    issue = if options[:copy]
122
              Issue.new( self.attributes.reject { |k,v| k == 'parent_id' })
123
            else
124
              self
125
            end
126

  
95 127
    transaction do
96 128
      if new_project && issue.project_id != new_project.id
97 129
        # delete issue relations
98 130
        unless Setting.cross_project_issue_relations?
99 131
          issue.relations_from.clear
100 132
          issue.relations_to.clear
133

  
134
          issue.children.each(&:move_to_root) unless options[:copy]
101 135
        end
102 136
        # issue is moved to another project
103 137
        # reassign to the category with same name if any
......
129 163
          # Manually update project_id on related time entries
130 164
          TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
131 165
        end
166
        if new_project && issue.project_id != new_project.id && !Setting.cross_project_issue_relations?
167
          issue.move_to_root
168
        end
132 169
      else
133 170
        Issue.connection.rollback_db_transaction
134 171
        return false
......
136 173
    end
137 174
    return issue
138 175
  end
139
  
176

  
177
  # Cache awesome_nested_set's level attribute, it goes back to the
178
  # database and counts ancestors which can be expensive.
179
  def level
180
    unless @level
181
      @level = super
182
    end
183
    @level
184
  end
185

  
140 186
  def priority_id=(pid)
141 187
    self.priority = nil
142 188
    write_attribute(:priority_id, pid)
......
160 206
    self.attributes_without_tracker_first = new_attributes, *args
161 207
  end
162 208
  alias_method_chain :attributes=, :tracker_first
209

  
210
  # Need to define the setter because awesome_nested_set removes the
211
  # parent_id setter since parent is an internal field.  If parent
212
  # isn't set though, then parent changes will not be logged to journals.
213
  def parent_id=(pid)
214
    if pid != id
215
      write_attribute(:parent_id, pid)
216
    else
217
      false # Circular reference
218
    end
219
  end
220

  
221
  def estimated_hours
222
    if leaf?
223
      read_attribute(:estimated_hours)
224
    else
225
      children.inject(0) do |sum, issue|
226
        if issue.estimated_hours.present?
227
          sum + issue.estimated_hours
228
        else
229
          sum
230
        end
231
      end
232
    end
233
  end
234

  
235
  # Returns the estimated_hours, disregarding child issues
236
  def original_estimated_hours
237
    read_attribute(:estimated_hours)
238
  end
163 239
  
164 240
  def estimated_hours=(h)
165
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
241
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) if leaf?
242
  end
243

  
244
  def due_date
245
    if leaf?
246
      read_attribute( :due_date)
247
    else
248
      unless @due_date # cache, expensive operation
249
        dates = leaves.map(&:due_date)
250
        @due_date = dates.select {|d| d }.max if (dates && dates.any?)
251
      end
252
      @due_date
253
    end
254
  end  
255
  
256
  [ :due_date, :done_ratio ].each do |method|
257
    src = <<-END_SRC
258
      def #{method}=(value)
259
        write_attribute( :#{method}, value) if leaf?
260
      end
261
      END_SRC
262
    class_eval src, __FILE__, __LINE__
166 263
  end
167 264
  
168 265
  def done_ratio
169
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
170
      status.default_done_ratio
266
    if leaf? 
267
      if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
268
        status.default_done_ratio
269
      else
270
        read_attribute(:done_ratio)
271
      end
171 272
    else
172
      read_attribute(:done_ratio)
273
      unless @done_ratio # cache, expensive operation
274
        total_planned_days = leaves.inject(0) {|sum,i| sum + i.duration}
275

  
276
        if total_planned_days == 0
277
          @done_ratio = 0
278
        else
279
          total_actual_days = leaves.inject(0) {|sum,i| sum + i.actual_days}
280
          @done_ratio = (total_actual_days * 100 / total_planned_days).floor
281
        end
282
      end
283
      @done_ratio
173 284
    end
174 285
  end
175 286

  
......
182 293
  end
183 294
  
184 295
  def validate
185
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
296
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? && leaf?
186 297
      errors.add :due_date, :not_a_date
187 298
    end
188 299
    
......
231 342
    
232 343
    # Update start/due dates of following issues
233 344
    relations_from.each(&:set_issue_to_dates)
234
    
345

  
346
    # If target version is set, but "Due to" date is not, set
347
    # it as the same as the date of target version.
348
    if leaf? && due_date.nil? && fixed_version && fixed_version.due_date
349
      self.update_attribute :due_date, fixed_version.due_date
350
    end
351

  
235 352
    # Close duplicates if the issue was closed
236 353
    if @issue_before_change && !@issue_before_change.closed? && self.closed?
237 354
      duplicates.each do |duplicate|
......
256 373
    updated_on_will_change!
257 374
    @current_journal
258 375
  end
376

  
377
  def journal_initilized?
378
    @current_journal
379
  end
259 380
  
260 381
  # Return true if the issue is closed, otherwise false
261 382
  def closed?
262 383
    self.status.is_closed?
263 384
  end
385

  
386
  def open?
387
    !closed?
388
  end
264 389
  
265 390
  # Return true if the issue is being reopened
266 391
  def reopened?
......
359 484
  def soonest_start
360 485
    @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
361 486
  end
487

  
488
  # Returns the number of days that have been worked on this issue.
489
  # Calculated by using the duration of the issue (start/end dates)
490
  # and the done ratio
491
  def actual_days
492
    if done_ratio
493
      (duration * done_ratio / 100).floor
494
    else
495
      0
496
    end
497
  end
362 498
  
363 499
  def to_s
364 500
    "#{tracker} ##{id}: #{subject}"
......
388 524
    Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
389 525
  end
390 526

  
527
  def leaf?
528
    new_record? || (right - left == 1)
529
  end
530

  
391 531
  private
392 532
  
393 533
  # Update issues so their versions are not pointing to a
......
402 542
              :include => [:project, :fixed_version]
403 543
              ).each do |issue|
404 544
      next if issue.project.nil? || issue.fixed_version.nil?
405
      unless issue.project.shared_versions.include?(issue.fixed_version)
545
      unless issue.project.shared_versions.collect(&:id).include?(issue.fixed_version_id)
406 546
        issue.init_journal(User.current)
407 547
        issue.fixed_version = nil
408 548
        issue.save
......
424 564
  def create_journal
425 565
    if @current_journal
426 566
      # attributes changes
427
      (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
567
      skip_attrs = %w(id description lock_version created_on updated_on)
568
      skip_attrs += %w(due_date done_ratio estimated_hours) unless leaf?
569

  
570
      # attributes changes
571
      (Issue.column_names - skip_attrs).each {|c|
428 572
        @current_journal.details << JournalDetail.new(:property => 'attr',
429 573
                                                      :prop_key => c,
430 574
                                                      :old_value => @issue_before_change.send(c),
......
442 586
      @current_journal.save
443 587
    end
444 588
  end
589

  
590

  
591
  def move_children_to_root_before_destroy
592
    unless Setting.delete_children?
593
      children.each( &:move_to_root)
594
      reload_nested_set
595
    end
596
  end
597
  
598
  def do_subtasks_hooks
599
    if parent
600
      # Need to reload the Issues.  Using the association or
601
      # parent.reload was keeping the object readonly.
602
      parent_issue = Issue.find parent.id
603
      self.reload
604

  
605
      # Update the parent status if this issue is open and the parent
606
      # is closed
607
      if open? && parent_issue.closed?
608
        parent_issue.init_journal(User.current)
609
        parent_issue.status = IssueStatus.find_by_id(Setting.reopened_parent_issue_status) || IssueStatus.default
610
      end
611
      
612
      # Set 'Target version' of parent if one was set on one of the
613
      # children issue and parent have no 'Target version'. Do the same
614
      # if 'Target version of the parent issue lower (by the release
615
      # date or by the version number).
616
      if parent_issue.fixed_version.nil? && fixed_version or
617
          ( parent_issue.fixed_version && fixed_version and
618
            parent_issue.fixed_version.project == fixed_version.project and
619
            parent_issue.fixed_version < fixed_version )
620
        parent_issue.init_journal(User.current) unless parent_issue.journal_initilized?
621
        parent_issue.fixed_version = fixed_version
622
      end
623
      parent_issue.save if parent_issue.changed?
624
    end
625
  end
626

  
627
  def set_parent
628
    if (@issue_before_change && @issue_before_change.parent_id != parent_id) ||
629
        self.lock_version == 0 # Newly saved record
630
      if parent_id.present?
631
        parent_issue = Issue.visible.find_by_id(parent_id)
632
        move_to_child_of parent_issue if parent_issue
633
      else
634
        move_to_root
635
      end
636
    end
637
  end
638
  
639
  def subtasks_validation
640
    unless children.empty?
641
      if IssueStatus.find_by_id( @attributes['status_id']).is_closed? && children.detect { |i| !i.closed? }
642
        errors.add( :status, l(:error_issue_subtasks_cant_close_parent))
643
      end
644
      
645
      children_max_fixed_version = children.select { |i| i.fixed_version } .max { |a,b| a.fixed_version <=> b.fixed_version }
646
      if @attributes['fixed_version_id'] && children_max_fixed_version
647
        if Version.find_by_id( @attributes['fixed_version_id']) < children_max_fixed_version.fixed_version
648
          errors.add :fixed_version, l(:error_issue_subtasks_cant_select_lower_target_version)
649
        end
650
      end
651
    end
652
  end
653

  
445 654
end
app/models/query.rb
78 78
  serialize :filters
79 79
  serialize :column_names
80 80
  serialize :sort_criteria, Array
81
  serialize :view_options
81 82
  
82 83
  attr_protected :project_id, :user_id
83 84
  
......
135 136
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
136 137
  ]
137 138
  cattr_reader :available_columns
139

  
140
  @@available_view_options =
141
    [ ViewOption.new( 'show_parents',
142
                      [ [ l(:label_view_option_parents_do_not_show), 
143
                          ViewOption::SHOW_PARENTS[:never] ],
144
                        [ l(:label_view_option_parents_show_always), 
145
                          ViewOption::SHOW_PARENTS[:always] ],
146
                        [ l(:label_view_option_parents_show_and_group), 
147
                          ViewOption::SHOW_PARENTS[:organize_by]]])
148
    ]
149
  cattr_reader :available_view_options
150

  
138 151
  
139 152
  def initialize(attributes = nil)
140 153
    super attributes
141 154
    self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
155
    self.view_options ||=  { 'show_parents' => 'do_not_show' }
142 156
  end
143 157
  
144 158
  def after_initialize
......
470 484
  rescue ::ActiveRecord::StatementInvalid => e
471 485
    raise StatementInvalid.new(e.message)
472 486
  end
487

  
488
  def set_view_option( option, value)
489
    self.view_options[option] = value
490
    # Clear group_by if organize_by_parent is selected
491
    if option == 'show_parents' && value == 'organize_by_parent'
492
      self.group_by = nil
493
    end
494
  end
495

  
496
  def values_for_view_option( option)
497
    @@available_view_options.find { |vo| vo.name == option }.available_values
498
  end
499

  
500
  def caption_for_view_option( option)
501
    @@available_view_options.find { |vo| vo.name == option }.caption
502
  end
473 503
  
474 504
  private
475 505
  
app/models/version.rb
16 16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 17

  
18 18
class Version < ActiveRecord::Base
19
  include Comparable
20

  
19 21
  before_destroy :check_integrity
20 22
  after_update :update_issues_from_sharing_change
21 23
  belongs_to :project
app/models/view_option.rb
1
class ViewOption
2
  attr_accessor :name, :available_values
3
  include Redmine::I18n
4

  
5
  unless const_defined? :SHOW_PARENTS
6
    SHOW_PARENTS = { :never       => 'do_not_show',
7
                     :always      => 'show_always',
8
                     :organize_by => 'organize_by_parent'}.freeze
9
  end
10
  
11
  def initialize( name, available_values)
12
    self.name = name
13
    self.available_values = available_values
14
  end
15

  
16
  def caption
17
    l("label_view_option_#{name}")
18
  end
19
end
20

  
app/views/issues/_action_menu.rhtml
1 1
<div class="contextual">
2 2
<%= 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
<%= link_to_if_authorized(l(:button_add_subissue),
4
                          {
5
                          :controller => 'issues',
6
                          :action => 'add_subissue',
7
                          :project_id => @project,
8
                          :parent_issue_id => @issue.id
9
                        },
10
                        :class => 'icon icon-add') %>
3 11
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
4 12
<% replace_watcher ||= 'watcher' %>
5 13
<%= watcher_tag(@issue, User.current, {:id => replace_watcher, :replace => ['watcher','watcher2']}) %>
app/views/issues/_attributes.rhtml
31 31
</div>
32 32

  
33 33
<div class="splitcontentright">
34
<% if @issue.new_record? ||  @issue.leaf? %>	
34 35
<p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
35 36
<p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
36 37
<p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
37 38
<% if Issue.use_field_for_done_ratio? %>
38 39
<p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
39 40
<% end %>
41
<% else %>
42
<p><label><%= l(:field_start_date) %></label> <%= format_date(@issue.start_date) %></p>
43
<p><label><%= l(:field_due_date) %></label> <%= format_date(@issue.due_date) %></p>
44
<p><label><%= l(:field_done_ratio) %></label> <%= "#{@issue.done_ratio}%" %></p>
45
<% end %>
46
</div>
47

  
48
<div>
49
<%= render :partial => 'parent_field' %>
40 50
</div>
41 51

  
42 52
<div style="clear:both;"> </div>
app/views/issues/_list.rhtml
12 12
	</tr></thead>
13 13
	<% previous_group = false %>
14 14
	<tbody>
15
	<% issues.each do |issue| -%>
16
  <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
17
    <% reset_cycle %>
18
    <tr class="group open">
19
    	<td colspan="<%= query.columns.size + 2 %>">
20
    		<span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
21
      	<%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
22
    	</td>
23
		</tr>
24
		<% previous_group = group %>
25
  <% end %>
26
	<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
27
	    <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
28
		<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
29
        <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
30
	</tr>
31
	<% end -%>
15
      <% emphasis_issues ||= [] %>
16
      <% if query.view_options['show_parents'] == ViewOption::SHOW_PARENTS[:organize_by] -%>
17
        <%= render :partial => 'list_organized_by_parent', :locals => { :issues => issues, :query => query, :emphasis_issues => emphasis_issues }%>
18
      <% else %>
19
      <% issues.each do |issue| -%>
20
        <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
21
          <% reset_cycle %>
22
          <tr class="group open">
23
            <td colspan="<%= query.columns.size + 2 %>">
24
    		  <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
25
              <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
26
            </td>
27
          </tr>
28
          <% previous_group = group %>
29
        <% end %>
30
        <% if query.view_options['show_parents'] == ViewOption::SHOW_PARENTS[:always] -%>
31
          <% issue.ancestors.each do |parent_issue| -%>
32
            <% if parent_issue.visible? %>
33
              <%= issue_content( parent_issue, query, :unfiltered => true) %>
34
            <% else %>
35
              <%= private_issue_content( parent_issue, query, :unfiltered => true) %>
36
            <% end %>
37
          <% end -%>
38
        <% end %>
39
        <%= issue_content( issue, query, :emphasis => ( emphasis_issues ? emphasis_issues.include?( issue) : false)) %>
40
      <% end -%>
41
      <% end -%>
32 42
	</tbody>
33 43
</table>
34 44
<% end -%>
app/views/issues/_list_organized_by_parent.rhtml
1
<%-
2
parents_on_first_lvl = []
3
issues.each do |i|
4
  if i.parent
5
    first_parent = i.root
6
  else
7
    first_parent = i
8
  end
9
  parents_on_first_lvl += [ first_parent ] unless parents_on_first_lvl.include?( first_parent)
10
end
11

  
12
parents_on_first_lvl.each do |parent| -%>
13
<%= issues_family_content( parent, issues, query, emphasis_issues) %>
14
<% end -%>
app/views/issues/_parent_field.rhtml
1
<%= hidden_field_tag('issue[parent_id]', (@parent_issue ? @parent_issue.id : ""), :id => :issue_parent_id) %>
2
<p><label><%= l(:field_parent_issue) %></label>
3
<% if authorize_for( 'issues', 'auto_complete_for_issue_parent') %>
4
  <% if @parent_issue && @parent_issue.visible? %>
5
    <%= text_field_tag( 'parent_issue', '', :value => @parent_issue) %>
6
  <% else %>
7
    <%= text_field_tag( 'parent_issue', '', :value => @parent_issue ? l(:text_private) : '') %>
8
  <% end %>
9
  <%= link_to_function( "Remove", 'clearValues(["issue_parent_id", "parent_issue"])') %>
10

  
11
  <div id="parent_issue_candidates" class="autocomplete"></div>
12
  <%= javascript_tag "observeParentIssueField('#{url_for(:controller => :issues,
13
                                                         :action => :auto_complete_for_issue_parent,
14
                                                         :id => @issue.id,
15
                                                         :project_id => @project.id) }')" %>
16
<% else %>
17
  <%= @parent_issue || "-" %>
18
<% end %>
19
</p>
app/views/issues/_subissues_list.rhtml
1
<% if @issue.root.self_and_descendants.size > 1 %>
2
  <% content_for :header_tags do %>
3
    <%= javascript_include_tag 'context_menu' %>
4
    <%= stylesheet_link_tag 'context_menu' %>
5
  <% end %>
6
	<hr />
7
  <p><strong><%=l(:label_issues_hierarchy)%></strong></p>
8
  <div id="subissues">
9
		<%= render( :partial => 'issues/list',
10
								:locals => {
11
									:issues 				 => @issue.root.self_and_descendants,
12
									:emphasis_issues => [ @issue ],
13
									:query  				 => @query }) %>
14
  </div>
15
  <div id="context-menu" style="display: none;"></div>
16
  <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
17
<% end %>
app/views/issues/context_menu.rhtml
83 83
		<ul>
84 84
		<% (0..10).map{|x|x*10}.each do |p| -%>
85 85
		    <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post,
86
		                                  :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
86
		                                  :selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || !@issues.all? {|i| i.leaf? }) %></li>
87 87
		<% end -%>
88 88
		</ul>
89 89
	</li>
app/views/issues/index.rhtml
29 29
						<td><%= l(:field_group_by) %></td>
30 30
						<td><%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></td>
31 31
					</tr>
32
                    <% @query.view_options.each_key do |voption| -%>
33
                    <tr>
34
                      <td><%= @query.caption_for_view_option(voption) %></td>
35
                      <td><%= select_tag( "view_options[#{voption}]", options_for_select(@query.values_for_view_option(voption), @query.view_options[voption])) %></td>
36
                    </tr>
37
                    <% end %>
32 38
				</table>
33 39
			</div>
34 40
		</fieldset>
app/views/issues/show.rhtml
38 38
    <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
39 39
    <% end %>
40 40
</tr>
41
<% if !@issue.leaf? && @issue.original_estimated_hours %>
42
   <td colspan="2">&nbsp;</td>
43
   <th class="estimated-hours"><%=l(:field_original_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.original_estimated_hours) %></td>
44
<% end %>
41 45
<%= render_custom_fields_rows(@issue) %>
42 46
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
43 47
</table>
......
54 58

  
55 59
<%= link_to_attachments @issue %>
56 60

  
61
<%= render :partial => 'subissues_list' %>
62

  
57 63
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
58 64

  
59 65
<% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
app/views/projects/roadmap.rhtml
10 10
    <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
11 11

  
12 12
    <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
13
    <% issues.each do |i|
14
         issues += i.ancestors if i.child?
15
       end
16
       issues.uniq! %>
13 17
    <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
14 18
    <ul>
15
    <%- issues.each do |issue| -%>
16
        <li><%= link_to_issue(issue, :project => (@project != issue.project)) %></li>
17
    <%- end -%>
19
    <%= render_list_of_related_issues( issues, version) %>
18 20
    </ul>
19 21
    </fieldset>
20 22
    <% end %>
app/views/queries/_form.rhtml
22 22

  
23 23
<p><label for="query_group_by"><%= l(:field_group_by) %></label>
24 24
<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
25

  
26
<% @query.view_options.each_key do |voption| -%>
27
<p><label><%= @query.caption_for_view_option(voption) %></label>
28
<%= select_tag("view_options[#{voption}]", options_for_select(@query.values_for_view_option(voption), @query.view_options[voption])) %></p>
29
<% end %>
30

  
31
<p><label for="query_group_by"><%= l(:field_group_by) %></label>
32
<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
25 33
</div>
26 34

  
27 35
<fieldset><legend><%= l(:label_filter_plural) %></legend>
app/views/settings/_issues.rhtml
8 8
<p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
9 9

  
10 10
<p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
11

  
12
<p><%= setting_select :delete_children, [ [l(:general_text_Yes), "1" ], [l(:general_text_No),  "0" ] ] %></p>
13

  
14
<p><%= setting_select :reopened_parent_issue_status, [["", 0]] + IssueStatus.all(:order => 'position ASC').collect{|status| [status.name, status.id.to_s]} %></p>
11 15
</div>
12 16

  
13 17
<fieldset class="box settings"><legend><%= l(:setting_issue_list_default_columns) %></legend>
......
15 19
        Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
16 20
</fieldset>
17 21

  
22

  
23
<fieldset class="box settings"><legend><%= l(:setting_subissues_list_columns) %></legend>
24
<%= setting_multiselect(:subissues_list_columns, Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
25
</fieldset>
26

  
18 27
<%= submit_tag l(:button_save) %>
19 28
<% end %>
app/views/versions/show.rhtml
33 33
<%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
34 34

  
35 35
<% issues = @version.fixed_issues.find(:all,
36
                                       :include => [:status, :tracker, :priority],
37
                                       :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
36
                                       :include => [:status, :tracker, :priority, :fixed_version],
37
                                       :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
38
       issues ||= []
39
       issues.each do |i|
40
         issues += i.ancestors if i.child?
41
       end
42
       issues.uniq!
43
%>
38 44
<% if issues.size > 0 %>
39 45
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
40
<ul>
41
<% issues.each do |issue| -%>
42
    <li><%= link_to_issue(issue) %></li>
43
<% end -%>
44
</ul>
46
  <%= render_list_of_related_issues( issues, @version) %>
45 47
</fieldset>
46 48
<% end %>
47 49
</div>
config/locales/en.yml
165 165
  error_issue_done_ratios_not_updated: "Issue done ratios not updated."
166 166
  error_workflow_copy_source: 'Please select a source tracker or role'
167 167
  error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
168
  error_issue_subtasks_cant_close_parent: "Can't close parent issue while one of the children is still open."
169
  error_issue_subtasks_cant_select_lower_target_version: "Can't set target version of parent issue lower than any of the children."
168 170
  
169 171
  warning_attachments_not_saved: "{{count}} file(s) could not be saved."
170 172
  
......
264 266
  field_assignable: Issues can be assigned to this role
265 267
  field_redirect_existing_links: Redirect existing links
266 268
  field_estimated_hours: Estimated time
269
  field_original_estimated_hours: Original estimated time
267 270
  field_column_names: Columns
268 271
  field_time_zone: Time zone
269 272
  field_searchable: Searchable
......
276 279
  field_content: Content
277 280
  field_group_by: Group results by
278 281
  field_sharing: Sharing
282
  field_parent_issue: Child of
279 283
  
280 284
  setting_app_title: Application title
281 285
  setting_app_subtitle: Application subtitle
......
329 333
  setting_issue_done_ratio_issue_status: Use the issue status
330 334
  setting_start_of_week: Start calendars on
331 335
  setting_rest_api_enabled: Enable REST web service
336
  setting_subissues_list_columns: Columns for subissues list
337
  setting_delete_children: Delete children when parent destroyed
338
  setting_reopened_parent_issue_status: Status applied to parent when reopening
332 339
  
333 340
  permission_add_project: Create project
334 341
  permission_add_subprojects: Create subprojects
......
743 750
  label_api_access_key: API access key
744 751
  label_missing_api_access_key: Missing an API access key
745 752
  label_api_access_key_created_on: "API access key created {{value}} ago"
753
  label_view_option_parents_do_not_show: Never
754
  label_view_option_parents_show_always: Always
755
  label_view_option_parents_show_and_group: Organize by parent
756
  label_issues_hierarchy: Issues hierarchy
757
  label_view_option_show_parents: Show parents
746 758
  
747 759
  button_login: Login
748 760
  button_submit: Submit
......
787 799
  button_quote: Quote
788 800
  button_duplicate: Duplicate
789 801
  button_show: Show
802
  button_add_subissue: Add sub-issue
790 803
  
791 804
  status_active: active
792 805
  status_registered: registered
......
853 866
  text_wiki_page_destroy_children: "Delete child pages and all their descendants"
854 867
  text_wiki_page_reassign_children: "Reassign child pages to this parent page"
855 868
  text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
869
  text_private: Private
856 870
  
857 871
  default_role_manager: Manager
858 872
  default_role_developper: Developer
config/settings.yml
180 180
  default: ''
181 181
rest_api_enabled:
182 182
  default: 0
183
delete_children:
184
  default: 1
185
subissues_list_columns:
186
  serialized: true
187
  default:
188
  - id
189
  - subject
190
  - status
191
  - start_date
192
  - due_date
193
reopened_parent_issue_status:
194
  default: ''
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/20090115162652_add_default_value_of_view_optoins_queries.rb
1
class AddDefaultValueOfViewOptoinsQueries < 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_parent_id_lft_and_rgt.rb
1
class AddIssuesParentIdLftAndRgt < ActiveRecord::Migration
2

  
3
  def self.up
4
    add_column :issues, :parent_id, :integer, :default => nil
5
    add_column :issues, :lft, :integer
6
    add_column :issues, :rgt, :integer
7
  end
8

  
9
  def self.down
10
    remove_column :issues, :parent_id
11
    remove_column :issues, :lft
12
    remove_column :issues, :rgt
13
  end
14
end
db/migrate/20090406213899_issues_rebuild.rb
1
# Need to assume Issues are valid in order to rebuild.
2
class Issue < ActiveRecord::Base
3
  def valid?
4
    true
5
  end
6
end
7

  
8
class IssuesRebuild < ActiveRecord::Migration
9
  def self.up
10
    Issue.rebuild!
11
  end
12

  
13
  def self.down
14
  end
15
end
db/migrate/20091211204929_add_lft_rgt_indexes_to_issues.rb
1
class AddLftRgtIndexesToIssues < ActiveRecord::Migration
2
  def self.up
3
    add_index :issues, :lft
4
    add_index :issues, :rgt
5
  end
6

  
7
  def self.down
8
    remove_index :issues, :lft
9
    remove_index :issues, :rgt
10
  end
11
end
db/migrate/20091211205222_add_indexes_to_issues_parent_id.rb
1
class AddIndexesToIssuesParentId < ActiveRecord::Migration
2
  def self.up
3
    add_index :issues, :parent_id
4
  end
5

  
6
  def self.down
7
    remove_index :issues, :parent_id
8
  end
9
end
lib/redmine.rb
45 45
                                  :reports => :issue_report}
46 46
    map.permission :add_issues, {:issues => [:new, :update_form]}
47 47
    map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :update_form]}
48
    map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
48
    map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy], :issues => [:add_subissue, :auto_complete_for_issue_parent]}
49 49
    map.permission :add_issue_notes, {:issues => [:edit, :reply]}
50 50
    map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
51 51
    map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
... This diff was truncated because it exceeds the maximum size that can be displayed.