subissues-v1.73.diff

sub-issues patch, v1.73 - Aleksei Gusev, 2009-02-05 15:22

Download (66.5 KB)

View differences:

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

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

  
47
  def auto_complete_for_issue_parent
48
    @phrase = params[:issue_parent]
49
    @candidates = []
50

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

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

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

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

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

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

  
140 187
    if request.get? || request.xhr?
141 188
      @issue.start_date ||= Date.today
142 189
    else
......
144 191
      # Check that the user is allowed to apply the requested status
145 192
      @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
146 193
      if @issue.save
194
        set_parent( @issue, params[:issue_parent_issue_id]) if params[:issue_parent_issue_id]
147 195
        attach_files(@issue, params[:attachments])
148 196
        flash[:notice] = l(:notice_successful_create)
149 197
        Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
......
156 204
    @priorities = Enumeration::get_values('IPRI')
157 205
    render :layout => !request.xhr?
158 206
  end
207

  
208
  def add_subissue
209
    if params[:issue_parent_issue_id].nil?
210
      flash.now[:error] = 'No parent issue specified.'
211
      render :nothing => true, :layout => true
212
      return
213
    else
214
      redirect_to( :controller => 'issues', :action => 'new',
215
                   :issue_parent_issue_id => params[:issue_parent_issue_id])
216
      return
217
    end
218
  end
159 219
  
160 220
  # Attributes that can be updated on workflow transition (without :edit permission)
161 221
  # TODO: make it configurable (at least per role)
......
186 246
      call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
187 247

  
188 248
      if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
249
        set_parent(@issue, params[:issue_parent_issue_id]) if params[:issue_parent_issue_id]
189 250
        # Log spend time
190 251
        if current_role.allowed_to?(:log_time)
191 252
          @time_entry.save
......
248 309
        call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
249 310
        # Don't save any change to the issue if the user is not authorized to apply the requested status
250 311
        if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
312
          set_parent(issue, params[:issue_parent_issue_id]) unless params[:issue_parent_issue_id].blank?
251 313
          # Send notification for each issue (if changed)
252 314
          Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
253 315
        else
......
412 474

  
413 475
  def update_form
414 476
    @issue = Issue.new(params[:issue])
477
    @parent_issue = @issue.parent
415 478
    render :action => :new, :layout => false
416 479
  end
417 480
  
......
425 488
private
426 489
  def find_issue
427 490
    @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
491
    @parent_issue = @issue.parent
428 492
    @project = @issue.project
429 493
  rescue ActiveRecord::RecordNotFound
430 494
    render_404
......
481 545
            @query.add_short_filter(field, params[field]) if params[field]
482 546
          end
483 547
        end
484
        session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
548
        if params[:view_options] and params[:view_options].is_a? Hash
549
          params[:view_options].each_pair do |name, value|
550
            @query.set_view_option( name, value)
551
          end
552
        end
553
        session[:query] = {
554
          :project_id => @query.project_id,
555
          :filters => @query.filters,
556
          :view_options => @query.view_options
557
        }
485 558
      else
486 559
        @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
487
        @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
560
        @query ||= Query.new(:name => "_",
561
                             :project => @project,
562
                             :filters => session[:query][:filters],
563
                             :view_options => session[:query][:view_options])
488 564
        @query.project = @project
489 565
      end
490 566
    end
suissues-patch/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
......
293 295
      @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
294 296
    end
295 297
  end
298
  
299
  
300
  def sort_as_tree(issues)
301
    issues.sort!{|a,b| a.hierarchical_level <=> b.hierarchical_level}
302
    @sorted_issues = []
303
    issues.each do |issue|
304
      if @sorted_issues.empty?
305
        @sorted_issues << issue
306
        next
307
      end
308
      @time_to_stop = false #indicates when this task reaches its parent task (important because it has to stop between its parent task and the next aunt task
309
      @sorted_issues.each do |sorted_issue|
310
        #if same parent and smaller date, stop; if same parent, same date and smaller id, stop; after parent and before next parent, stop; 
311
        if ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date > issue.start_date)) ||
312
         ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date == issue.start_date) && (sorted_issue.id > issue.id)) ||
