0001-YetAnotherUpdate-Of-Ported-the_redmine_subtasks-plugin_from_Aleksei_Guse.patch

ciaran jessup, 2010-02-03 22:25

Download (109 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, :update_form]
29

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

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

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

  
188 194
    if request.post?
195
      @issue.parent_id = params[:issue][:parent_id] if params[:issue] && params[:issue][:parent_id]
196
      
189 197
      @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
190 198
      @time_entry.attributes = params[:time_entry]
191 199
      if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.valid?
......
213 221
    attachments.each(&:destroy)
214 222
  end
215 223

  
224
  def add_subissue
225
    redirect_to :action => 'new',
226
                :project_id => @parent_issue.project,
227
                :issue => { :parent_id => @parent_issue.id }
228
  end
229

  
216 230
  def reply
217 231
    journal = Journal.find(params[:journal_id]) if params[:journal_id]
218 232
    if journal
......
370 384
                              :order => "start_date, effective_date",
371 385
                              :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]
372 386
                              )
387
      # Parent issues that might not have the due_date set but do have
388
      # child issues that do have due_date set should be included.
389
      events.each do |issue|
390
        if issue.leaf?
391
          # Can't use the Issue#visible named_scope because it causes
392
          # a SQL error with the awesome_nested_set
393
          ancestors = issue.ancestors.all(:include => [:tracker, :assigned_to, :priority, :project],
394
                                          :order => "start_date",
395
                                          :conditions => 'start_date IS NOT NULL')
396
          ancestors.map! {|i| i.visible? ? i : nil }.compact!
397

  
398
          events += ancestors.flatten if ancestors.present?
399
        end
400
      end
401

  
373 402
      # Versions
374 403
      events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
375
                                   
376
      @gantt.events = events
404

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

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

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

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

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

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

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

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

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

  
552
  def find_optional_parent_issue
553
    if params[:issue] && !params[:issue][:parent_id].blank?
554
      @parent_issue = Issue.visible.find_by_id( params[:issue][:parent_id])
555
    end
556
  end
470 557
  
471 558
  # Filter for bulk operations
472 559
  def find_issues
......
523 610
        end
524 611
        @query.group_by = params[:group_by]
525 612
        @query.column_names = params[:query] && params[:query][:column_names]
526
        session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
613
        if params[:view_options] and params[:view_options].is_a? Hash
614
          params[:view_options].each_pair do |name, value|
615
            @query.set_view_option( name, value)
616
          end
617
        end
618

  
619
        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}
527 620
      else
528 621
        @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
529 622
        @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
623
        if session[:query][:view_options]
624
          session[:query][:view_options].each_pair do |name, value|
625
            @query.set_view_option( name, value)
626
          end
627
        end
530 628
        @query.project = @project
531 629
      end
532 630
    end
533 631
  end
632

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

  
642
  end
534 643
  
535 644
  # Rescues an invalid query statement. Just in case...
536 645
  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)
......
161 207
  end
162 208
  alias_method_chain :attributes=, :tracker_first
163 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
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
        if Issue.use_status_for_done_ratio?
275
          total_done_ratio=children.inject(0) {|sum, i| sum + i.done_ratio}
276
          if total_done_ratio == 0 
277
            @done_ratio = 0
278
          else 
279
            @done_ratio = (total_done_ratio / children.size )
280
          end
281
        else 
282
          total_planned_days = leaves.inject(0) {|sum,i| sum + i.duration}
283
 
284
          if total_planned_days == 0
285
            @done_ratio = 0
286
          else
287
            total_actual_days = leaves.inject(0) {|sum,i| sum + i.actual_days}
288
            @done_ratio = (total_actual_days * 100 / total_planned_days).floor
289
          end
290
        end
291
      end
292
      @done_ratio
173 293
    end
174 294
  end
175 295

  
......
182 302
  end
183 303
  
184 304
  def validate
185
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
305
    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? && leaf?
186 306
      errors.add :due_date, :not_a_date
187 307
    end
188 308
    
......
231 351
    
232 352
    # Update start/due dates of following issues
233 353
    relations_from.each(&:set_issue_to_dates)
234
    
354

  
355
    # If target version is set, but "Due to" date is not, set
356
    # it as the same as the date of target version.
357
    if leaf? && due_date.nil? && fixed_version && fixed_version.due_date
358
      self.update_attribute :due_date, fixed_version.due_date
359
    end
360

  
235 361
    # Close duplicates if the issue was closed
236 362
    if @issue_before_change && !@issue_before_change.closed? && self.closed?
237 363
      duplicates.each do |duplicate|
......
256 382
    updated_on_will_change!
257 383
    @current_journal
258 384
  end
385

  
386
  def journal_initilized?
387
    @current_journal
