0001-YetAnotherUpdate-Of-Ported-the_redmine_subtasks-plugin_from_Aleksei_Guse.patch
| b/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) |
| b/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 |
| b/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) |
| b/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 |
| b/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 |
| b/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 |
| b/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 |
| b/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 |
|
| b/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 |
| b/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 | ||
| b/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']}) %>
|
| b/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> |
| b/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;"> </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;"> </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 -%> |
| b/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 -%> |
|
| b/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> |
|
| b/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 %> |
|
| b/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> |
| b/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> |
| b/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"> </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? %>
|
| b/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 %> |
| b/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> |
| b/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 %> |
| b/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> |
| b/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 |
| b/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: '' |
|
| b/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 |
|
| b/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 |
|
| b/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 |
|
| b/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 |
|
| b/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 |
|
| b/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 |
|
| b/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
|