313
         (@time_to_stop && (sorted_issue.hierarchical_level < issue.hierarchical_level))
314
          @sorted_issues.insert(@sorted_issues.index(sorted_issue), issue)
315
          break
316
        end
317
        @time_to_stop = true if sorted_issue == issue.parent      
318
      end
319
      #if this issue's parent is the last element
320
      @sorted_issues << issue if @time_to_stop
321
    end
322
    @sorted_issues
323
  end
324
  
325
  #assumes that first level issues are ordered by date (sort_as_tree)
326
  def integrate_versions_with_issues_tree(issues, versions)
327
    versions.sort! {|x,y| x.start_date <=> y.start_date }    
328
    versions.each do |version|
329
      issues << version if issues.empty?
330
      issues.each do |issue|
331
        if ((issue.is_a? Issue && issue.root?) || (issue.is_a? Version)) && version.start_date < issue.start_date
332
          #insert version before a root task or another version whose date is immediately after this task's one 
333
          issues.insert(issues.index(issue), version)
334
        elsif issue == issues.last
335
          issues << version        
336
        end
337
      end
338
    end
339
    issues
340
  end  
341
  
296 342
end
suissues-patch/app/controllers/queries_controller.rb
30 30
    params[:fields].each do |field|
31 31
      @query.add_filter(field, params[:operators][field], params[:values][field])
32 32
    end if params[:fields]
33
    
33

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

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

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

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

  
23 29
  def render_issue_tooltip(issue)
24 30
    @cached_label_start_date ||= l(:field_start_date)
......
193 199
    export.rewind
194 200
    export
195 201
  end
202
  
203
  def set_parent(issue, parent_id)
204
    if (issue && parent_id.to_s.size > 0)
205
      issue.relations_from.each do |relation|
206
        if relation.relation_type == IssueRelation::TYPE_PARENTS
207
          relation.destroy
208
        end
209
      end
210
      issue.reload
211
      IssueRelation.create do |relation|
212
        relation.issue_from = issue
213
        relation.issue_to = Issue.find(parent_id)
214
        relation.relation_type = IssueRelation::TYPE_PARENTS
215
        unless relation.save
216
          flash[:error] = "Can't set ##{parent_id} as parent for the issue."
217
        end
218
      end unless parent_id == '0'
219
    end    
220
  end
221
  
222
  def auto_complete_result_parent_issue(candidates, phrase)
223
    return "" if candidates.empty?
224
    candidates.map! do |c|
225
      content_tag("li", highlight( c.to_s, phrase), :id => String( c[:id]))
226
    end
227
    content_tag("ul", candidates.uniq)
228
  end
196 229
end
suissues-patch/app/helpers/queries_helper.rb
1
# -*- coding: mule-utf-8 -*-
1 2
# redMine - project management software
2 3
# Copyright (C) 2006-2007  Jean-Philippe Lang
3 4
#
......
27 28
                      content_tag('th', column.caption)
28 29
  end
29 30
  
30
  def column_content(column, issue)
31
  def column_content(column, issue, query)
31 32
    if column.is_a?(QueryCustomFieldColumn)
32 33
      cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 34
      show_value(cv)
......
40 41
      else
41 42
        case column.name
42 43
        when :subject
43
        h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
44
          link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
44
          subject_in_tree(issue, value, query)
45 45
        when :done_ratio
46 46
          progress_bar(value, :width => '80px')
47 47
        when :fixed_version
......
52 52
      end
53 53
    end
54 54
  end
55
  
56
  def subject_in_tree(issue, value, query)
57
    case query.view_options['show_parents']
58
    when Query::VIEW_OPTIONS_SHOW_PARENTS_NEVER
59
      content_tag('div', subject_text(issue, value), :class=>'issue-subject')
60
    else
61
      content_tag('span', content_tag('div', subject_text(issue, value), :class=>'issue-subject'), :class=>"issue-subject-level-#{issue.hierarchical_level}")
62
    end
63
  end
64
  
65
  def subject_text(issue, value)
66
    subject_text = link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
67
    h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + subject_text
68
  end
69

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

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

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

  
28 28
  has_many :journals, :as => :journalized, :dependent => :destroy
