├── .github └── workflows │ └── test.yml ├── README.md ├── app ├── controllers │ └── issues_panel_controller.rb ├── helpers │ └── issues_panel_helper.rb ├── models │ └── issue_card.rb └── views │ └── issues_panel │ ├── _modal_form.html.erb │ ├── _query_form.html.erb │ ├── _show_issue_description.html.erb │ ├── index.html.erb │ ├── move_issue_card.js.erb │ └── new_issue_card.js.erb ├── assets └── stylesheets │ └── redmine_issues_panel.css ├── config ├── locales │ ├── en.yml │ └── ja.yml └── routes.rb ├── images └── how_to_activate.png ├── init.rb ├── lib ├── redmine │ └── helpers │ │ └── issues_panel.rb └── redmine_issues_panel │ ├── issue_query_patch.rb │ ├── queries_controller_patch.rb │ └── view_hook.rb └── test ├── fixtures └── queries.yml ├── functional └── issues_panel_controller_test.rb ├── test_helper.rb └── unit ├── issue_card_test.rb ├── issue_query_test.rb └── lib └── redmine └── helpers └── issues_panel_test.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | include: 14 | - redmine-repository: 'redmica/redmica' 15 | redmine-version: 'stable-3.1' 16 | ruby-version: '3.3' 17 | - redmine-repository: 'redmica/redmica' 18 | redmine-version: 'master' 19 | ruby-version: '3.3' 20 | - redmine-repository: 'redmine/redmine' 21 | redmine-version: 'master' 22 | ruby-version: '3.3' 23 | 24 | steps: 25 | - uses: hidakatsuya/action-setup-redmine@v1 26 | with: 27 | repository: ${{ matrix.redmine-repository }} 28 | version: ${{ matrix.redmine-version }} 29 | ruby-version: ${{ matrix.ruby-version }} 30 | database: 'postgres:14' 31 | 32 | - uses: actions/checkout@v4 33 | with: 34 | path: plugins/redmine_issues_panel 35 | 36 | - run: bin/rails redmine:plugins:test NAME=redmine_issues_panel 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine Issues Panel 2 | 3 | This is a plugin for Redmine to display issues by statuses and change it's status by DnD. 4 | 5 | ## Features 6 | 7 | * Filter and group issues with custom queries 8 | * Drag and drop to change status 9 | * Update issues via the context menu 10 | * Add a new Issue from the panel 11 | 12 | ## Install 13 | 14 | #### Place the plugin source at Redmine plugins directory. 15 | 16 | `git clone` or copy an unarchived plugin to 17 | `plugins/redmine_issues_panel` on your Redmine installation path. 18 | 19 | ``` 20 | $ git clone https://github.com/redmica/redmine_issues_panel.git /path/to/redmine/plugins/redmine_issues_panel 21 | ``` 22 | 23 | ## How to activate the Issues Panel 24 | 25 | #### Check the 'Issues Panel' checkbox on the Project->Settings->Modules and save it. 26 | 27 |  28 | 29 | ## Test 30 | 31 | ``` 32 | $ cd /path/to/redmine 33 | $ bundle exec rake redmine:plugins:test NAME=redmine_issues_panel RAILS_ENV=test 34 | ``` 35 | 36 | ## Uninstall 37 | 38 | #### Remove the plugin directory. 39 | 40 | ``` 41 | $ cd /path/to/redmine 42 | $ rm -rf plugins/redmine_issues_panel 43 | ``` 44 | 45 | ## Licence 46 | 47 | This plugin is licensed under the GNU General Public License, version 2 (GPLv2) 48 | 49 | ## Author 50 | 51 | [Takenori Takaki (Far End Technologies)](https://www.farend.co.jp) 52 | -------------------------------------------------------------------------------- /app/controllers/issues_panel_controller.rb: -------------------------------------------------------------------------------- 1 | class IssuesPanelController < ApplicationController 2 | before_action :find_optional_project, :except => [:show_issue_description] 3 | before_action :find_issue_card, :only => [:show_issue_description, :move_issue_card] 4 | 5 | rescue_from Query::StatementInvalid, :with => :query_statement_invalid 6 | 7 | helper :projects 8 | helper :issues 9 | helper :queries 10 | helper :watchers 11 | helper :custom_fields 12 | include QueriesHelper 13 | 14 | def index 15 | retrieve_issue_panel(params) 16 | end 17 | 18 | def show_issue_description 19 | if flash[:error].nil? 20 | render json: { description: render_to_string(partial: 'issues_panel/show_issue_description', locals: { issue_card: @issue_card }) } 21 | else 22 | render json: { error_message: flash[:error] } 23 | end 24 | end 25 | 26 | def move_issue_card 27 | if flash[:error].nil? 28 | @issue_card.init_journal(User.current) 29 | @issue_card.move!(params) 30 | end 31 | rescue Exception => e 32 | flash.now[:error] = e.message 33 | ensure 34 | retrieve_issue_panel 35 | render :layout => false 36 | end 37 | 38 | def new_issue_card 39 | @issue_card = IssueCard.new 40 | @issue_card.project = @project 41 | @issue_card.project ||= @issue_card.allowed_target_projects.first 42 | @issue_card.author ||= User.current 43 | @issue_card.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date? 44 | attrs = (params[:issue] || {}).deep_dup 45 | @issue_card.instance_variable_set(:@safe_attribute_names, ['project_id', 'tracker_id', 'status_id', 'category_id', 'assigned_to_id', 'priority_id', 'fixed_version_id', 'subject', 'is_private']) 46 | @issue_card.safe_attributes = attrs 47 | @allowed_statuses = @issue_card.new_statuses_allowed_to(User.current) 48 | @priorities = IssuePriority.active 49 | end 50 | 51 | private 52 | 53 | def find_issue_card 54 | @issue_card = IssueCard.find(params[:id]) 55 | raise Unauthorized unless @issue_card.visible? 56 | rescue ActiveRecord::RecordNotFound 57 | @issue_card = IssueCard.new 58 | flash.now[:error] = l(:error_issue_not_found_in_project) 59 | rescue Unauthorized 60 | flash.now[:error] = l(:notice_not_authorized_to_change_this_issue) 61 | end 62 | 63 | def retrieve_issue_panel(params={}) 64 | @issues_panel = Redmine::Helpers::IssuesPanel.new(params) 65 | retrieve_query 66 | # retrieve optional query filter in session 67 | session_key = IssueQuery.name.underscore.to_sym 68 | if session[session_key] 69 | if params[:set_filter] && params[:query] && params[:query][:issues_num_per_row] 70 | session[session_key][:issues_num_per_row] = @query.issues_num_per_row 71 | elsif params[:query_id].blank? && session[session_key][:issues_num_per_row] 72 | @query.issues_num_per_row = session[session_key][:issues_num_per_row] 73 | end 74 | end 75 | @issues_panel.query = @query 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/helpers/issues_panel_helper.rb: -------------------------------------------------------------------------------- 1 | module IssuesPanelHelper 2 | 3 | def _project_issues_panel_path(project, *args) 4 | if project 5 | project_issues_panel_path(project, *args) 6 | else 7 | issues_panel_path(*args) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/issue_card.rb: -------------------------------------------------------------------------------- 1 | class IssueCard < Issue 2 | validate do 3 | (@move_attributes.keys - self.changes_to_save.keys).each do |attr| 4 | self.errors.add attr, l('activerecord.errors.messages.can_t_be_changed') 5 | end 6 | validate_custom_field_values 7 | end 8 | 9 | def move!(attributes={}) 10 | @move_attributes, @custom_field_attributes = {}, {} 11 | if attributes[:status_id] && self.status_id.to_s != attributes[:status_id].to_s 12 | @move_attributes['status_id'] = attributes[:status_id].to_s 13 | end 14 | if attributes[:group_key] && attributes[:group_value] 15 | if attributes[:group_key] == 'custom_field_values' 16 | group_values = attributes[:group_value].to_s.split(',') 17 | if self.custom_field_value(group_values[0]) != group_values[1] 18 | @custom_field_attributes['custom_field_values'] = Hash[*[group_values[0], group_values[1]]] 19 | end 20 | else 21 | if self.attributes[attributes[:group_key]].to_s != attributes[:group_value].to_s 22 | @move_attributes[attributes[:group_key]] = attributes[:group_value].to_s 23 | end 24 | end 25 | end 26 | if @move_attributes.any? || @custom_field_attributes.any? 27 | self.safe_attributes = @move_attributes.merge(@custom_field_attributes) 28 | self.save! 29 | end 30 | end 31 | 32 | # Override Issue#css_classes 33 | def css_classes(user=User.current) 34 | s = super 35 | s << ' icon icon-checked' if closed? 36 | s 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/views/issues_panel/_modal_form.html.erb: -------------------------------------------------------------------------------- 1 |
12 | <%= f.check_box :is_private, :no_label => true %> 13 |
14 | <% end %> 15 | 16 | <% projects = @issue_card.allowed_target_projects(User.current, @project) %> 17 | <% if @issue_card.safe_attribute?('project_id') && (@project.nil? || projects.length > 1) %> 18 |19 | <%= f.select :project_id, project_tree_options_for_select(projects, :selected => @issue_card.project), {:required => true}, 20 | :onchange => "updateIssueFrom('#{escape_javascript new_issue_card_path}', this)" %> 21 |
22 | <% end %> 23 | 24 | <% if @issue_card.safe_attribute?('tracker_id') || (@issue_card.persisted? && @issue_card.tracker_id_changed?) %> 25 |26 | <%= f.select :tracker_id, trackers_options_for_select(@issue_card), {:required => true}, 27 | :onchange => "updateIssueFrom('#{escape_javascript new_issue_card_path}', this)" %> 28 |
29 | <% end %> 30 | 31 | <% if @issue_card.safe_attribute? 'subject' %> 32 |<%= f.text_field :subject, :size => 80, :maxlength => 255, :required => true %>
33 | <% end %> 34 | 35 | <% if @issue_card.safe_attribute?('status_id') && @allowed_statuses.present? %> 36 |37 | <%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), {:required => true}, 38 | :onchange => "updateIssueFrom('#{escape_javascript new_issue_card_path}', this)" %> 39 |
40 | <% else %> 41 |<%= @issue_card.status %>
42 | <% end %> 43 | 44 | <% if @issue_card.safe_attribute? 'priority_id' %> 45 |<%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), {:required => true} %>
46 | <% end %> 47 | 48 | <% if @issue_card.safe_attribute? 'assigned_to_id' %> 49 |50 | <%= f.select :assigned_to_id, principals_options_for_select(@issue_card.assignable_users, @issue_card.assigned_to), 51 | :include_blank => true, :required => @issue_card.required_attribute?('assigned_to_id') %> 52 | <% if @issue_card.assignable_users.include?(User.current) %> 53 |
56 | <% end %> 57 | 58 | <% if @issue_card.safe_attribute?('category_id') && @issue_card.project.issue_categories.any? %> 59 | 54 | <% end %> 55 |60 | <%= f.select :category_id, (@issue_card.project.issue_categories.collect {|c| [c.name, c.id]}), 61 | {:include_blank => true, :required => @issue_card.required_attribute?('category_id')}, 62 | :onchange => "updateIssueFrom('#{escape_javascript new_issue_card_path}', this)" %> 63 | <%= link_to(sprite_icon('add',l(:label_issue_category_new)), 64 | new_project_issue_category_path(@issue_card.project), 65 | :remote => true, 66 | :method => 'get', 67 | :title => l(:label_issue_category_new), 68 | :tabindex => 200, 69 | :class => 'icon-only icon-add' 70 | ) if User.current.allowed_to?(:manage_categories, @issue_card.project) %> 71 |
72 | <% end %> 73 | 74 | <% if @issue_card.safe_attribute?('fixed_version_id') && @issue_card.assignable_versions.any? %> 75 |76 | <%= f.select :fixed_version_id, version_options_for_select(@issue_card.assignable_versions, @issue_card.fixed_version), 77 | :include_blank => true, :required => @issue_card.required_attribute?('fixed_version_id') %> 78 | <%= link_to(sprite_icon('add', l(:label_version_new)), 79 | new_project_version_path(@issue_card.project), 80 | :remote => true, 81 | :method => 'get', 82 | :title => l(:label_version_new), 83 | :tabindex => 200, 84 | :class => 'icon-only icon-add' 85 | ) if User.current.allowed_to?(:manage_versions, @issue_card.project) %> 86 |
87 | <% end %> 88 | 89 | <% custom_field_values = @issue_card.editable_custom_field_values %> 90 | <% if custom_field_values.present? %> 91 | <% custom_field_values.each do |value| %> 92 | <% if value.custom_field.is_required? || (params[:issue].is_a?(Hash) && params[:issue][:custom_field_values].is_a?(Hash) && params[:issue][:custom_field_values].keys.include?(value.custom_field.id.to_s)) %> 93 |<%= custom_field_tag_with_label :issue, value, :required => @issue_card.required_attribute?(value.custom_field_id) %>
94 | <% end %> 95 | <% end -%> 96 | <% end %> 97 |<%= l(:notice_issues_panel_truncated, :max => @issues_panel.issues_limit) %>
21 | <% end %> 22 | <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %> 23 | <%= hidden_field_tag('back_url', url_for(:params => request.query_parameters), :id => nil) %> 24 |' + selector + '
'); 15 | }); 16 | JS 17 | html << javascript_tag(js) 18 | end 19 | return html 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/fixtures/queries.yml: -------------------------------------------------------------------------------- 1 | --- 2 | queries_100: 3 | id: 100 4 | type: IssueQuery 5 | project_id: 1 6 | visibility: 2 7 | name: Issues Panel Query 8 | filters: | 9 | --- 10 | status_id: 11 | :values: 12 | - "1" 13 | - "2" 14 | - "5" 15 | :operator: "=" 16 | 17 | user_id: 1 18 | column_names: 19 | -------------------------------------------------------------------------------- /test/functional/issues_panel_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class IssuesPanelControllerTest < ActionController::TestCase 4 | def setup 5 | @request.session[:user_id] = 2 6 | end 7 | 8 | def test_index_with_out_query 9 | get :index 10 | assert_response :success 11 | assert_query_form 12 | end 13 | 14 | def test_index_should_forbidden_if_module_disabled 15 | # Enabled Issues Panel 16 | @project = Project.find(1) 17 | @project.enabled_modules = [] #<< EnabledModule.new(name: 'issues_panel') 18 | @project.save! 19 | 20 | get :index, :params => { 21 | :project_id => 1 22 | } 23 | assert_response :forbidden 24 | end 25 | 26 | def test_index_with_issues_panel_query 27 | # Enabled Issues Panel 28 | @project = Project.find(1) 29 | @project.enabled_modules << EnabledModule.new(name: 'issues_panel') 30 | @project.save! 31 | 32 | get :index, :params => { 33 | :project_id => 1, 34 | :query_id => 100 35 | } 36 | assert_response :success 37 | 38 | # query form 39 | assert_query_form 40 | 41 | issue_ids_on_status_1 = [1, 3, 5, 6, 7, 9, 10, 13, 14] 42 | issue_ids_on_status_2 = [2] 43 | issue_ids_on_status_5 = [8, 11, 12] 44 | 45 | # issues panel 46 | assert_select 'table#issues_panel.issues-panel' do 47 | assert_select 'thead' do 48 | assert_select 'tr' do 49 | assert_select 'th', "New#{issue_ids_on_status_1.count}" 50 | assert_select 'th', "Assigned#{issue_ids_on_status_2.count}" 51 | assert_select 'th', "Closed#{issue_ids_on_status_5.count}" 52 | end 53 | end 54 | assert_select 'tr' do 55 | assert_select 'td.issue-card-receiver[data-status-id=1]' do 56 | assert_issue_cards(issue_ids_on_status_1) 57 | end 58 | assert_select 'td.issue-card-receiver[data-status-id=2]' do 59 | assert_issue_cards(issue_ids_on_status_2) 60 | end 61 | assert_select 'td.issue-card-receiver[data-status-id=5]' do 62 | assert_issue_cards(issue_ids_on_status_5) 63 | end 64 | end 65 | end 66 | end 67 | 68 | def assert_query_form 69 | # query form 70 | assert_select 'form#query_form' do 71 | assert_select 'div#query_form_with_buttons.hide-when-print' do 72 | assert_select 'div#query_form_content' do 73 | assert_select 'fieldset#filters.collapsible' 74 | assert_select 'fieldset#options' 75 | end 76 | assert_select 'p.buttons' 77 | end 78 | end 79 | end 80 | 81 | def assert_issue_cards(issue_ids=[]) 82 | issue_ids.each do |issue_id| 83 | assert_select "div#issue-card-#{issue_id}.issue-card[data-issue-id=#{issue_id}]" 84 | end 85 | end 86 | 87 | def test_move_issue_card 88 | put :move_issue_card, :xhr => true, :params => { 89 | :id => 1, :status_id => 5 90 | } 91 | assert_response :success 92 | assert_match "$('#issue-card-1').remove()", response.body 93 | assert_match "$('#issues-count-on-status-1').html('0')", response.body 94 | assert_match "$('#issues-count-on-status-5').html('4')", response.body 95 | assert_match "$('.issues-count-on-group').html('0');", response.body 96 | assert_match "$('#issues-count-on-group-').html('4');", response.body 97 | assert_match "loadDraggableSettings();", response.body 98 | end 99 | 100 | def test_move_issue_card_but_record_not_found 101 | put :move_issue_card, :xhr => true, :params => { 102 | :id => 99999, :status_id => 2 103 | } 104 | assert_response :success 105 | assert_match "alert('#{I18n.t(:error_issue_not_found_in_project)}')", response.body 106 | assert_match "('#issue-card-').animate( {left: 0, top: 0}, 500 );", response.body 107 | end 108 | 109 | def test_move_issue_card_but_unauthorized 110 | IssueCard.any_instance.stubs(:visible?).returns(false) 111 | put :move_issue_card, :xhr => true, :params => { 112 | :id => 1, :status_id => 2 113 | } 114 | assert_response :success 115 | assert_match "alert('#{I18n.t(:notice_not_authorized_to_change_this_issue)}')", response.body 116 | assert_match "('#issue-card-1').animate( {left: 0, top: 0}, 500 );", response.body 117 | end 118 | 119 | def test_move_issue_card_but_exception_raised 120 | error_message_on_move = 'error message on move' 121 | IssueCard.any_instance.stubs(:move!).raises(error_message_on_move) 122 | put :move_issue_card, :xhr => true, :params => { 123 | :id => 1, :status_id => 2 124 | } 125 | assert_response :success 126 | assert_match "alert('#{error_message_on_move}')", response.body 127 | assert_match "('#issue-card-1').animate( {left: 0, top: 0}, 500 );", response.body 128 | end 129 | 130 | def assert_modal_issue_card() 131 | assert_match "showModal('new-issue-card-modal', '450px');", response.body 132 | assert_match "$('#new-issue-card-modal').addClass('new-issue-card');", response.body 133 | end 134 | 135 | def test_new_issue_card 136 | get :new_issue_card, :xhr => true, :params => { 137 | :status_id => 5 138 | } 139 | assert_response :success 140 | assert_modal_issue_card 141 | end 142 | 143 | def test_new_issue_card_method_post 144 | post :new_issue_card, :xhr => true, :params => { 145 | :status_id => 5 146 | } 147 | assert_response :success 148 | assert_modal_issue_card 149 | end 150 | 151 | def test_show_description 152 | issue = Issue.generate!(:description => 'Issue Description', :author => User.current) 153 | get :show_issue_description, :xhr => true, :params => { 154 | :id => issue.id 155 | } 156 | assert_response :success 157 | assert_equal 'application/json', response.media_type 158 | data = ActiveSupport::JSON.decode(response.body) 159 | assert_select(Nokogiri::HTML(data['description']), "div.issue") do 160 | assert_select 'div.subject', "##{issue.id}: #{issue.subject}" 161 | assert_select 'div.description' do 162 | assert_select 'div.wiki', issue.description 163 | end 164 | end 165 | end 166 | 167 | def test_show_description_but_record_not_found 168 | put :show_issue_description, :xhr => true, :params => { 169 | :id => 99999 170 | } 171 | assert_response :success 172 | assert_equal 'application/json', response.media_type 173 | data = ActiveSupport::JSON.decode(response.body) 174 | assert_equal I18n.t(:error_issue_not_found_in_project), data['error_message'] 175 | end 176 | 177 | def test_show_issue_description_but_unauthorized 178 | IssueCard.any_instance.stubs(:visible?).returns(false) 179 | put :show_issue_description, :xhr => true, :params => { 180 | :id => 1 181 | } 182 | 183 | assert_response :success 184 | assert_equal 'application/json', response.media_type 185 | data = ActiveSupport::JSON.decode(response.body) 186 | assert_equal I18n.t(:notice_not_authorized_to_change_this_issue), data['error_message'] 187 | end 188 | 189 | def test_show_issue_description_when_description_is_nil 190 | issue = Issue.generate!(:description => nil, :author => User.current) 191 | get :show_issue_description, :xhr => true, :params => { 192 | :id => issue.id 193 | } 194 | assert_response :success 195 | assert_equal 'application/json', response.media_type 196 | data = ActiveSupport::JSON.decode(response.body) 197 | assert_select(Nokogiri::HTML(data['description']), "div.issue") do 198 | assert_select 'div.subject', "##{issue.id}: #{issue.subject}" 199 | assert_select 'div.description' do 200 | assert_select 'div.wiki', '' 201 | end 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | ActiveSupport::TestCase.fixture_paths << File.dirname(__FILE__) + '/fixtures' 4 | 5 | class ActiveSupport::TestCase 6 | # This is not necessary because `fixtures :all` is already used in Redmine trunk, 7 | # but it is set here because RedMica has not yet followed that change. 8 | # If RedMica follows Redmine trunk in the future, this setting will not be necessary. 9 | fixtures :all 10 | end 11 | -------------------------------------------------------------------------------- /test/unit/issue_card_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class IssueCardTest < ActiveSupport::TestCase 4 | def setup 5 | User.current = User.find(1) 6 | end 7 | 8 | def test_move_status 9 | issue_card = IssueCard.find(1) 10 | assert_equal 1, issue_card.status_id 11 | 12 | issue_card.move!({ :status_id => 2 }) 13 | assert_equal 2, issue_card.status_id 14 | end 15 | 16 | def test_move_relation_group 17 | issue_card = IssueCard.find(1) 18 | 19 | assert_equal 1, issue_card.tracker_id 20 | issue_card.move!({ :group_key => 'tracker_id', :group_value => 2 }) 21 | assert_equal 2, issue_card.tracker_id 22 | 23 | assert_equal 1, issue_card.project_id 24 | issue_card.move!({ :group_key => 'project_id', :group_value => 2 }) 25 | assert_equal 2, issue_card.project_id 26 | 27 | assert_equal 4, issue_card.category_id 28 | issue_card.move!({ :group_key => 'category_id', :group_value => 3 }) 29 | assert_equal 3, issue_card.category_id 30 | 31 | assert_nil issue_card.assigned_to_id 32 | issue_card.move!({ :group_key => 'assigned_to_id', :group_value => 2 }) 33 | assert_equal 2, issue_card.assigned_to_id 34 | 35 | assert_equal 4, issue_card.priority_id 36 | issue_card.move!({ :group_key => 'priority_id', :group_value => 7 }) 37 | assert_equal 7, issue_card.priority_id 38 | 39 | assert_nil issue_card.fixed_version_id 40 | issue_card.move!({ :group_key => 'fixed_version_id', :group_value => 5 }) 41 | assert_equal 5, issue_card.fixed_version_id 42 | end 43 | 44 | def test_move_custom_field_group 45 | issue_card = IssueCard.find(1) 46 | field = IssueCustomField.find_by_name('Database') 47 | assert issue_card.available_custom_fields.include?(field) 48 | assert_nil issue_card.custom_field_value(field.id) 49 | 50 | issue_card.move!({ :group_key => 'custom_field_values', :group_value => '1,PostgreSQL'}) 51 | assert_equal 'PostgreSQL', issue_card.custom_field_value(field.id) 52 | end 53 | 54 | def test_css_classes_include_icon_checked 55 | closed_issue_card = IssueCard.find(8) 56 | closed_classes = closed_issue_card.css_classes.split(' ') 57 | assert_include 'icon', closed_classes 58 | assert_include 'icon-checked', closed_classes 59 | 60 | issue_card = IssueCard.find(1) 61 | classes = issue_card.css_classes.split(' ') 62 | assert_not_include 'icon', classes 63 | assert_not_include 'icon-checked', classes 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/unit/issue_query_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class IssueQueryTest < ActiveSupport::TestCase 4 | def test_build_from_params_should_set_issues_num_per_row 5 | q = IssueQuery.create!(:name => 'issue panel', :options => {}) 6 | 7 | q.build_from_params({ :issues_num_per_row => 2 }) 8 | assert_equal 2, q.options[:issues_num_per_row] 9 | 10 | q.build_from_params({ :query => { :issues_num_per_row => 3 } }) 11 | assert_equal 3, q.options[:issues_num_per_row] 12 | end 13 | 14 | def test_issues_num_per_row_should_set_options_value 15 | q = IssueQuery.create!(:name => 'issue panel', :options => {}) 16 | q.issues_num_per_row = 2 17 | assert_equal 2, q.options[:issues_num_per_row] 18 | end 19 | 20 | def test_issues_num_per_row_should_return_default_value 21 | q = IssueQuery.create!(:name => 'issue panel', :options => {}) 22 | assert_equal 1, q.issues_num_per_row 23 | end 24 | 25 | def test_issues_num_per_row_should_return_options_value 26 | q = IssueQuery.create!(:name => 'issue panel', :options => {:issues_num_per_row => 3}) 27 | assert_equal 3, q.issues_num_per_row 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/unit/lib/redmine/helpers/issues_panel_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../../../../test_helper', __FILE__) 2 | 3 | class Redmine::Helpers::IssuesPanelHelperTest < Redmine::HelperTest 4 | include ERB::Util 5 | include Rails.application.routes.url_helpers 6 | include Redmine::I18n 7 | 8 | def setup 9 | User.current = User.find(1) 10 | end 11 | 12 | def test_initialize 13 | with_settings :gantt_items_limit => 500 do 14 | issues_panel = Redmine::Helpers::IssuesPanel.new 15 | assert_equal 500, issues_panel.issues_limit 16 | assert_equal false, issues_panel.truncated 17 | end 18 | end 19 | 20 | def test_initialize_with_issues_limit 21 | with_settings :gantt_items_limit => 500 do 22 | issues_panel = Redmine::Helpers::IssuesPanel.new({:issues_limit => 100}) 23 | assert_equal 100, issues_panel.issues_limit 24 | assert_equal false, issues_panel.truncated 25 | end 26 | end 27 | 28 | def test_set_issue_query 29 | issues_panel = Redmine::Helpers::IssuesPanel.new 30 | issue_query = IssueQuery.new 31 | issues_panel.query = issue_query 32 | assert_equal issue_query, issues_panel.query 33 | end 34 | 35 | def test_update_trancated 36 | issues_limit = 100 37 | with_settings :gantt_items_limit => issues_limit do 38 | issues_panel = Redmine::Helpers::IssuesPanel.new({:issues_limit => 100}) 39 | query = IssueQuery.new 40 | 41 | query.stubs(:issue_count).returns(issues_limit - 1) 42 | issues_panel.query = query 43 | assert_equal false, issues_panel.truncated 44 | 45 | query.stubs(:issue_count).returns(issues_limit) 46 | issues_panel.query = query 47 | assert_equal false, issues_panel.truncated 48 | 49 | query.stubs(:issue_count).returns(issues_limit + 1) 50 | issues_panel.query = query 51 | assert_equal true, issues_panel.truncated 52 | end 53 | end 54 | 55 | def test_panel_statuses_with_project 56 | issues_panel = Redmine::Helpers::IssuesPanel.new() 57 | query = IssueQuery.new() 58 | project = Project.find(1) 59 | query.project = project 60 | issues_panel.query = query 61 | 62 | assert_equal project.rolled_up_statuses.where(:is_closed => false), issues_panel.panel_statuses 63 | end 64 | 65 | def test_panel_statuses_without_project 66 | issues_panel = Redmine::Helpers::IssuesPanel.new() 67 | query = IssueQuery.new() 68 | issues_panel.query = query 69 | 70 | assert_equal IssueStatus.all.sorted.where(:is_closed => false), issues_panel.panel_statuses 71 | end 72 | 73 | def test_panel_statuses_with_include_tracker_filter 74 | WorkflowTransition.where(:tracker_id => [1, 2]).delete_all 75 | WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 0, :new_status_id => 1) 76 | WorkflowTransition.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 0, :new_status_id => 5) 77 | issues_panel = Redmine::Helpers::IssuesPanel.new() 78 | query = IssueQuery.new() 79 | query.filters = {'tracker_id' => {:operator => "=", :values => [1, 2]}} 80 | issues_panel.query = query 81 | 82 | assert_equal IssueStatus.all.sorted.where(id: [0, 1, 5]), issues_panel.panel_statuses 83 | end 84 | 85 | def test_panel_statuses_with_exclude_tracker_filter 86 | WorkflowTransition.where(:tracker_id => 3).delete_all 87 | WorkflowTransition.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 1, :new_status_id => 2) 88 | issues_panel = Redmine::Helpers::IssuesPanel.new() 89 | query = IssueQuery.new() 90 | query.filters = {'tracker_id' => {:operator => "!", :values => [1, 2]}} 91 | issues_panel.query = query 92 | 93 | assert_equal IssueStatus.all.sorted.where(:id => [1, 2]), issues_panel.panel_statuses 94 | end 95 | 96 | def test_issues 97 | issues_limit = 3 98 | issues_panel = Redmine::Helpers::IssuesPanel.new({:issues_limit => issues_limit}) 99 | query = IssueQuery.new(:filters => { :status_id => {:operator => "*", :values => [""] } } ) 100 | 101 | issues_panel.query = query 102 | assert_equal issues_limit, issues_panel.issues.count 103 | end 104 | end 105 | --------------------------------------------------------------------------------