Feature #4272 » dynamic-issue-columns.patch
| app/controllers/issues_controller.rb | ||
|---|---|---|
| 22 | 22 | before_filter :find_issue, :only => [:show, :edit, :reply] | 
| 23 | 23 | before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] | 
| 24 | 24 | before_filter :find_project, :only => [:new, :update_form, :preview] | 
| 25 | before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] | |
| 25 |   before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu, :update_column] | |
| 26 | 26 | before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] | 
| 27 | 27 | accept_key_auth :index, :show, :changes | 
| 28 | 28 | |
| ... | ... | |
| 415 | 415 | end | 
| 416 | 416 |  | 
| 417 | 417 | def context_menu | 
| 418 | @issues = Issue.find_all_by_id(params[:ids], :include => :project) | |
| 419 | if (@issues.size == 1) | |
| 420 | @issue = @issues.first | |
| 421 | @allowed_statuses = @issue.new_statuses_allowed_to(User.current) | |
| 418 | # Wants issue context menu | |
| 419 | if params[:header_context_menu].nil? && !params[:ids].blank? | |
| 420 | @issues = Issue.find_all_by_id(params[:ids], :include => :project) | |
| 421 | if (@issues.size == 1) | |
| 422 | @issue = @issues.first | |
| 423 | @allowed_statuses = @issue.new_statuses_allowed_to(User.current) | |
| 424 | end | |
| 425 | projects = @issues.collect(&:project).compact.uniq | |
| 426 | @project = projects.first if projects.size == 1 | |
| 427 | ||
| 428 |       @can = { | |
| 429 | :edit => (@project && User.current.allowed_to?(:edit_issues, @project)), | |
| 430 | :log_time => (@project && User.current.allowed_to?(:log_time, @project)), | |
| 431 | :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), | |
| 432 | :move => (@project && User.current.allowed_to?(:move_issues, @project)), | |
| 433 | :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), | |
| 434 | :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) | |
| 435 | } | |
| 436 | ||
| 437 | if @project | |
| 438 | @assignables = @project.assignable_users | |
| 439 | @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) | |
| 440 | end | |
| 441 | ||
| 442 | @priorities = IssuePriority.all.reverse | |
| 443 | @statuses = IssueStatus.find(:all, :order => 'position') | |
| 444 | @back = params[:back_url] || request.env['HTTP_REFERER'] | |
| 445 |  | |
| 446 | render :layout => false | |
| 447 | else | |
| 448 | # Wants a header context menu | |
| 449 | find_project unless params[:project_id].blank? | |
| 450 | retrieve_query | |
| 451 | @back = params[:back_url] || request.env['HTTP_REFERER'] | |
| 452 |  | |
| 453 | render :action => 'header_context_menu', :layout => false | |
| 422 | 454 | end | 
| 423 | projects = @issues.collect(&:project).compact.uniq | |
| 424 | @project = projects.first if projects.size == 1 | |
| 455 | end | |
| 456 | ||
| 457 | # Takes the params and stores the updated column information from the index | |
| 458 | def update_column | |
| 459 | find_project unless params[:project_id].blank? | |
| 460 | retrieve_query | |
| 425 | 461 | |
| 426 |     @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), | |
| 427 | :log_time => (@project && User.current.allowed_to?(:log_time, @project)), | |
| 428 | :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), | |
| 429 | :move => (@project && User.current.allowed_to?(:move_issues, @project)), | |
| 430 | :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), | |
| 431 | :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) | |
| 432 | } | |
| 433 | if @project | |
| 434 | @assignables = @project.assignable_users | |
| 435 | @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) | |
| 462 | respond_to do |format| | |
| 463 |       format.html { redirect_to :action => 'index', :project_id => params[:project_id] } | |
| 464 |       format.js {render(:update) {|page| page.redirect_to :action => 'index', :project_id => params[:project_id] }} | |
| 436 | 465 | end | 
| 437 |  | |
| 438 | @priorities = IssuePriority.all.reverse | |
| 439 | @statuses = IssueStatus.find(:all, :order => 'position') | |
| 440 | @back = params[:back_url] || request.env['HTTP_REFERER'] | |
| 441 |  | |
| 442 | render :layout => false | |
| 443 | 466 | end | 
| 444 | 467 | |
| 445 | 468 | def update_form | 
| ... | ... | |
| 498 | 521 |       cond << " OR project_id = #{@project.id}" if @project | 
| 499 | 522 | @query = Query.find(params[:query_id], :conditions => cond) | 
| 500 | 523 | @query.project = @project | 
| 501 |       session[:query] = {:id => @query.id, :project_id => @query.project_id} | |
| 502 | 524 | sort_clear | 
| 525 | @query.column_names = session[:query][:column_names] unless session[:query].nil? || session[:query][:column_names].nil? || session[:query][:id] != @query.id | |
| 526 |  | |
| 527 | # Update column list | |
| 528 | if params[:column].present? | |
| 529 | @query.column_names = update_columns_from_session(@query, params[:column].to_sym) | |
| 530 | end | |
| 531 |       session[:query] = {:id => @query.id, :project_id => @query.project_id, :column_names => @query.column_names} | |
| 503 | 532 | else | 
| 504 | 533 | if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) | 
| 505 | 534 | # Give it a name, required to be valid | 
| 506 | 535 | @query = Query.new(:name => "_") | 
| 507 | 536 | @query.project = @project | 
| 537 | @query.column_names = session[:query][:column_names] unless session[:query].nil? || session[:query][:column_names].nil? || session[:query][:id] != @query.id | |
| 508 | 538 | if params[:fields] and params[:fields].is_a? Array | 
| 509 | 539 | params[:fields].each do |field| | 
| 510 | 540 | @query.add_filter(field, params[:operators][field], params[:values][field]) | 
| ... | ... | |
| 514 | 544 | @query.add_short_filter(field, params[field]) if params[field] | 
| 515 | 545 | end | 
| 516 | 546 | end | 
| 547 | ||
| 548 | # Update column list | |
| 549 | if params[:column].present? | |
| 550 | columns = update_columns_from_session(@query, params[:column].to_sym) | |
| 551 | ||
| 552 | # Order columns based on the Query ordering | |
| 553 |           columns = @query.available_columns.select {|c| columns.include?(c.name)}.collect(&:name) | |
| 554 | ||
| 555 | @query.column_names = columns | |
| 556 | end | |
| 557 |  | |
| 517 | 558 | @query.group_by = params[:group_by] | 
| 518 |         session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by} | |
| 559 |         session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names} | |
| 519 | 560 | else | 
| 520 | 561 | @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] | 
| 521 | @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by]) | |
| 562 | @query.column_names = session[:query][:column_names] if session[:query][:id] && session[:query][:column_names] | |
| 563 | @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) | |
| 522 | 564 | @query.project = @project | 
| 523 | 565 | end | 
| 524 | 566 | end | 
| 525 | 567 | end | 
| 568 |  | |
| 569 | def update_columns_from_session(query, column_name) | |
| 570 | # Force to symbol | |
| 571 | column_symbol = column_name.to_sym | |
| 572 |  | |
| 573 | columns = @query.columns.collect(&:name) # Current columns | |
| 574 | # Columns from session | |
| 575 | if session[:query] && session[:query][:column_names] | |
| 576 | # Set Union | |
| 577 | columns = columns | session[:query][:column_names] | |
| 578 | end | |
| 579 |  | |
| 580 | # Delete if the update column exists, otherwise add it | |
| 581 | if columns.include?(column_symbol) | |
| 582 | columns.delete(column_symbol) | |
| 583 | else | |
| 584 | columns << column_symbol | |
| 585 | end | |
| 586 |  | |
| 587 | return columns | |
| 588 | end | |
| 526 | 589 | end | 
| app/controllers/queries_controller.rb | ||
|---|---|---|
| 27 | 27 | @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? | 
| 28 | 28 | @query.column_names = nil if params[:default_columns] | 
| 29 | 29 |  | 
| 30 | # Use the columns from the session if they are set | |
| 31 | if @query.column_names.nil? && session[:query] && session[:query][:column_names] | |
| 32 | @query.column_names = session[:query][:column_names] | |
| 33 | end | |
| 34 | ||
| 30 | 35 | params[:fields].each do |field| | 
| 31 | 36 | @query.add_filter(field, params[:operators][field], params[:values][field]) | 
| 32 | 37 | end if params[:fields] | 
| ... | ... | |
| 52 | 57 | @query.column_names = nil if params[:default_columns] | 
| 53 | 58 |  | 
| 54 | 59 | if @query.save | 
| 60 | # Clear the session[:query] to use the saved query | |
| 61 |         session[:query] = { :id => @query.id, :project_id => @query.project_id, :column_names => @query.column_names} | |
| 55 | 62 | flash[:notice] = l(:notice_successful_update) | 
| 56 | 63 | redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query | 
| 57 | 64 | end | 
| app/views/issues/_list.rhtml | ||
|---|---|---|
| 1 | 1 | <% form_tag({}) do -%>	 | 
| 2 | 2 | <%= hidden_field_tag 'back_url', url_for(params) %> | 
| 3 | 3 | <table class="list issues"> | 
| 4 | <thead><tr> | |
| 5 |         <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', | |
| 4 | <thead><tr id="issue-header" class="no-select hascontextmenu"> | |
| 5 |       <th><%= check_box_tag("header_context_menu", false, false, :style => 'display: none;', :class => 'no-select' ) %> | |
| 6 |           <%= hidden_field_tag("query_id", @query.id) if @query %> | |
| 7 |           <%= hidden_field_tag("project_id", @project.id) if @project %> | |
| 8 |           <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;', | |
| 6 | 9 |                                                            :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> | 
| 7 | 10 | </th> | 
| 8 | 11 | 		<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> | 
| app/views/issues/header_context_menu.rhtml | ||
|---|---|---|
| 1 | <ul> | |
| 2 | <% @query.available_columns.each do |column| %> | |
| 3 | <li> | |
| 4 | <%= link_to_remote(column.caption, | |
| 5 |                        { | |
| 6 |                         :url => { | |
| 7 | :controller => 'issues', | |
| 8 | :action => 'update_column', | |
| 9 | :project_id => @project, | |
| 10 | :set_filter => true, | |
| 11 | :column => column.name, | |
| 12 | :query_id => @query.id | |
| 13 | }, | |
| 14 |                         :with => "Form.serialize('query_form')" | |
| 15 | }, | |
| 16 |                        { | |
| 17 | :class => @query.columns.include?(column) ? 'icon-checked' : 'icon-none', | |
| 18 | :method => :post | |
| 19 | })%> | |
| 20 | </li> | |
| 21 | <% end %> | |
| 22 | </ul> | |
| public/javascripts/context_menu.js | ||
|---|---|---|
| 34 | 34 | var tr = Event.findElement(e, 'tr'); | 
| 35 | 35 | 		if (tr == document || tr == undefined  || !tr.hasClassName('hascontextmenu')) { return; } | 
| 36 | 36 | Event.stop(e); | 
| 37 | 		if (!this.isSelected(tr)) { | |
| 37 |     	        if (!this.isSelected(tr)) { | |
| 38 | 38 | this.unselectAll(); | 
| 39 | 39 | this.addSelection(tr); | 
| 40 | 40 | this.lastSelected = tr; | 
| ... | ... | |
| 48 | 48 |     if (window.opera && e.altKey) {	return; } | 
| 49 | 49 |     if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {       | 
| 50 | 50 | var tr = Event.findElement(e, 'tr'); | 
| 51 |       if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu')) { | |
| 51 |         if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu') && !tr.hasClassName('no-select')) { | |
| 52 | 52 | // a row was clicked, check if the click was on checkbox | 
| 53 | 53 | var box = Event.findElement(e, 'input'); | 
| 54 | 54 |         if (box!=document && box!=undefined) { | 
| ... | ... | |
| 155 | 155 | }, | 
| 156 | 156 |  | 
| 157 | 157 |   addSelection: function(tr) { | 
| 158 |     tr.addClassName('context-menu-selection'); | |
| 158 |     if (!tr.hasClassName('no-select')) { | |
| 159 |       tr.addClassName('context-menu-selection'); | |
| 160 | } | |
| 159 | 161 | this.checkSelectionBox(tr, true); | 
| 160 | 162 | }, | 
| 161 | 163 |  | 
| ... | ... | |
| 197 | 199 | 		if (all_checked) { | 
| 198 | 200 | boxes[i].checked = false; | 
| 199 | 201 | 			boxes[i].up('tr').removeClassName('context-menu-selection'); | 
| 200 | 		} else if (boxes[i].checked == false) { | |
| 202 | 		} else if (boxes[i].checked == false && !boxes[i].hasClassName('no-select')) { | |
| 201 | 203 | boxes[i].checked = true; | 
| 202 | 204 | 			boxes[i].up('tr').addClassName('context-menu-selection'); | 
| 203 | 205 | } | 
| test/functional/issues_controller_test.rb | ||
|---|---|---|
| 1093 | 1093 |                             :attributes => { :href => '#', | 
| 1094 | 1094 | :class => 'icon-del disabled' } | 
| 1095 | 1095 | end | 
| 1096 |  | |
| 1096 | ||
| 1097 | def test_context_menu_headers | |
| 1098 | @request.session[:user_id] = 2 | |
| 1099 | get :context_menu, :header_context_menu => '1' | |
| 1100 | assert_response :success | |
| 1101 | assert_template 'header_context_menu' | |
| 1102 | ||
| 1103 | assert_select 'ul' do | |
| 1104 | assert_select 'a', 'Project' | |
| 1105 | assert_select 'a', 'Tracker' | |
| 1106 | end | |
| 1107 | end | |
| 1108 | ||
| 1109 | def test_update_column_to_add_a_column | |
| 1110 | @request.session[:user_id] = 2 | |
| 1111 | post :update_column, :project_id => 'ecookbook', :column => 'author' | |
| 1112 | assert_redirected_to :action =>'index', :project_id => 'ecookbook' | |
| 1113 | ||
| 1114 | assert session[:query] | |
| 1115 | assert session[:query][:column_names] | |
| 1116 | assert session[:query][:column_names].include?(:author) | |
| 1117 | end | |
| 1118 | ||
| 1119 | def test_update_column_to_remove_a_column | |
| 1120 | @request.session[:user_id] = 2 | |
| 1121 | post :update_column, :project_id => 'ecookbook', :column => 'status' | |
| 1122 | assert_redirected_to :action =>'index', :project_id => 'ecookbook' | |
| 1123 | ||
| 1124 | assert session[:query] | |
| 1125 | assert session[:query][:column_names] | |
| 1126 | assert !session[:query][:column_names].include?(:status) | |
| 1127 | end | |
| 1128 | ||
| 1097 | 1129 | def test_destroy_routing | 
| 1098 | 1130 | assert_recognizes( #TODO: use DELETE on issue URI (need to change forms) | 
| 1099 | 1131 |       {:controller => 'issues', :action => 'destroy', :id => '1'}, | 
| test/functional/queries_controller_test.rb | ||
|---|---|---|
| 58 | 58 | :disabled => nil } | 
| 59 | 59 | end | 
| 60 | 60 |  | 
| 61 | def test_get_new_project_query_with_columns_from_session | |
| 62 | @request.session[:user_id] = 2 | |
| 63 |     @request.session[:query] = {:column_names => ['tracker', 'subject']} | |
| 64 | get :new, :project_id => 1 | |
| 65 | assert_response :success | |
| 66 | assert_template 'new' | |
| 67 | ||
| 68 | assert_select '#query_default_columns[checked=checked]', false | |
| 69 | ||
| 70 | assert_select 'select#selected_columns' do | |
| 71 | assert_select 'option[value=tracker]' | |
| 72 | assert_select 'option[value=subject]' | |
| 73 | end | |
| 74 | end | |
| 75 | ||
| 61 | 76 | def test_new_project_public_query | 
| 62 | 77 | @request.session[:user_id] = 2 | 
| 63 | 78 | post :new, |