29 29
  has_many :time_entries, :dependent => :delete_all
30 30
  has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
  
32 32
  has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33
  has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33
  has_many :relations_to,   :class_name => 'IssueRelation', :foreign_key => 'issue_to_id',   :dependent => :delete_all
34 34
  
35 35
  acts_as_attachable :after_remove => :attachment_removed
36 36
  acts_as_customizable
......
118 118
    return issue
119 119
  end
120 120
  
121
  def priority_id=(pid)
122
    self.priority = nil
123
    write_attribute(:priority_id, pid)
124
  end
125
  
126 121
  def estimated_hours=(h)
127 122
    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
128 123
  end
......
139 134
    if start_date && soonest_start && start_date < soonest_start
140 135
      errors.add :start_date, :activerecord_error_invalid
141 136
    end
137

  
138
    if IssueStatus.find_by_id( @attributes['status_id']).is_closed? && children.find { |i| !i.closed? }
139
      errors.add :status, "Can't close parent issue while on of the children still is open."
140
    end
141

  
142
    unless children.empty?
143
      children_max_fixed_version = children.select { |i| i.fixed_version } .max { |a,b| a.fixed_version <=> b.fixed_version }
144
      if @attributes['fixed_version_id'] && children_max_fixed_version
145
        if Version.find_by_id( @attributes['fixed_version_id']) < children_max_fixed_version.fixed_version
146
          errors.add :fixed_version, "Can't set target version of parent issue lower than any of the children."
147
        end
148
      end
149
    end
142 150
  end
143 151
  
144 152
  def validate_on_create
......
159 167
        @current_journal.details << JournalDetail.new(:property => 'attr',
160 168
                                                      :prop_key => c,
161 169
                                                      :old_value => @issue_before_change.send(c),
162
                                                      :value => send(c)) unless send(c)==@issue_before_change.send(c)
170
                                                      :value => send(c)) unless send(c)==@issue_before_change.send(c) || (!self.edge? && %w(status_id priority_id fixed_version_id start_date due_date done_ratio estimated_hours).include?(c))
163 171
      }
164 172
      # custom fields changes
165 173
      custom_values.each {|c|
......
172 180
      }      
173 181
      @current_journal.save
174 182
    end
183

  
184
    # If target version is set, but "Due to" date is not, set it as
185
    # the same as the date of target version.
186
    if self.fixed_version && self.due_date.nil?
187
      self.due_date = self.fixed_version.due_date if self.fixed_version.due_date
188
    end
189

  
175 190
    # Save the issue even if the journal is not saved (because empty)
176 191
    true
177 192
  end
......
183 198
    # Update start/due dates of following issues
184 199
    relations_from.each(&:set_issue_to_dates)
185 200
    
201
    # Set default status of parent if new status openes the issue.
202
    relations_from.each do |relation|
203
      if relation.relation_type == IssueRelation::TYPE_PARENTS
204
        relation.set_issue_to_default_status
205
        relation.set_issue_to_target_version
206
      end
207
    end
208

  
186 209
    # Close duplicates if the issue was closed
187 210
    if @issue_before_change && !@issue_before_change.closed? && self.closed?
188 211
      duplicates.each do |duplicate|
......
195 218
        duplicate.update_attribute :status, self.status
196 219
      end
197 220
    end
221

  
198 222
  end
199
  
223

  
200 224
  def init_journal(user, notes = "")
201 225
    @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
202 226
    @issue_before_change = self.clone
......
208 232
    @current_journal
209 233
  end
210 234
  
235
  def priority_id=(pid)
236
    self.priority = nil
237
    write_attribute(:priority_id, pid)
238
  end
239

  
211 240
  # Return true if the issue is closed, otherwise false
212 241
  def closed?
213 242
    self.status.is_closed?
......
264 293
  # Returns the due date or the target due date if any
265 294
  # Used on gantt chart
266 295
  def due_before
267
    due_date || (fixed_version ? fixed_version.effective_date : nil)
296
    due_date || (fixed_version ? fixed_version.effective_date : 0)
297
  end
298
  
299
  def duration1