388
  end
259 389
  
260 390
  # Return true if the issue is closed, otherwise false
261 391
  def closed?
262 392
    self.status.is_closed?
263 393
  end
394

  
395
  def open?
396
    !closed?
397
  end
264 398
  
265 399
  # Return true if the issue is being reopened
266 400
  def reopened?
......
359 493
  def soonest_start
360 494
    @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
361 495
  end
496

  
497
  # Returns the number of days that have been worked on this issue.
498
  # Calculated by using the duration of the issue (start/end dates)
499
  # and the done ratio
500
  def actual_days
501
    if done_ratio
502
      (duration * done_ratio / 100).floor
503
    else
504
      0
505
    end
506
  end
362 507
  
363 508
  def to_s
364 509
    "#{tracker} ##{id}: #{subject}"
......
388 533
    Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
389 534
  end
390 535

  
536
  def leaf?
537
    new_record? || (right - left == 1)
538
  end
539

  
391 540
  private
392 541
  
393 542
  # Update issues so their versions are not pointing to a
......
402 551
              :include => [:project, :fixed_version]
403 552
              ).each do |issue|
404 553
      next if issue.project.nil? || issue.fixed_version.nil?
405
      unless issue.project.shared_versions.include?(issue.fixed_version)
554
      unless issue.project.shared_versions.collect(&:id).include?(issue.fixed_version_id)
406 555
        issue.init_journal(User.current)
407 556
        issue.fixed_version = nil
408 557
        issue.save
......
424 573
  def create_journal
425 574
    if @current_journal
426 575
      # attributes changes
427
      (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
576
      skip_attrs = %w(id description lock_version created_on updated_on)
577
      skip_attrs += %w(due_date done_ratio estimated_hours) unless leaf?
578

  
579
      # attributes changes
580
      (Issue.column_names - skip_attrs).each {|c|
428 581
        @current_journal.details << JournalDetail.new(:property => 'attr',
429 582
                                                      :prop_key => c,
430 583
                                                      :old_value => @issue_before_change.send(c),
......
442 595
      @current_journal.save
443 596
    end
444 597
  end
598

  
599

  
600
  def move_children_to_root_before_destroy
601
    unless Setting.delete_children?
602
      children.each( &:move_to_root)
603
      reload_nested_set
604
    end
605
  end
606

  
607
  def do_subtasks_hooks
608
    if parent
609
      # Need to reload the Issues.  Using the association or
610
      # parent.reload was keeping the object readonly.
611
      parent_issue = Issue.find parent.id
612
      self.reload
613

  
614
      # Update the parent status if this issue is open and the parent
615
      # is closed
616
      if open? && parent_issue.closed?
617
        parent_issue.init_journal(User.current)
618
        parent_issue.status = IssueStatus.find_by_id(Setting.reopened_parent_issue_status) || IssueStatus.default
619
      end
620

  
621
      # Set 'Target version' of parent if one was set on one of the
622
      # children issue and parent have no 'Target version'. Do the same
623
      # if 'Target version of the parent issue lower (by the release
624
      # date or by the version number).
625
      if parent_issue.fixed_version.nil? && fixed_version or
626
          ( parent_issue.fixed_version && fixed_version and
627
            parent_issue.fixed_version.project == fixed_version.project and
628
            parent_issue.fixed_version < fixed_version )
629
        parent_issue.init_journal(User.current) unless parent_issue.journal_initilized?
630
        parent_issue.fixed_version = fixed_version
631
      end
632
      parent_issue.save if parent_issue.changed?
633
    end
634
  end
635

  
636
  def set_parent
637
    if (@issue_before_change && @issue_before_change.parent_id != parent_id) ||
638
        self.lock_version == 0 # Newly saved record
639
      if parent_id.present?
640
        parent_issue = Issue.visible.find_by_id(parent_id)
641
        move_to_child_of parent_issue if parent_issue
642
      else
643
        move_to_root
644
      end
645
    end
646
  end
647

  
648
  def subtasks_validation
649
    unless children.empty?
650
      if IssueStatus.find_by_id( @attributes['status_id']).is_closed? && children.detect { |i| !i.closed? }
651
        errors.add( :status, l(:error_issue_subtasks_cant_close_parent))
652
      end
653

  
654
      children_max_fixed_version = children.select { |i| i.fixed_version } .max { |a,b| a.fixed_version <=> b.fixed_version }
655
      if @attributes['fixed_version_id'] && children_max_fixed_version
656
        if Version.find_by_id( @attributes['fixed_version_id']) < children_max_fixed_version.fixed_version
657
          errors.add :fixed_version, l(:error_issue_subtasks_cant_select_lower_target_version)
658
        end
659
      end
660
    end
661
  end
662

  
445 663
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_url => @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.