300
    (start_date && due_date) ? (due_date - start_date + 1) : 0
268 301
  end
269 302
  
270 303
  def duration
......
275 308
    @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
276 309
  end
277 310
  
311
  def self.visible_by(usr)
312
    with_scope(:find => { :conditions => Project.visible_by(usr) }) do
313
      yield
314
    end
315
  end
316
  
317
  def done_ratio
318
    if children?
319
      @total_planned_days ||= 0
320
      @total_actual_days ||= 0
321
      children.each do |child| # from every subtask get the total number of days and the number of days already "worked"
322
        planned_days = child.duration1
323
        actual_days = child.done_ratio ?  (planned_days * child.done_ratio / 100).floor : 0
324
        @total_planned_days += planned_days
325
        @total_actual_days += actual_days
326
      end
327
      @total_done_ratio = @total_planned_days != 0 ? (@total_actual_days * 100 / @total_planned_days).floor : 0
328
    else
329
      read_attribute(:done_ratio)
330
    end
331
  end
332
  
333
  def estimated_hours
334
    if children?
335
      is_set = false
336
      children.each do |child|
337
        if child.estimated_hours
338
          if is_set
339
            @est_hours += child.estimated_hours
340
          else
341
            @est_hours = child.estimated_hours
342
            is_set = true
343
          end
344
        end     
345
      end
346
      @est_hours
347
    else
348
      read_attribute(:estimated_hours)
349
    end
350
  end
351
  
352
  def start_date
353
    calculate 'start_date'
354
  end  
355
  
356
  def due_date
357
    calculate 'due_date'
358
  end  
359
  
360
  def calculate(field)
361
    if children?
362
      @value = eval "children.first.#{field}" 
363
      children.each do |child|        
364
        case field
365
          when 'start_date'
366
          if child.start_date && (!@value || @value > child.start_date)
367
            @value = child.start_date
368
          end
369
          when 'due_date'
370
          if child.due_date && (!@value || @value < child.due_date)
371
            @value = child.due_date
372
          end
373
        end        
374
      end
375
      @value
376
    else
377
      read_attribute(eval(":#{field}"))
378
    end
379
  end  
380

  
381
  def children?
382
    children != []
383
  end
384
  
385
  def children
386
    children = []
387
    relations_to.each do |relation|
388
      if relation.relation_type == IssueRelation::TYPE_PARENTS
389
        children << relation.other_issue(self)
390
      end
391
    end
392
    children
393
  end
394
  
395
  def parent
396
    relations_from.each do |relation|
397
      if relation.relation_type == IssueRelation::TYPE_PARENTS
398
        return parent = relation.other_issue(self)
399
      end
400
    end
401
    return nil
402
  end
403

  
404
  def parents_hierarchy
405
    return [] unless p = parent
406
    parents = [ p ]
407
    while p = p.parent
408
      parents += [ p ]
409
    end
410
    parents
411
  end
412

  
413
  def parent?
414
    parent != nil
415
  end 
416
  
417
  def root?
418
    !parent?
419
  end
420
  
421
  def ancestors(issue=self)
422
    a = []
423
    return a if ! issue.parent?
424
    (a << issue.parent) | ancestors(issue.parent)
425
  end
426
  
427
  #First level tasks have hierarchical level = 1 and so on
428
  def hierarchical_level(issue=self)
429
    issue.parent? ? (1 + hierarchical_level(issue.parent)) : 1
430
  end
431
  
432
  def edge?
433
    relations_to.each do |relation|
434
      if relation.relation_type == IssueRelation::TYPE_PARENTS
435
        return false
436
      end
437
    end
438
    return true
439
  end
440

  
441
  def orphan?
442
    root? && edge?
443
  end
444
  
445
  def self.find_with_parents( *args)
446
    issues = find( *args)
447
    return [] if issues.empty?
448
    issues.each do |i|
449
      while not i.root?
450
        issues += [ i.parent ]
451
        i = i.parent
452
      end
453
    end
454
    issues.uniq
455
  end
456
  
278 457
  def to_s
279 458
    "#{tracker} ##{id}: #{subject}"
280 459
  end
suissues-patch/app/models/issue_relation.rb
17 17

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

  
40
  # Only one parent allowed.
41
  validates_uniqueness_of( :relation_type,
42
                           :scope   => [:issue_from_id, :relation_type],
43
                           :message => l(:error_issue_can_have_only_one_parent),
44
                           :if      => Proc.new { |issue_relation| issue_relation.relation_type == TYPE_PARENTS })
37 45
  
38 46
  attr_protected :issue_from_id, :issue_to_id
39 47
  
......
59 67
    else
60 68
      self.delay = nil
61 69
    end
70

  
71
    # Set new status to parent if new status openes the issue.
72
    if TYPE_PARENTS == relation_type
73
      set_issue_to_default_status
74
      set_issue_to_target_version
75
    end
76

  
62 77
    set_issue_to_dates
63 78
  end
79

  
80
  def set_issue_to_default_status
81
    if issue_to.closed? && !issue_from.closed?
82
      issue_to.update_attribute :status, IssueStatus.default
83
    end
84
  end
85

  
86
  def set_issue_to_target_version
87
    if issue_to.fixed_version.nil? && issue_from.fixed_version or
88
        ( issue_to.fixed_version && issue_from.fixed_version and
89
          issue_to.fixed_version.project == issue_from.fixed_version.project and
90
          issue_to.fixed_version < issue_from.fixed_version )
91
      issue_to.update_attribute :fixed_version, issue_from.fixed_version
92
    end
93
  end
64 94
  
65 95
  def set_issue_to_dates
66 96
    soonest_start = self.successor_soonest_start
suissues-patch/app/models/query.rb
48 48
  end
49 49
end
50 50

  
51
class ViewOption
52
  attr_accessor :name, :available_values
53
  include GLoc
54
  
55
  def initialize( name, available_values)
56
    self.name = name
57
    self.available_values = available_values
58
  end
59

  
60
  def caption
61
    set_language_if_valid( User.current.language)
62
    l("label_view_option_#{name}")
63
  end
64
end
65

  
51 66
class Query < ActiveRecord::Base
52 67
  belongs_to :project
53 68
  belongs_to :user
54 69
  serialize :filters
55 70
  serialize :column_names
71
  serialize :view_options
56 72
  
57 73
  attr_protected :project_id, :user_id
58 74
  
......
109 125
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 126
  ]
111 127
  cattr_reader :available_columns
128

  
129
  VIEW_OPTIONS_SHOW_PARENTS_NEVER              = 'do_not_show'
130
  VIEW_OPTIONS_SHOW_PARENTS_ALWAYS             = 'show_always'
131
  VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT = 'organize_by_parent'
132

  
133
  @@available_view_options = [
134
    ViewOption.new( 'show_parents', [ [ l(:label_view_option_parents_do_not_show), VIEW_OPTIONS_SHOW_PARENTS_NEVER ],
135
                                      [ l(:label_view_option_parents_show_always), VIEW_OPTIONS_SHOW_PARENTS_ALWAYS ],
136
                                      [ l(:label_view_option_parents_show_and_group), VIEW_OPTIONS_SHOW_PARENTS_ORGANIZE_BY_PARENT ] ])
137
  ]
138
  cattr_reader :available_view_options
112 139
  
113 140
  def initialize(attributes = nil)
114 141
    super attributes
115 142
    self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
143
    self.view_options ||=  { 'show_parents' => 'do_not_show' }
116 144
    set_language_if_valid(User.current.language)
117 145
  end
118 146
  
......
317 345
    
318 346
    (filters_clauses << project_statement).join(' AND ')
319 347
  end
320
  
348

  
349
  def set_view_option( option, value)
350
    self.view_options[option] = value
351
  end
352

  
353
  def values_for_view_option( option)
354
    @@available_view_options.find { |vo| vo.name == option }.available_values
355
  end
356

  
357
  def caption_for_view_option( option)
358
    @@available_view_options.find { |vo| vo.name == option }.caption
359
  end
360

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

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

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

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

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

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

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

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

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

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

  
34
<% issues = @version.fixed_issues.find(:all,
35
                                       :include => [:status, :tracker],
36
                                       :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
37
<% if issues.size > 0 %>
34
<% if @issues.size > 0 %>
38 35
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
39
<ul>
40
<% issues.each do |issue| -%>
41
    <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
42
<% end -%>
43
</ul>
36
<%= render_list_of_related_issues( @issues, @version) %>
44 37
</fieldset>
45 38
<% end %>
46 39
</div>
suissues-patch/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
suissues-patch/db/migrate/20090121172432_add_default_value_of_view_option_queries.rb
1
class AddDefaultValueOfViewOptionQueries < ActiveRecord::Migration
2
  def self.up
3
    Query.find(:all).each do |q|
4
      q.view_options ||= { 'show_parents' => 'do_not_show' }
5
      q.save!
6
    end
7
  end
8

  
9
  def self.down
10
  end
11
end
suissues-patch/lang/en.yml
84 84
error_scm_command_failed: "An error occurred when trying to access the repository: %s"
85 85
error_scm_annotate: "The entry does not exist or can not be annotated."
86 86
error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
87
error_issue_can_have_only_one_parent: allows only one relation (a task can have only one parent).
87 88

  
88 89
warning_attachments_not_saved: "%d file(s) could not be saved."
89 90

  
......
187 188
field_comments_sorting: Display comments
188 189
field_parent_title: Parent page
189 190
field_editable: Editable
191
field_calendar_firstday: First day of week
192
field_parent: Subproject of
193
field_parent_issue: Child of
194
field_parent_title: Parent page
190 195

  
191 196
setting_app_title: Application title
192 197
setting_app_subtitle: Application subtitle
......
587 592
label_issue_watchers: Watchers
588 593
label_example: Example
589 594
label_display: Display
595
label_children: parent of
596
label_parents: child of
597
label_view_option_parents_do_not_show: Never
598
label_view_option_parents_show_always: Always
599
label_view_option_parents_show_and_group: Organize by parent
600
label_view_option_show_parents: Show parents
590 601

  
591 602
button_login: Login
592 603
button_submit: Submit
......
627 638
button_update: Update
628 639
button_configure: Configure
629 640
button_quote: Quote
641
button_add_subissue: Add sub-issue
630 642

  
631 643
status_active: active
632 644
status_registered: registered
suissues-patch/lib/redmine.rb
35 35
                                  :queries => :index,
36 36
                                  :reports => :issue_report}, :public => true                    
37 37
    map.permission :add_issues, {:issues => :new}
38
    map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
39
    map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
38
    map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :update_subject]}
39
    map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy], :issues => :add_subissue}
40 40
    map.permission :add_issue_notes, {:issues => [:edit, :reply]}
41 41
    map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
42 42
    map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
suissues-patch/lib/redmine/version.rb
23 23
     	  if entries.match(%r{^\d+})
24 24
     	    revision = $1.to_i if entries.match(%r{^\d+\s+dir\s+(\d+)\s})
25 25
     	  else
26
   	        xml = REXML::Document.new(entries)
27
   	        revision = xml.elements['wc-entries'].elements[1].attributes['revision'].to_i
28
   	      end
29
   	    rescue
30
   	      # Could not find the current revision
31
   	    end
32
 	  end
33
 	  revision
26
            xml = REXML::Document.new(entries)
27
            revision = xml.elements['wc-entries'].elements[1].attributes['revision'].to_i
28
          end
29
        rescue
30
          # Could not find the current revision
31
        end
32
      end
33
      revision
34
    end
35

  
36
    def self.warecorp_revision
37
      begin
38
        tag = %x{ git name-rev --tags `git log -1 --pretty=format:'%H'` }.split[1]
39
        tag = ( tag.match %r{tags/(.*)})[1]
40
      rescue
41
        tag = "undefined"
42
      end
43
      tag
34 44
    end
35 45

  
36 46
    REVISION = self.revision
47
    WARECORP_REVISION = self.warecorp_revision
37 48
    ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact
38
    STRING = ARRAY.join('.')
49
    STRING = ARRAY.join('.') + " / #{WARECORP_REVISION}"
39 50
    
40 51
    def self.to_a; ARRAY end
41 52
    def self.to_s; STRING end    
suissues-patch/public/stylesheets/application.css
704 704
  #main { background: #fff; }
705 705
  #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
706 706
}
707

  
708
/***** Subtasks *****/
709
/*blocks composed of icon (+, - or none), project identification (if there is one) and link to the task*/
710
.issue-subject-level-1, .issue-subject-level-1 .issue-subject { 
711
    margin-left: 0em;
712
}
713

  
714
.issue-subject-level-2, .issue-subject-level-2 .issue-subject { 
715
    margin-left: 1em;
716
}
717

  
718
.issue-subject-level-3, .issue-subject-level-3 .issue-subject { 
719
    margin-left: 2em;
720
}
721

  
722
.issue-subject-level-4, .issue-subject-level-4 .issue-subject { 
723
    margin-left: 3em;
724
}
725

  
726
.issue-subject-level-5, .issue-subject-level-5 .issue-subject { 
727
    margin-left: 4em;
728
}
729

  
730
.issue-subject-level-2, .issue-subject-level-2 .issue-subject, .issue-subject-level-3, .issue-subject-level-3 .issue-subject, .issue-subject-level-4, .issue-subject-level-4 .issue-subject, .issue-subject-level-5, .issue-subject-level-5 .issue-subject { 
731
    background-image: url(../images/corner-dots.gif);
732
    background-repeat: no-repeat;
733
    background-position: -3px center;
734
}
735

  
736
/* Used to show issues which is not pass by filter, but should by
737
   shown as parent for other issue. */
738
.issue-unfiltered {
739
    opacity: 0.5;
740
    filter: alpha(opacity=50);
741
}
742

  
743
.expanded-issue {
744
	background-image: url(contract.png);
745
	background-repeat: no-repeat;
746
}
747

  
748
.contracted-issue {
749
	background-image: url(expand.png);
750
	background-repeat: no-repeat;
751
}
752

  
753
.expand-icon, .contract-icon{
754
/* 	position: absolute; */
755
	vertical-align: middle;
756
}
757

  
758
/*text after the icon, which needs to be indented so that all its lines stay completely after the icon*/
759
.issue-subject{
760
	padding-left: 1em;
761
}
762

  
suissues-patch/test/fixtures/queries.yml
19 19
      - "125"
20 20
      :operator: "="
21 21

  
22
  view_options: |
23
    ---
24
    show_parents: "do_not_show"
25
    
22 26
  user_id: 1
23 27
  column_names: 
24 28
queries_002: 
......
37 41
      - "1"
38 42
      :operator: o
39 43

  
44
  view_options: |
45
    ---
46
    show_parents: "do_not_show"
47
    
40 48
  user_id: 3
41 49
  column_names: 
42 50
queries_003: 
......
51 59
      - "3"
52 60
      :operator: "="
53 61

  
62
  view_options: |
63
    ---
64
    show_parents: "do_not_show"
65
    
54 66
  user_id: 3
55 67
  column_names: 
56 68
queries_004: 
......
65 77
      - "3"
66 78
      :operator: "="
67 79

  
80
  view_options: |
81
    ---
82
    show_parents: "do_not_show"
83
    
68 84
  user_id: 2
69 85
  column_names: 
suissues-patch/test/fixtures/versions.yml
23 23
  id: 3
24 24
  description: Future version
25 25
  effective_date: 
26
onlinestore_1_0: 
27
  created_on: 2006-07-19 21:00:33 +02:00
28
  name: "1.0"
29
  project_id: 1
30
  updated_on: 2006-07-19 21:00:33 +02:00
31
  id: 4
32
  description: Future version
33
  effective_date: 
26 34
  
suissues-patch/test/functional/issues_controller_test.rb
22 22
class IssuesController; def rescue_action(e) raise e end; end
23 23

  
24 24
class IssuesControllerTest < Test::Unit::TestCase
25

  
25 26
  fixtures :projects,
26 27
           :users,
27 28
           :roles,
28 29
           :members,
30
           :queries,
29 31
           :issues,
30 32
           :issue_statuses,
31 33
           :versions,
......
960 962
    assert_equal 2, TimeEntry.find(1).issue_id
... This diff was truncated because it exceeds the maximum size that can be displayed.