├── .gitignore ├── LICENSE ├── README.md ├── app ├── controllers │ ├── project_project_enumerations_controller.rb │ └── project_project_list_values_controller.rb ├── models │ └── project_enumeration.rb └── views │ ├── custom_fields │ └── formats │ │ ├── _project_enumeration.html.erb │ │ └── _project_list_value.html.erb │ ├── project_project_enumerations │ ├── _form.html.erb │ ├── create.js.erb │ ├── edit.html.erb │ ├── index.api.rsb │ ├── index.html.erb │ ├── new.html.erb │ └── show.api.rsb │ ├── project_project_list_values │ ├── _form.html.erb │ ├── create.js.erb │ ├── edit.html.erb │ ├── index.api.rsb │ ├── index.html.erb │ ├── new.html.erb │ └── show.api.rsb │ ├── projects │ └── settings │ │ ├── _custom_field_checkbox.html.erb │ │ ├── _issues.html.erb │ │ ├── _project_enumerations.html.erb │ │ └── _project_list_values.html.erb │ └── settings │ └── _redmine_smile_project_enumerations_custom_field_format.html.erb ├── assets ├── images │ ├── loading.gif │ └── reorder.png └── stylesheets │ └── style.css ├── config ├── locales │ ├── ca.yml │ ├── en-GB.yml │ ├── en.yml │ ├── es.yml │ ├── fr.yml │ └── uk.yml └── routes.rb ├── db └── migrate │ ├── 20190901140000_fix_migration_name.rb │ ├── 20190904123000_create_project_enumerations.rb │ └── 20191204234500_add_project_enumeration_position.rb ├── doc ├── Project_Enumeration_In_Issue_20191004.png ├── Project_Enumerations_CF_20191004.png ├── Project_Enumerations_Edit_20191004.png └── Project_Enumerations_Edit_enumeration_20191015.png ├── init.rb ├── lib ├── controllers │ └── smile_controllers_projects.rb ├── helpers │ └── smile_helpers_projects.rb ├── models │ ├── smile_models_custom_field.rb │ └── smile_models_project.rb ├── project_enumeration_field_format.rb ├── project_list_value_field_format.rb ├── redmine_smile_project_enumerations_custom_field_format │ └── hooks.rb └── smile_tools.rb ├── scripts └── test_it.sh └── test ├── fixtures ├── custom_fields.yml ├── custom_fields_projects.yml ├── custom_fields_trackers.yml ├── custom_values.yml └── project_enumerations.yml ├── functional └── issues_controller_test.rb ├── test_helper.rb └── unit └── custom_value_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | capybara-*.html 3 | .rspec 4 | /log 5 | /tmp 6 | /db/*.sqlite3 7 | /public/system 8 | /coverage/ 9 | /spec/tmp 10 | **.orig 11 | rerun.txt 12 | pickle-email-*.html 13 | config/initializers/secret_token.rb 14 | config/secrets.yml 15 | 16 | ## Environment normalisation: 17 | /.bundle 18 | /vendor/bundle 19 | 20 | # these should all be checked in to normalise the environment: 21 | # Gemfile.lock, .ruby-version, .ruby-gemset 22 | 23 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 24 | .rvmrc 25 | 26 | # SVN 27 | .svn 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Smile - Open Source Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redmine_smile_project_enumerations_custom_field_format 2 | ================================================= 3 | 4 | Redmine plugin that adds new custom field format, 5 | that allows to have **Enumerations** whose values are 6 | **set in the project** 7 | 8 | # How it works 9 | 10 | ## What it does 11 | 12 | * Adds a new value in the CustomFiels types : **Project Enumeration** 13 | 14 | To manage a **Key / Value list** whose possible values are configured in the project. 15 | The **key is stored** in the **custom_values** table 16 | 17 | * Adds a new value in the CustomFiels types : **Project Values List** 18 | 19 | To manage a **Values list** whose possible values are configured in the project. 20 | The **value is stored** in the **custom_values** table 21 | 22 | * Adds a new permission : **manage_project_enumerations** 23 | 24 | This permission allows to edit Project Enumerations values for the project. 25 | When a user has this permission, 2 new tabs appear in the Project Settings (depending if Custom Fields of the new type exist or not) 26 | - **Project Enumerations** 27 | - **Project List of Values** 28 | 29 | * Manages Enumeration **statuses** like for Versions : 30 | 31 | If Enumeration is **locked** or **closed**, possible value will not be present in the dropdown list (depends on the Custom Field status configuration) : 32 | 33 | ![Example of status configuration](https://user-images.githubusercontent.com/540464/79513668-aefa6b80-8044-11ea-91a5-0158912c47fc.png "Example of status configuration") 34 | 35 | * Splits Custom Field Project configuration **by Tracker** 36 | 37 | * **Rewrites** **app/views/projects/settings/_issues.html.erb** 38 | 39 | C.f. : [Redmine Patch : Project Custom Fields configuration : split by tracker](http://www.redmine.org/issues/30739) 40 | 41 | * Adds three **Hooks** in this **partial** : 42 | * **view_project_settings_tracker_before_checkbox** 43 | * **view_project_settings_tracker_after_checkbox** 44 | * **view_project_settings_issues_custom_fields** 45 | 46 | * Tested with **Redmine V4.0.3** 47 | 48 | ## How it is implemented 49 | 50 | - Adds new **FieldFormat** derived form **RecordList** 51 | - **Redmine::FieldFormat::ProjectEnumerationFormat** 52 | - **Redmine::FieldFormat::ProjectListValueFormat** 53 | 54 | - 🔑 Rewrites Projects Controller **settings** action 55 | 56 | To manage : Project Custom Fields configuration, split by tracker (optimization) 57 | 58 | - 🔑 Extends Projects Helper **project_settings_tabs** method 59 | 60 | - Adds new methods to **Project** model 61 | - **shared_enumerations** 62 | - **shared_list_values** 63 | 64 | - Adds new **Controller** 65 | - **ProjectProjectEnumerationsController** 66 | - **ProjectProjectListValuesController** 67 | 68 | - Adds new **Views** 69 | - for **possible values** CRUD **edition** 70 | - in **views/project_project_enumerations** 71 | - in **views/project_list_values** 72 | - for **Custom Field** **Configuration** 73 | - in **views/custom_fields/formats** 74 | - 🔑 **Rewrites** partial **app/views/projects/settings/_issues.html.erb** 75 | 76 | # Testing 77 | 78 | ```console 79 | # From plugin root, redmine_test mysql database must exist 80 | scripts/test_it.sh 81 | ``` 82 | * Tested with other than Issue Custom Field (Project, Version, ...) 83 | 84 | # TODOs 85 | 86 | * Add Admin view for all Project Enumerations 87 | * Add more Tests 88 | * Fix TODOS 89 | * Edit position for shared values 90 | 91 | # Changelog 92 | 93 | * **V1.3.15** Bugfix 94 | 95 | Removed a potential issue with **to_prepare** 96 | 97 | * **V1.3.14** Compatibility with **Redmine Wiki Extensions plugin** 98 | 99 | That has to accept other plugins too ! 100 | 101 | * **V1.3.13** Compatibility with **Redmine Issue Templates plugin** 102 | 103 | Project settings tabs : manage controller in allowed_to? in **project_settings_tabs_with_project_enumerations** 104 | 105 | * **V1.3.12** Fixed permission name : **edit_project_list_values** -> **manage_project_enumerations** 106 | * **V1.3.11** Fixed Project settings **Issue Tracking** tab not visible for Redmine < 4 107 | 108 | With Redmine 4, **Trackers** / **Custom Fields** settings have moved to a new tab 109 | And **Modules** tab has been merged to **Information** tab 110 | 111 | * **V1.3.10** Fixed error with Postgresql : is_for_all = 1 112 | 113 | PG::UndefinedFunction: ERROR: operateur boolean = integer does not exist 114 | 115 | * **V1.3.9** Project Custom Fields configuration, split by tracker : optimization 116 | * **V1.3.8** Value field input : 40 -> 80 characters 117 | * **V1.3.7** Fix : Project enumeration on project, bug at project creation 118 | 119 | Fixed a native bug : Projet has project method (self) making think that it has project permissions to check ! 120 | 121 | * **V1.3.6** Fix : scope enabled_on_project, test if project nil 122 | * **V1.3.5** Extends size of Project Enumerations value column : 60 -> 255 cars 123 | * **V1.3.4** Enable formats on UserCustomField 124 | * **V1.3.3** Lower constraint on rails version : 4 -> 3.4 125 | * **V1.3.2** Fix migration file name, remove one 0 at the end 126 | * **V1.3.1** Fixed missing partial **_custom_field_checkbox.html.erb** 127 | * **V1.3.0** Splits Custom Field Project configuration **by Tracker** 128 | 129 | * (+) Add 3 Hooks in **app/views/projects/settings/_issues.html.erb** 130 | 131 | * **V1.2.2** Manage **is_for_all** Custom Fields 132 | 133 | * (+) BugFix target_class : Model shared with ProjectEnumeration for ProjectListValueFormat 134 | 135 | * **V1.2.1** BugFix show only Project Enumeration values for project, or shared to project 136 | 137 | * (+) Show list of not enabled List Values Custom Fields 138 | * (+) Show tab in project settings only if at least one Custom Field of the type exists 139 | 140 | * **V1.2.0** Display not enabled project enumerations, single page to edit List Values like Enumerations 141 | 142 | * Display errors on project enumerations update. 143 | * Edit other than Issue Custom Fields project 144 | 145 | * **V1.1.0** Fix XSS issue with Project Enumeration value edition 146 | * **V1.0.9** Project Enumeration create, render model errors 147 | * **V1.0.8** Project Enumeration sorting by position, + create Project Enumeration at the end 148 | * **V1.0.7** Project Enumeration edition like Key/Value edition : single page by Custom Field 149 | * **V1.0.6** New Project List of Values type added 150 | * **V1.0.5** Tests added on issue edit, disabled possible values (locked, closed) 151 | * **V1.0.4** Tests added on issue show 152 | * **V1.0.3** Tests initialized 153 | * **V1.0.2** shared_enumerations fixed (namespaces) 154 | * **V1.0.1** Fixed redirect to Project enumerations tab after update 155 | 156 | Project Enumeration status editable at creation 157 | 158 | * **V1.0** Initial version 159 | 160 | 161 | Enjoy ! 162 | 163 | ![alt text](https://compteur-visites.ennder.fr/sites/36/token/githubpe/image "Logo") 164 | -------------------------------------------------------------------------------- /app/controllers/project_project_enumerations_controller.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2017 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | class ProjectProjectEnumerationsController < ApplicationController 19 | menu_item :settings 20 | model_object ProjectEnumeration 21 | 22 | before_action :find_model_object, :except => [:index, :new, :create, :update_each] 23 | before_action :find_project_from_association, :except => [:index, :new, :create, :update_each] 24 | before_action :find_project_by_project_id, :only => [:index, :new, :create, :update_each] 25 | before_action :find_custom_field_by_custom_field_id, :only => [:index, :new, :create, :update_each] 26 | 27 | before_action :authorize 28 | 29 | 30 | # TODO create API views 31 | accept_api_auth :index, :create, :update, :destroy 32 | 33 | helper :projects, :custom_fields 34 | helper_method :project_enumeration_custom_field_title 35 | 36 | def index 37 | find_project_enumerations_for_custom_field(@custom_field.id) 38 | 39 | @project_enumeration = ProjectEnumeration.new( 40 | :project_id => @project.id, 41 | :custom_field_id => @custom_field.id 42 | ) 43 | respond_to do |format| 44 | format.html do 45 | end 46 | format.api 47 | end 48 | end 49 | 50 | def new 51 | @project_enumeration = ProjectEnumeration.new( 52 | :project_id => @project.id, 53 | :custom_field_id => @custom_field.id 54 | ) 55 | 56 | @project_enumeration.safe_attributes = params[:project_enumeration] 57 | 58 | respond_to do |format| 59 | format.html 60 | format.js 61 | end 62 | end 63 | 64 | def create 65 | find_project_enumerations_for_custom_field(@custom_field.id) 66 | 67 | if @project_enumerations.is_a?(Array) 68 | max_position = 0 69 | else 70 | max_position = @project_enumerations.maximum(:position) 71 | end 72 | 73 | max_position ||= 0 74 | max_position += 1 75 | 76 | @project_enumeration = ProjectEnumeration.new( 77 | :project_id => @project.id, 78 | :custom_field_id => @custom_field.id, 79 | :position => max_position 80 | ) 81 | 82 | if params[:project_enumeration] 83 | attributes = params[:project_enumeration].dup 84 | attributes.delete('sharing') unless attributes.nil? || @project_enumeration.allowed_sharings.include?(attributes['sharing']) 85 | 86 | if Rails.version < '5' 87 | # Avoid ActiveModel::ForbiddenAttributesError 88 | attributes.permit! 89 | end 90 | 91 | @project_enumeration.safe_attributes = attributes 92 | end 93 | 94 | if request.post? 95 | if @project_enumeration.save 96 | respond_to do |format| 97 | format.html do 98 | flash[:notice] = l(:notice_successful_create) 99 | redirect_back_or_default settings_project_path(@project, :tab => 'project_enumerations') 100 | end 101 | format.js { 102 | find_project_enumerations_for_custom_field(@custom_field.id) 103 | render :action => 'create' 104 | } 105 | format.api do 106 | render :action => 'show', :status => :created, :location => project_project_enumerations_url(@project_enumeration) 107 | end 108 | end 109 | else 110 | respond_to do |format| 111 | format.html { render :action => 'new' } 112 | format.js { 113 | find_project_enumerations_for_custom_field(@custom_field.id) 114 | 115 | # Render errors to flash message 116 | error_msg = @project_enumeration.errors.full_messages 117 | 118 | if error_msg.any? 119 | flash[:error] = error_msg.join('
'.html_safe) 120 | end 121 | 122 | render :action => 'create' 123 | } 124 | format.api { render_validation_errors(@project_enumeration) } 125 | end 126 | end 127 | end 128 | end 129 | 130 | def edit 131 | end 132 | 133 | def update 134 | if params[:project_enumeration] 135 | attributes = params[:project_enumeration].dup 136 | attributes.delete('sharing') unless @project_enumeration.allowed_sharings.include?(attributes['sharing']) 137 | 138 | @project_enumeration.safe_attributes = attributes 139 | if @project_enumeration.save 140 | respond_to do |format| 141 | format.html { 142 | flash[:notice] = l(:notice_successful_update) 143 | redirect_back_or_default settings_project_path(@project, :tab => 'project_enumerations') 144 | } 145 | format.api { render_api_ok } 146 | end 147 | else 148 | respond_to do |format| 149 | format.html { render :action => 'edit' } 150 | format.api { render_validation_errors(@project_enumeration) } 151 | end 152 | end 153 | end 154 | end 155 | 156 | def update_each 157 | project_shared_enumerations = @project.shared_enumerations.to_a 158 | saved = ProjectEnumeration.update_each(@project, update_each_params, project_shared_enumerations) 159 | 160 | if saved 161 | flash[:notice] = l(:notice_successful_update) 162 | else 163 | # Render errors to flash message 164 | 165 | error_msg = [] 166 | project_shared_enumerations.each do |pe| 167 | pe.errors.full_messages.each do |m| 168 | error_msg << "#{pe.value} : #{m} (#{l(:field_position)} #{pe.position})" 169 | end 170 | end 171 | 172 | if error_msg.any? 173 | flash[:error] = error_msg.join('
'.html_safe) 174 | end 175 | end 176 | 177 | redirect_to :action => 'index', :custom_field_id => @custom_field.id 178 | end 179 | 180 | def destroy 181 | @project_enumeration.destroy 182 | custom_field_id = @project_enumeration.custom_field_id 183 | respond_to do |format| 184 | format.html { 185 | flash[:notice] = l(:notice_successful_delete) 186 | redirect_back_or_default project_project_enumerations_path(@project, :custom_field_id => custom_field_id) 187 | } 188 | format.api { render_api_ok } 189 | format.js { 190 | flash[:notice] = l(:notice_successful_delete) 191 | redirect_back_or_default project_project_enumerations_path(@project, :custom_field_id => custom_field_id) 192 | } 193 | end 194 | end 195 | 196 | 197 | protected 198 | 199 | def find_model_object 200 | model = self.class.model_object 201 | if model 202 | @object = @project_enumeration = model.find(params[:id]) 203 | end 204 | rescue ActiveRecord::RecordNotFound 205 | render_404 206 | end 207 | 208 | def find_custom_field_by_custom_field_id 209 | @custom_field = CustomField.find(params[:custom_field_id]) 210 | rescue ActiveRecord::RecordNotFound 211 | render_404 212 | end 213 | 214 | def find_project_enumerations_for_custom_field(custom_field_id) 215 | enumeration_custom_field_ids_enabled_on_project = CustomField.enabled_on_project(@project).where(:field_format => 'project_enumeration').pluck(:id) 216 | 217 | if enumeration_custom_field_ids_enabled_on_project.include?(custom_field_id) 218 | @project_enumerations = ProjectEnumeration.where(:custom_field_id => custom_field_id).where(:project_id => @project.id).for_enumerations.order_by_custom_field_then_position 219 | else 220 | @project_enumerations = [] 221 | end 222 | end 223 | 224 | def update_each_params 225 | # params.require(:project_enumerations).permit(:value, :status, :sharing, :position) does not work here with param like this: 226 | # "project_enumerations":{"0":{"name": ...}, "1":{"name...}} 227 | 228 | filtered_params = {} 229 | params[:project_enumerations].each do |id, v| 230 | params_for_enumeration = {} 231 | v.each do |id, v| 232 | next unless ['value', 'status', 'sharing', 'position'].include?(id) 233 | params_for_enumeration[id] = v 234 | end 235 | 236 | filtered_params[id] = params_for_enumeration 237 | end 238 | =begin 239 | params.permit(:project_enumerations => [:value, :status, :sharing, :position]). 240 | require(:project_enumerations) 241 | =end 242 | filtered_params 243 | end 244 | 245 | def project_enumeration_custom_field_title(custom_field) 246 | items = [] 247 | 248 | items << [l(:label_project_enumeration_plural), settings_project_path(@project, :tab => 'project_enumerations')] 249 | items << (custom_field.nil? || custom_field.new_record? ? l(:label_custom_field_new) : custom_field.name) 250 | 251 | if Rails.version >= '5' 252 | helpers.title(*items) 253 | else 254 | view_context.title(*items) 255 | end 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /app/controllers/project_project_list_values_controller.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2017 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | class ProjectProjectListValuesController < ApplicationController 19 | menu_item :settings 20 | model_object ProjectEnumeration 21 | 22 | before_action :find_model_object, :except => [:index, :new, :create, :update_each] 23 | before_action :find_project_from_association, :except => [:index, :new, :create, :update_each] 24 | before_action :find_project_by_project_id, :only => [:index, :new, :create, :update_each] 25 | before_action :find_custom_field_by_custom_field_id, :only => [:index, :new, :create, :update_each] 26 | 27 | before_action :authorize 28 | 29 | 30 | # TODO create API views 31 | accept_api_auth :index, :create, :update, :destroy 32 | 33 | helper :projects, :custom_fields 34 | helper_method :project_list_value_custom_field_title 35 | 36 | 37 | def index 38 | find_project_list_values_for_custom_field(@custom_field.id) 39 | 40 | @project_list_value = ProjectEnumeration.new( 41 | :project_id => @project.id, 42 | :custom_field_id => @custom_field.id 43 | ) 44 | respond_to do |format| 45 | format.html do 46 | end 47 | format.api 48 | end 49 | end 50 | 51 | def new 52 | @project_list_value = ProjectEnumeration.new( 53 | :project_id => @project.id, 54 | :custom_field_id => @custom_field.id 55 | ) 56 | 57 | @project_list_value.safe_attributes = params[:project_list_value] 58 | 59 | respond_to do |format| 60 | format.html 61 | format.js 62 | end 63 | end 64 | 65 | def create 66 | find_project_list_values_for_custom_field(@custom_field.id) 67 | 68 | if @project_list_values.is_a?(Array) 69 | max_position = 0 70 | else 71 | max_position = @project_list_values.maximum(:position) 72 | end 73 | 74 | max_position ||= 0 75 | max_position += 1 76 | 77 | @project_list_value = ProjectEnumeration.new( 78 | :project_id => @project.id, 79 | :custom_field_id => @custom_field.id, 80 | :position => max_position 81 | ) 82 | 83 | if params[:project_list_value] 84 | attributes = params[:project_list_value].dup 85 | attributes.delete('sharing') unless attributes.nil? || @project_list_value.allowed_sharings.include?(attributes['sharing']) 86 | 87 | if Rails.version < '5' 88 | # Avoid ActiveModel::ForbiddenAttributesError 89 | attributes.permit! 90 | end 91 | 92 | @project_list_value.safe_attributes = attributes 93 | end 94 | 95 | if request.post? 96 | if @project_list_value.save 97 | respond_to do |format| 98 | format.html do 99 | flash[:notice] = l(:notice_successful_create) 100 | redirect_back_or_default settings_project_path(@project, :tab => 'project_list_values') 101 | end 102 | format.js { 103 | find_project_list_values_for_custom_field(@custom_field.id) 104 | render :action => 'create' 105 | } 106 | format.api do 107 | render :action => 'show', :status => :created, :location => project_project_list_values_url(@project_list_value) 108 | end 109 | end 110 | else 111 | respond_to do |format| 112 | format.html { render :action => 'new' } 113 | format.js { 114 | find_project_list_values_for_custom_field(@custom_field.id) 115 | 116 | # Render errors to flash message 117 | error_msg = @project_list_value.errors.full_messages 118 | 119 | if error_msg.any? 120 | flash[:error] = error_msg.join('
'.html_safe) 121 | end 122 | 123 | render :action => 'create' 124 | } 125 | format.api { render_validation_errors(@project_list_value) } 126 | end 127 | end 128 | end 129 | end 130 | 131 | def edit 132 | end 133 | 134 | def update 135 | if params[:project_list_value] 136 | attributes = params[:project_list_value].dup 137 | attributes.delete('sharing') unless @project_list_value.allowed_sharings.include?(attributes['sharing']) 138 | 139 | @project_list_value.safe_attributes = attributes 140 | if @project_list_value.save 141 | respond_to do |format| 142 | format.html { 143 | flash[:notice] = l(:notice_successful_update) 144 | redirect_back_or_default settings_project_path(@project, :tab => 'project_list_values') 145 | } 146 | format.api { render_api_ok } 147 | end 148 | else 149 | respond_to do |format| 150 | format.html { render :action => 'edit' } 151 | format.api { render_validation_errors(@project_list_value) } 152 | end 153 | end 154 | end 155 | end 156 | 157 | def update_each 158 | project_shared_list_values = @project.shared_list_values.to_a 159 | saved = ProjectEnumeration.update_each(@project, update_each_params, project_shared_list_values) 160 | 161 | if saved 162 | flash[:notice] = l(:notice_successful_update) 163 | else 164 | # Render errors to flash message 165 | 166 | error_msg = [] 167 | project_shared_list_values.each do |pe| 168 | pe.errors.full_messages.each do |m| 169 | error_msg << "#{pe.value} : #{m} (#{l(:field_position)} #{pe.position})" 170 | end 171 | end 172 | 173 | if error_msg.any? 174 | flash[:error] = error_msg.join('
'.html_safe) 175 | end 176 | end 177 | 178 | redirect_to :action => 'index', :custom_field_id => @custom_field.id 179 | end 180 | 181 | def destroy 182 | @project_list_value.destroy 183 | custom_field_id = @project_list_value.custom_field_id 184 | respond_to do |format| 185 | format.html { 186 | flash[:notice] = l(:notice_successful_delete) 187 | redirect_back_or_default project_project_list_values_path(@project, :custom_field_id => custom_field_id) 188 | } 189 | format.api { render_api_ok } 190 | format.js { 191 | flash[:notice] = l(:notice_successful_delete) 192 | redirect_back_or_default project_project_list_values_path(@project, :custom_field_id => custom_field_id) 193 | } 194 | end 195 | end 196 | 197 | 198 | protected 199 | 200 | def find_model_object 201 | model = self.class.model_object 202 | if model 203 | @object = @project_list_value = model.find(params[:id]) 204 | end 205 | rescue ActiveRecord::RecordNotFound 206 | render_404 207 | end 208 | 209 | def find_custom_field_by_custom_field_id 210 | @custom_field = CustomField.find(params[:custom_field_id]) 211 | rescue ActiveRecord::RecordNotFound 212 | render_404 213 | end 214 | 215 | def find_project_list_values_for_custom_field(custom_field_id) 216 | list_value_custom_field_ids_enabled_on_project = CustomField.enabled_on_project(@project).where(:field_format => 'project_list_value').pluck(:id) 217 | 218 | if list_value_custom_field_ids_enabled_on_project.include?(custom_field_id) 219 | @project_list_values = ProjectEnumeration.where(:custom_field_id => custom_field_id).where(:project_id => @project.id).for_list_values.order_by_custom_field_then_position 220 | else 221 | @project_list_values = [] 222 | end 223 | end 224 | 225 | def update_each_params 226 | # params.require(:project_list_values).permit(:value, :status, :sharing, :position) does not work here with param like this: 227 | # "project_list_values":{"0":{"name": ...}, "1":{"name...}} 228 | 229 | filtered_params = {} 230 | params[:project_list_values].each do |id, v| 231 | params_for_list_value = {} 232 | v.each do |id, v| 233 | next unless ['value', 'status', 'sharing', 'position'].include?(id) 234 | params_for_list_value[id] = v 235 | end 236 | 237 | filtered_params[id] = params_for_list_value 238 | end 239 | =begin 240 | params.permit(:project_list_values => [:value, :status, :sharing, :position]). 241 | require(:project_list_values) 242 | =end 243 | filtered_params 244 | end 245 | 246 | def project_list_value_custom_field_title(custom_field) 247 | items = [] 248 | 249 | items << [l(:label_project_list_value_plural), settings_project_path(@project, :tab => 'project_list_values')] 250 | items << (custom_field.nil? || custom_field.new_record? ? l(:label_custom_field_new) : custom_field.name) 251 | 252 | if Rails.version >= '5' 253 | helpers.title(*items) 254 | else 255 | view_context.title(*items) 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /app/models/project_enumeration.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2017 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | class ProjectEnumeration < ActiveRecord::Base 19 | include Redmine::SafeAttributes 20 | 21 | belongs_to :project 22 | belongs_to :custom_field 23 | 24 | ENUMERATION_STATUSES = %w(open locked closed) 25 | ENUMERATION_SHARINGS = %w(none descendants hierarchy tree system) 26 | 27 | validates_presence_of :value 28 | validates_uniqueness_of :value, :scope => [:project_id] 29 | validates_length_of :value, :maximum => 255 30 | 31 | validates_presence_of :custom_field 32 | 33 | validates_inclusion_of :status, :in => ENUMERATION_STATUSES 34 | validates_inclusion_of :sharing, :in => ENUMERATION_SHARINGS 35 | 36 | scope :valued, lambda {|arg| where("LOWER(#{table_name}.value) = LOWER(?)", arg.to_s.strip)} 37 | scope :like, lambda {|arg| 38 | if arg.present? 39 | pattern = "%#{arg.to_s.strip}%" 40 | where([Redmine::Database.like("#{Version.table_name}.value", '?'), pattern]) 41 | end 42 | } 43 | 44 | scope :open, lambda { where(:status => 'open') } 45 | scope :status, lambda {|status| 46 | if status.present? 47 | where(:status => status.to_s) 48 | end 49 | } 50 | 51 | scope :visible, lambda {|*args| 52 | joins(:project). 53 | where(Project.allowed_to_condition(args.first || User.current, :view_issues)) 54 | } 55 | 56 | scope :order_by_custom_field_then_position, lambda { joins(:custom_field).order('custom_fields.name, position') } 57 | 58 | scope :for_enumerations, lambda { joins(:custom_field).where('custom_fields.field_format' => 'project_enumeration') } 59 | 60 | scope :for_list_values, lambda { joins(:custom_field).where('custom_fields.field_format' => 'project_list_value') } 61 | 62 | safe_attributes 'value', 63 | 'status', 64 | 'sharing', 65 | 'custom_field_id', 66 | 'position' 67 | 68 | # Returns true if +user+ or current user is allowed to view the enumerations 69 | def visible?(user=User.current) 70 | user.allowed_to?(:view_issues, self.project) 71 | end 72 | 73 | def closed? 74 | status == 'closed' 75 | end 76 | 77 | def open? 78 | status == 'open' 79 | end 80 | 81 | def name; value end 82 | 83 | def to_s; value end 84 | 85 | def to_s_with_project 86 | "#{project} - #{value}" 87 | end 88 | 89 | # Enumerations are sorted by value 90 | def <=>(enumeration) 91 | value == enumeration.name ? id <=> enumeration.id : value <=> enumeration.name 92 | end 93 | 94 | # Sort Enumerations by status (open, locked then closed enumerations) 95 | def self.sort_by_status(enumerations) 96 | enumerations.sort do |a, b| 97 | if a.status == b.status 98 | a <=> b 99 | else 100 | b.status <=> a.status 101 | end 102 | end 103 | end 104 | 105 | # TODO add specific enumerations css 106 | # TODO css_classes needed for enumerations ? 107 | def css_classes 108 | [ 109 | "version-#{status}" 110 | ].join(' ') 111 | end 112 | 113 | def self.fields_for_order_statement(table=nil) 114 | table ||= table_name 115 | [ 116 | "#{table}.position, #{table}.value", "#{table}.id" 117 | ] 118 | end 119 | 120 | scope :sorted, lambda { order(fields_for_order_statement) } 121 | 122 | # Returns the sharings that +user+ can set the enumeration to 123 | def allowed_sharings(user = User.current) 124 | ENUMERATION_SHARINGS.select do |s| 125 | if sharing == s 126 | true 127 | else 128 | case s 129 | when 'system' 130 | # Only admin users can set a systemwide sharing 131 | user.admin? 132 | when 'hierarchy', 'tree' 133 | # Only users allowed to edit the root project can 134 | # set sharing to hierarchy or tree 135 | project.nil? || user.allowed_to?(:edit_project, project.root) 136 | else 137 | true 138 | end 139 | end 140 | end 141 | end 142 | 143 | # Returns true if the enumeration is shared, otherwise false 144 | def shared? 145 | sharing != 'none' 146 | end 147 | 148 | def self.update_each(project, attributes, project_shared_enumerations) 149 | transaction do 150 | attributes.each do |project_enumeration_id, project_enumeration_attributes| 151 | project_enumeration = project_shared_enumerations.find{|pe| pe.id.to_s == project_enumeration_id} 152 | if project_enumeration 153 | if block_given? 154 | yield project_enumeration, project_enumeration_attributes 155 | else 156 | project_enumeration.safe_attributes = project_enumeration_attributes 157 | end 158 | unless project_enumeration.save 159 | raise ActiveRecord::Rollback 160 | end 161 | end 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /app/views/custom_fields/formats/_project_enumeration.html.erb: -------------------------------------------------------------------------------- 1 |

2 | 3 | 8 | 13 | <% ProjectEnumeration::ENUMERATION_STATUSES.each do |status| %> 14 | 21 | <% end %> 22 | <%= hidden_field_tag 'custom_field[version_status][]', '' %> 23 |

24 |

<%= edit_tag_style_tag f %>

25 | -------------------------------------------------------------------------------- /app/views/custom_fields/formats/_project_list_value.html.erb: -------------------------------------------------------------------------------- 1 |

2 | 3 | 8 | 13 | <% ProjectEnumeration::ENUMERATION_STATUSES.each do |status| %> 14 | 21 | <% end %> 22 | <%= hidden_field_tag 'custom_field[version_status][]', '' %> 23 |

24 |

<%= edit_tag_style_tag f %>

25 | -------------------------------------------------------------------------------- /app/views/project_project_enumerations/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= back_url_hidden_field_tag %> 2 | <%= error_messages_for 'project_enumeration' %> 3 | 4 | <%= hidden_field_tag :custom_field_id, @project_enumeration.custom_field_id %> 5 | 6 |
7 |

<%= f.text_field :value, :maxlength => 80, :size => 80, :required => true %>

8 | 9 |

<%= f.select :status, ProjectEnumeration::ENUMERATION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %>

10 | 11 |

<%= f.select :sharing, @project_enumeration.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %>

12 |
13 | -------------------------------------------------------------------------------- /app/views/project_project_enumerations/create.js.erb: -------------------------------------------------------------------------------- 1 | $('#content').html('<%= escape_javascript(render(:template => 'project_project_enumerations/index')) %>'); 2 | value_elt = $('#project_enumeration_value'); 3 | value_elt.focus(); 4 | value_elt.effect("highlight"); 5 | 6 | $('#flash_placeholder').before('<%= escape_javascript(render_flash_messages) %>'); 7 | <% 8 | flash.delete(:notice) 9 | flash.delete(:error) 10 | -%> -------------------------------------------------------------------------------- /app/views/project_project_enumerations/edit.html.erb: -------------------------------------------------------------------------------- 1 |

<%=l(:label_project_enumeration)%>

2 | 3 | <%= labelled_form_for @project_enumeration, :url => {:controller => :project_project_enumerations, :action => :update, :id => @project_enumeration.id}, :html => {:multipart => true} do |f| %> 4 | <%= render :partial => 'form', :locals => { :f => f } %> 5 | <%= submit_tag l(:button_save) %> 6 | <% end %> 7 | 8 | -------------------------------------------------------------------------------- /app/views/project_project_enumerations/index.api.rsb: -------------------------------------------------------------------------------- 1 | api.array :project_enumerations do 2 | @project_enumerations.each do |entity| 3 | api.entity do 4 | api.id entity.id 5 | api.value entity.value 6 | api.status entity.status 7 | api.sharing entity.sharing 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /app/views/project_project_enumerations/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag 'style.css', :plugin => 'redmine_smile_project_enumerations_custom_field_format', :media => 'all' %> 2 | 3 | 4 | <% 5 | if User.current.admin? 6 | -%> 7 | <%= custom_field_title @custom_field %> 8 | <% 9 | else 10 | -%> 11 | <%= project_enumeration_custom_field_title(@custom_field) %> 12 | <% 13 | end 14 | -%> 15 | 16 | <% if @project_enumerations.any? %> 17 | <%= form_tag update_each_project_project_enumerations_path(@project, :custom_field_id => @custom_field.id), :method => :put do %> 18 |
19 | 54 |
55 |

56 | <%= submit_tag(l(:button_save)) %> | 57 | <%= link_to l(:button_back), settings_project_path(@project, :tab => 'project_enumerations') %> 58 |

59 | <% 60 | end # form_tag ... 61 | else 62 | -%> 63 |

<%= l(:label_no_data) %>

64 | <% 65 | end # if @project_enumerations.any? 66 | -%> 67 | 68 |
69 |

<%= l(:label_project_enumeration_new) %>

70 | 71 | <%= form_for @project_enumeration, :url => create_project_project_enumerations_path(:custom_field_id => @project_enumeration.custom_field_id), :method => :post, :remote => true do |f| %> 72 | <%= f.hidden_field :project_id, :value => @project_enumeration.project_id %> 73 |

<%= text_field_tag 'project_enumeration[value]', '', :size => 40 %>

74 |

<%= l(:field_status) %> : <%= f.select :status, ProjectEnumeration::ENUMERATION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %>

75 |

<%= l(:field_sharing) %> : <%= f.select :sharing, @project_enumeration.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %>

76 | <%= submit_tag(l(:button_add)) %>

77 | <% end %> 78 | 79 | <%= javascript_tag do %> 80 | $(function() { 81 | $("#project_enumerations").sortable({ 82 | handle: ".sort-handle", 83 | update: function(event, ui) { 84 | $("#project_enumerations li").each(function(){ 85 | $(this).find("input.position").val($(this).index()+1); 86 | }); 87 | } 88 | }); 89 | }); 90 | <% end %> 91 | -------------------------------------------------------------------------------- /app/views/project_project_enumerations/new.html.erb: -------------------------------------------------------------------------------- 1 |

<%=l(:label_project_enumeration_new)%>

2 | 3 | <%= labelled_form_for @project_enumeration, :url => {:controller => :project_project_enumerations, :action => :create}, :html => {:multipart => true} do |f| %> 4 | <%= render :partial => 'form', :locals => { :f => f } %> 5 | <%= submit_tag l(:button_create) %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/project_project_enumerations/show.api.rsb: -------------------------------------------------------------------------------- 1 | api.project_enumeration do 2 | api.id @project_enumeration.id 3 | api.value @project_enumeration.value 4 | api.status @project_enumeration.status 5 | api.sharing @project_enumeration.sharing 6 | end -------------------------------------------------------------------------------- /app/views/project_project_list_values/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= back_url_hidden_field_tag %> 2 | <%= error_messages_for 'project_list_value' %> 3 | 4 | <%= hidden_field_tag :custom_field_id, @project_list_value.custom_field_id %> 5 | 6 |
7 |

<%= f.text_field :value, :maxlength => 80, :size => 80, :required => true %>

8 | 9 |

<%= f.select :status, ProjectEnumeration::ENUMERATION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %>

10 | 11 |

<%= f.select :sharing, @project_list_value.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %>

12 |
13 | -------------------------------------------------------------------------------- /app/views/project_project_list_values/create.js.erb: -------------------------------------------------------------------------------- 1 | $('#content').html('<%= escape_javascript(render(:template => 'project_project_list_values/index')) %>'); 2 | value_elt = $('#project_list_value_value'); 3 | value_elt.focus(); 4 | value_elt.effect("highlight"); 5 | 6 | $('#flash_placeholder').before('<%= escape_javascript(render_flash_messages) %>'); 7 | <% 8 | flash.delete(:notice) 9 | flash.delete(:error) 10 | -%> -------------------------------------------------------------------------------- /app/views/project_project_list_values/edit.html.erb: -------------------------------------------------------------------------------- 1 |

<%=l(:label_project_list_value)%>

2 | 3 | <%= labelled_form_for @project_list_value, :url => {:controller => :project_project_list_values, :action => :update, :id => @project_list_value.id}, :html => {:multipart => true} do |f| %> 4 | <%= render :partial => 'form', :locals => { :f => f } %> 5 | <%= submit_tag l(:button_save) %> 6 | <% end %> 7 | 8 | -------------------------------------------------------------------------------- /app/views/project_project_list_values/index.api.rsb: -------------------------------------------------------------------------------- 1 | api.array :project_list_values do 2 | @project_list_values.each do |entity| 3 | api.entity do 4 | api.id entity.id 5 | api.value entity.value 6 | api.status entity.status 7 | api.sharing entity.sharing 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /app/views/project_project_list_values/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <% 3 | if User.current.admin? 4 | -%> 5 | <%= custom_field_title @custom_field %> 6 | <% 7 | else 8 | -%> 9 | <%= project_list_value_custom_field_title(@custom_field) %> 10 | <% 11 | end 12 | -%> 13 | 14 | <% if @project_list_values.any? %> 15 | <%= form_tag update_each_project_project_list_values_path(@project, :custom_field_id => @custom_field.id), :method => :put do %> 16 |
17 |
    18 | <% 19 | @project_list_values.each_with_index do |project_list_value, position| 20 | -%> 21 |
  • 22 | 23 | <%= hidden_field_tag "project_list_values[#{project_list_value.id}][id]", project_list_value.id %> 24 | <%= hidden_field_tag "project_list_values[#{project_list_value.id}][position]", position, :class => 'position' %> 25 | 26 | <%= text_field_tag "project_list_values[#{project_list_value.id}][value]", project_list_value.value, :size => 80 %> 27 | <%= delete_link destroy_project_project_list_values_path(@project, :id => project_list_value) %> 28 | 29 |      30 | 34 | 35 |    36 | 40 | <% 41 | if project_list_value.project_id != @project.id 42 | -%> 43 | 44 |    45 | <%= l(:field_project) %> : <%= @project_list_value.project.name %> 46 | <% 47 | end 48 | -%> 49 |
  • 50 | <% end %> 51 |
52 |
53 |

54 | <%= submit_tag(l(:button_save)) %> | 55 | <%= link_to l(:button_back), settings_project_path(@project, :tab => 'project_list_values') %> 56 |

57 | <% 58 | end # form_tag ... 59 | else 60 | -%> 61 |

<%= l(:label_no_data) %>

62 | <% 63 | end # if @project_list_values.any? 64 | -%> 65 | 66 |
67 |

<%= l(:label_project_list_value_new) %>

68 | 69 | <%= form_for @project_list_value, :url => create_project_project_list_values_path(:custom_field_id => @project_list_value.custom_field_id), :method => :post, :remote => true do |f| %> 70 | <%= f.hidden_field :project_id, :value => @project_list_value.project_id %> 71 |

<%= text_field_tag 'project_list_value[value]', '', :size => 40 %>

72 |

<%= l(:field_status) %> : <%= f.select :status, ProjectEnumeration::ENUMERATION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %>

73 |

<%= l(:field_sharing) %> : <%= f.select :sharing, @project_list_value.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %>

74 | <%= submit_tag(l(:button_add)) %>

75 | <% end %> 76 | 77 | <%= javascript_tag do %> 78 | $(function() { 79 | $("#project_list_values").sortable({ 80 | handle: ".sort-handle", 81 | update: function(event, ui) { 82 | $("#project_list_values li").each(function(){ 83 | $(this).find("input.position").val($(this).index()+1); 84 | }); 85 | } 86 | }); 87 | }); 88 | <% end %> 89 | -------------------------------------------------------------------------------- /app/views/project_project_list_values/new.html.erb: -------------------------------------------------------------------------------- 1 |

<%=l(:label_project_list_value_new)%>

2 | 3 | <%= labelled_form_for @project_list_value, :url => {:controller => :project_project_list_values, :action => :create}, :html => {:multipart => true} do |f| %> 4 | <%= render :partial => 'form', :locals => { :f => f } %> 5 | <%= submit_tag l(:button_create) %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /app/views/project_project_list_values/show.api.rsb: -------------------------------------------------------------------------------- 1 | api.project_list_value do 2 | api.id @project_list_value.id 3 | api.value @project_list_value.value 4 | api.status @project_list_value.status 5 | api.sharing @project_list_value.sharing 6 | end -------------------------------------------------------------------------------- /app/views/projects/settings/_custom_field_checkbox.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Smile specific : 3 | # RM V4.0.0 OK 4 | -%> 5 | -------------------------------------------------------------------------------- /app/views/projects/settings/_issues.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Plugin specific 3 | # * #763230: Project Custom Fields configuration : split by tracker 4 | # 2019-02 5 | -%> 6 | <%= labelled_form_for @project do |f| %> 7 | <%= hidden_field_tag 'tab', 'issues' %> 8 | 9 | <% unless @trackers.empty? %> 10 |
<%= toggle_checkboxes_link('#project_trackers input[type=checkbox]') %><%= l(:label_tracker_plural)%> 11 | <% @trackers.each do |tracker| %> 12 | 32 | <% end %> 33 | <%= hidden_field_tag 'project[tracker_ids][]', '' %> 34 |
35 | <% end %> 36 | 37 | <% unless @issue_custom_fields.empty? %> 38 | <% 39 | ################# 40 | # Plugin specific : Project Custom Fields configuration : split by tracker 41 | all_issue_custom_fields = @project.all_issue_custom_fields 42 | # Plugin specific : spliting in blocks depending if C.F. is specific to a tracker or not 43 | # Plugin specific : toggle_checkboxes_link removed 44 | -%> 45 |
<%=l(:label_custom_field_plural)%> 46 |
<%= l(:label_tracker_all) %> 47 | <% @issue_custom_fields.select(&:is_for_all?).each do |custom_field| %> 48 | <%= render :partial => 'projects/settings/custom_field_checkbox', :locals => {:custom_field => custom_field, :all_issue_custom_fields => all_issue_custom_fields} %> 49 | <% end %> 50 |
51 | 52 |
<%= toggle_checkboxes_link('#project_issue_custom_fields_multiple input[type=checkbox]:enabled') %><%= l(:label_tracker_plural) %> : <%= l(:field_multiple) %> 53 | <% @issue_custom_fields.select{|cf| cf.trackers.size > 1}.each do |custom_field| %> 54 | <%= render :partial => 'projects/settings/custom_field_checkbox', :locals => {:custom_field => custom_field, :all_issue_custom_fields => all_issue_custom_fields} %> 55 | <% end %> 56 |
57 | 58 | <% 59 | single_tracker_issue_custom_fields = @issue_custom_fields.select{|cf| cf.trackers.size == 1} 60 | if single_tracker_issue_custom_fields.any? 61 | -%> 62 |
<%=l(:label_tracker)%> 63 | <% 64 | Tracker.sorted.each do |t| 65 | issue_custom_fields_for_tracker = single_tracker_issue_custom_fields.select{|cf| cf.trackers.first == t} 66 | if issue_custom_fields_for_tracker.any? 67 | -%> 68 |
<%= toggle_checkboxes_link("#project_issue_custom_fields_tracker_#{t.id} input[type=checkbox]:enabled") %><%= t.name %> 69 | <% issue_custom_fields_for_tracker.each do |custom_field| %> 70 | <%= render :partial => 'projects/settings/custom_field_checkbox', :locals => {:custom_field => custom_field, :all_issue_custom_fields => all_issue_custom_fields} %> 71 | <% end %> 72 |
73 | <% end %> 74 | <% end %> 75 |
76 | <% end %> 77 | <% 78 | # END -- Plugin specific : Project Custom Fields configuration : split by tracker 79 | ######################## 80 | -%> 81 | <%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %> 82 |
83 | 84 | <%= call_hook(:view_project_settings_issues_custom_fields, { :issue_custom_fields => @issue_custom_fields, :project => @project }) %> 85 | <% end %> 86 | 87 |
88 | <% if @project.safe_attribute?('default_version_id') %> 89 |

<%= f.select :default_version_id, project_default_version_options(@project), include_blank: l(:label_none) %>

90 | <% end %> 91 | 92 | <% if @project.safe_attribute?('default_assigned_to_id') %> 93 |

<%= f.select :default_assigned_to_id, project_default_assigned_to_options(@project), include_blank: l(:label_none) %>

94 | <% end %> 95 |
96 | 97 |

<%= submit_tag l(:button_save) %>

98 | <% end %> 99 | -------------------------------------------------------------------------------- /app/views/projects/settings/_project_enumerations.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | if User.current.allowed_to?(:manage_project_enumerations, @project) 3 | -%> 4 |

5 | <% 6 | @enumeration_custom_fields_enabled_on_project_options.each do |cf_name, cf_id| 7 | -%> 8 | <%= cf_name %> : 9 | <%= link_to "#{ l(:label_enumeration_new) }", project_project_enumerations_path(@project, :custom_field_id => cf_id, :back_url => ''), :class => 'icon icon-add' %> 10 |
11 | <% 12 | end 13 | 14 | if @enumeration_custom_fields_not_enabled_on_project.any? 15 | -%> 16 |
17 | <%= l(:label_not_enabled_on_project) %> :
18 | <% 19 | @enumeration_custom_fields_not_enabled_on_project.each do |cf| 20 | -%> 21 | <%= cf.name %> 22 |
23 | <% 24 | end 25 | end 26 | -%> 27 |

28 | <% 29 | end 30 | 31 | if @project_enumerations.any? 32 | -%> 33 |
34 |
35 | <% end %> 36 | <%= form_tag(settings_project_path(@project, :tab => 'project_enumerations'), :method => :get) do %> 37 |
<%= l(:label_filter_plural) %> 38 | 39 | 40 | <%= select_tag 'enumeration_custom_field_id', options_for_select([[l(:label_all), '']] + @enumeration_custom_fields_enabled_on_project_options, @enumeration_custom_field_id), :onchange => "this.form.submit(); return false;" %> 41 | 42 |   43 | <%= text_field_tag 'enumeration_value', @enumeration_value, :size => 30 %> 44 | 45 |   46 | <%= select_tag 'enumeration_status', options_for_select([[l(:label_all), '']] + ProjectEnumeration::ENUMERATION_STATUSES.collect {|s| [l("version_status_#{s}"), s]}, @enumeration_status), :onchange => "this.form.submit(); return false;" %> 47 | 48 |   49 | <%= select_tag 'enumeration_sharing', options_for_select([[l(:label_all), '']] + ProjectEnumeration::ENUMERATION_SHARINGS.collect {|s| [l("label_version_sharing_#{s}"), s]}, @enumeration_sharing), :onchange => "this.form.submit(); return false;" %> 50 | 51 | <%= submit_tag l(:button_apply), :name => nil %> 52 | <%= link_to l(:button_clear), settings_project_path(@project, :tab => 'project_enumerations'), :class => 'icon icon-reload' %> 53 |
54 | <% end %> 55 |   56 | 57 | <% if @project_enumerations.present? %> 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | <% 71 | @project_enumerations.each do |project_enumeration| 72 | shared_enumeration = (project_enumeration.project != @project) 73 | 74 | project_enumeration_value = ERB::Util.html_escape(project_enumeration.to_s) 75 | if shared_enumeration 76 | project_enumeration_value = ( 77 | project_enumeration_value + 78 | ':  ('.html_safe + 79 | link_to("#{l(:field_project)} : #{project_enumeration.project.name}".html_safe, project_project_enumerations_path(project_enumeration.project, :custom_field_id =>project_enumeration.custom_field_id)).html_safe + 80 | ')' 81 | ).html_safe 82 | end 83 | -%> 84 | 85 | 86 | 87 | 88 | 89 | 90 | <% 91 | # TODO add format_enumeration_sharing 92 | -%> 93 | 94 | 95 | 105 | 106 | <% end %> 107 | 108 |
<%= l(:label_custom_field) %><%= l(:label_project_enumeration_value) %><%= l(:field_status) %><%= l(:field_sharing) %>
<%= custom_field_name_tag(project_enumeration.custom_field) %><%= project_enumeration_value %><%= l("version_status_#{project_enumeration.status}") %> 96 | <% 97 | if !shared_enumeration && User.current.allowed_to?(:manage_project_enumerations, @project) 98 | -%> 99 | <%= link_to l(:button_edit), project_project_enumerations_path(@project, :custom_field_id => project_enumeration.custom_field_id), :class => 'icon icon-edit' %> 100 | <%= delete_link destroy_project_project_enumerations_path(@project, :id => project_enumeration) %> 101 | <% 102 | end 103 | -%> 104 |
109 | <% else %> 110 |

<%= l(:label_no_data) %>

111 | <% end %> 112 | -------------------------------------------------------------------------------- /app/views/projects/settings/_project_list_values.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | if User.current.allowed_to?(:manage_project_enumerations, @project) 3 | -%> 4 |

5 | <% 6 | @list_value_custom_fields_enabled_on_project_options.each do |cf_name, cf_id| 7 | -%> 8 | <%= cf_name %> : 9 | <%= link_to "#{ l(:label_project_list_value_new) } (#{cf_name})", project_project_list_values_path(@project, :custom_field_id => cf_id, :back_url => ''), :class => 'icon icon-add' %> 10 |
11 | <% 12 | end 13 | 14 | if @list_value_custom_fields_not_enabled_on_project.any? 15 | -%> 16 |
17 | <%= l(:label_not_enabled_on_project) %> :
18 | <% 19 | @list_value_custom_fields_not_enabled_on_project.each do |cf| 20 | -%> 21 | <%= cf.name %> 22 |
23 | <% 24 | end 25 | end 26 | -%> 27 |

28 | <% 29 | end 30 | 31 | 32 | if @project_list_values.any? 33 | -%> 34 |
35 |
36 | <% end %> 37 | <%= form_tag(settings_project_path(@project, :tab => 'project_list_values'), :method => :get) do %> 38 |
<%= l(:label_filter_plural) %> 39 | 40 | 41 | <%= select_tag 'list_value_custom_field_id', options_for_select([[l(:label_all), '']] + @list_value_custom_fields_enabled_on_project_options, @list_value_custom_field_id), :onchange => "this.form.submit(); return false;" %> 42 | 43 |   44 | <%= text_field_tag 'list_value_value', @list_value_value, :size => 30 %> 45 | 46 |   47 | <%= select_tag 'list_value_status', options_for_select([[l(:label_all), '']] + ProjectEnumeration::ENUMERATION_STATUSES.collect {|s| [l("version_status_#{s}"), s]}, @list_value_status), :onchange => "this.form.submit(); return false;" %> 48 | 49 |   50 | <%= select_tag 'list_value_sharing', options_for_select([[l(:label_all), '']] + ProjectEnumeration::ENUMERATION_SHARINGS.collect {|s| [l("label_version_sharing_#{s}"), s]}, @list_value_sharing), :onchange => "this.form.submit(); return false;" %> 51 | 52 | <%= submit_tag l(:button_apply), :name => nil %> 53 | <%= link_to l(:button_clear), settings_project_path(@project, :tab => 'project_list_values'), :class => 'icon icon-reload' %> 54 |
55 | <% end %> 56 |   57 | 58 | <% if @project_list_values.present? %> 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | <% 72 | @project_list_values.each do |project_list_value| 73 | shared_list_value = (project_list_value.project != @project) 74 | 75 | project_list_value_value = ERB::Util.html_escape(project_list_value.to_s) 76 | if shared_list_value 77 | project_list_value_value = ( 78 | project_list_value_value + 79 | ':  ('.html_safe + 80 | link_to("#{l(:field_project)} : #{project_list_value.project.name}".html_safe, project_project_list_values_path(project_list_value.project, :custom_field_id =>project_list_value.custom_field_id)).html_safe + 81 | ')' 82 | ).html_safe 83 | end 84 | -%> 85 | 86 | 87 | 88 | 89 | 90 | 91 | <% 92 | # TODO add format_list_value_sharing 93 | -%> 94 | 95 | 96 | 106 | 107 | <% end %> 108 | 109 |
<%= l(:label_custom_field) %><%= l(:label_project_list_value_value) %><%= l(:field_status) %><%= l(:field_sharing) %>
<%= custom_field_name_tag(project_list_value.custom_field) %><%= project_list_value_value %><%= l("version_status_#{project_list_value.status}") %> 97 | <% 98 | if !shared_list_value && User.current.allowed_to?(:manage_project_enumerations, @project) 99 | -%> 100 | <%= link_to l(:button_edit), project_project_list_values_path(@project, :custom_field_id => project_list_value.custom_field_id), :class => 'icon icon-edit' %> 101 | <%= delete_link destroy_project_project_list_values_path(@project, :id => project_list_value) %> 102 | <% 103 | end 104 | -%> 105 |
110 | <% else %> 111 |

<%= l(:label_no_data) %>

112 | <% end %> 113 | -------------------------------------------------------------------------------- /app/views/settings/_redmine_smile_project_enumerations_custom_field_format.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | plugin_name = :redmine_smile_project_enumerations_custom_field_format 3 | 4 | plugin_override_last_date = SmileTools.override_last_date(plugin_name) 5 | 6 | if plugin_override_last_date.is_a?(Time) 7 | plugin_override_last_date = plugin_override_last_date.to_s 8 | end 9 | -%> 10 |
11 |
12 |

[<%= SmileTools.override_count(plugin_name) %>] override(s) added by the plugin (<%= plugin_override_last_date %>) :

13 | 14 | <%= SmileTools.override_traces(plugin_name).html_safe %> 15 |
16 | -------------------------------------------------------------------------------- /assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/redmine_smile_project_enumerations_custom_field_format/b75f7ff51bdcd79256af67dc3a52ef394cf0daf9/assets/images/loading.gif -------------------------------------------------------------------------------- /assets/images/reorder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/redmine_smile_project_enumerations_custom_field_format/b75f7ff51bdcd79256af67dc3a52ef394cf0daf9/assets/images/reorder.png -------------------------------------------------------------------------------- /assets/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | .sort-handle { width:16px; height:16px; background:url(../images/reorder.png) no-repeat 0 50%; cursor:move; } 2 | .sort-handle.ajax-loading { background-image: url(../images/loading.gif); } 3 | tr.ui-sortable-helper { border:1px solid #e4e4e4; } -------------------------------------------------------------------------------- /config/locales/ca.yml: -------------------------------------------------------------------------------- 1 | # Catalan translations for Project Enumerations Custom Field Format Plugin 2 | # by Jérôme BATAILLE (redmine-support@smile.fr) 3 | 4 | ca: 5 | label_project_enumeration: Project Enumeration 6 | label_project_enumeration_value: Value 7 | label_project_enumeration_edit_values: Edit Values 8 | label_project_enumeration_plural: Project Enumerations 9 | label_project_enumeration_new: New Project Enumeration value 10 | 11 | permission_manage_project_enumeration: Manage Project Enumerations 12 | 13 | label_project_list_value: Project List of Values 14 | label_project_list_value_value: Value 15 | label_project_list_value_edit_values: Edit Values 16 | label_project_list_value_plural: Project Lists of Values 17 | label_project_list_value_new: New Project List of Values 18 | 19 | label_not_enabled_on_project: Not enabled on project 20 | field_position: Position 21 | -------------------------------------------------------------------------------- /config/locales/en-GB.yml: -------------------------------------------------------------------------------- 1 | # English translations for Project Enumerations Custom Field Format Plugin 2 | # by Jérôme BATAILLE (redmine-support@smile.fr) 3 | 4 | en-GB: 5 | label_project_enumeration: Project Enumeration 6 | label_project_enumeration_value: Value 7 | label_project_enumeration_edit_values: Edit Values 8 | label_project_enumeration_plural: Project Enumerations 9 | label_project_enumeration_new: New Project Enumeration value 10 | 11 | permission_manage_project_enumeration: Manage Project Enumerations 12 | 13 | label_project_list_value: Project List of Values 14 | label_project_list_value_value: Value 15 | label_project_list_value_edit_values: Edit Values 16 | label_project_list_value_plural: Project Lists of Values 17 | label_project_list_value_new: New Project List of Values 18 | 19 | label_not_enabled_on_project: Not enabled on project 20 | field_position: Position 21 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English translations for Project Records Custom Field Format Plugin 2 | # by Jérôme BATAILLE (redmine-support@smile.fr) 3 | 4 | en: 5 | label_project_enumeration: Project Enumeration 6 | label_project_enumeration_value: Value 7 | label_project_enumeration_edit_values: Edit Values 8 | label_project_enumeration_plural: Project Enumerations 9 | label_project_enumeration_new: New Project Enumeration value 10 | 11 | permission_manage_project_enumeration: Manage Project Enumerations 12 | 13 | label_project_list_value: Project List of Values 14 | label_project_list_value_value: Value 15 | label_project_list_value_edit_values: Edit Values 16 | label_project_list_value_plural: Project Lists of Values 17 | label_project_list_value_new: New Project List of Values 18 | 19 | label_not_enabled_on_project: Not enabled on project 20 | field_position: Position 21 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | # Spanish translations for Project Enumerations Custom Field Format Plugin 2 | # by Jérôme BATAILLE (redmine-support@smile.fr) 3 | 4 | es: 5 | label_project_enumeration: Project Enumeration 6 | label_project_enumeration_value: Value 7 | label_project_enumeration_edit_values: Edit Values 8 | label_project_enumeration_plural: Project Enumerations 9 | label_project_enumeration_new: New Project Enumeration value 10 | 11 | permission_manage_project_enumeration: Manage Project Enumerations 12 | 13 | label_project_list_value: Project List of Values 14 | label_project_list_value_value: Value 15 | label_project_list_value_edit_values: Edit Values 16 | label_project_list_value_plural: Project Lists of Values 17 | label_project_list_value_new: New Project List of Values 18 | 19 | label_not_enabled_on_project: Not enabled on project 20 | field_position: Position 21 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | # French translations for Project Records Custom Field Format Plugin 2 | # by Jérôme BATAILLE (redmine-support@smile.fr) 3 | 4 | fr: 5 | label_project_enumeration: Liste d'Énumérations du Projet 6 | label_project_enumeration_value: Valeur 7 | label_project_enumeration_edit_values: Éditer les valeurs 8 | label_project_enumeration_plural: Listes d'Énumérations du Projet 9 | label_project_enumeration_new: Nouvelle valeur d'Énumérations 10 | 11 | permission_manage_project_enumerations: Gérer les Liste de valeurs du Projet 12 | 13 | label_project_list_value: Liste de Valeurs du Projet 14 | label_project_list_value_value: Valeur 15 | label_project_list_value_edit_values: Éditer les valeurs 16 | label_project_list_value_plural: Listes de Valeurs du Projet 17 | label_project_list_value_new: Nouvelle valeur de Liste 18 | 19 | label_not_enabled_on_project: Pas activé sur le project 20 | field_position: Position 21 | -------------------------------------------------------------------------------- /config/locales/uk.yml: -------------------------------------------------------------------------------- 1 | # Ukrainian translations for Project Enumerations Custom Field Format Plugin 2 | # by Jérôme BATAILLE (redmine-support@smile.fr) 3 | 4 | uk: 5 | label_project_enumeration: Project Enumeration 6 | label_project_enumeration_value: Value 7 | label_project_enumeration_edit_values: Edit Values 8 | label_project_enumeration_plural: Project Enumerations 9 | label_project_enumeration_new: New Project Enumeration 10 | 11 | permission_manage_project_enumeration: Manage Project Enumerations 12 | 13 | label_project_list_value: Project List of Values 14 | label_project_list_value_value: Value 15 | label_project_list_value_edit_values: Edit Values 16 | label_project_list_value_plural: Project Lists of Values 17 | label_project_list_value_new: New Project List of Values 18 | 19 | label_not_enabled_on_project: Not enabled on project 20 | field_position: Position 21 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | resources :projects do 2 | resource :project_enumerations, :controller => 'project_project_enumerations', :only => [:new, :edit] do 3 | collection do 4 | get 'index' 5 | end 6 | 7 | member do 8 | post 'create', :as => 'create' 9 | put 'update', :as => 'update' 10 | put 'update_each', :as => 'update_each' 11 | delete 'destroy', :as => 'destroy' 12 | end 13 | end 14 | 15 | resource :project_list_values, :controller => 'project_project_list_values', :only => [:new, :edit] do 16 | collection do 17 | get 'index' 18 | end 19 | 20 | member do 21 | post 'create', :as => 'create' 22 | put 'update', :as => 'update' 23 | put 'update_each', :as => 'update_each' 24 | delete 'destroy', :as => 'destroy' 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20190901140000_fix_migration_name.rb: -------------------------------------------------------------------------------- 1 | if Redmine::VERSION::MAJOR < 4 2 | migration = ActiveRecord::Migration 3 | else 4 | migration = ActiveRecord::Migration[4.2] 5 | end 6 | 7 | class FixMigrationName < migration 8 | def self.up 9 | execute "update schema_migrations set version='20190904123000-redmine_smile_project_enumerations_custom_field_format' where version='201909041230000-redmine_smile_project_enumerations_custom_field_format'" 10 | end 11 | 12 | def self.down 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20190904123000_create_project_enumerations.rb: -------------------------------------------------------------------------------- 1 | if Redmine::VERSION::MAJOR < 4 2 | migration = ActiveRecord::Migration 3 | else 4 | migration = ActiveRecord::Migration[4.2] 5 | end 6 | 7 | class CreateProjectEnumerations < migration 8 | def change 9 | create_table :project_enumerations do |t| 10 | t.integer :project_id, :null => false 11 | t.integer :custom_field_id, :null => false 12 | t.string :value 13 | t.string :status, :default => 'open' 14 | t.string :sharing, :default => 'none', :null => false 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20191204234500_add_project_enumeration_position.rb: -------------------------------------------------------------------------------- 1 | if Redmine::VERSION::MAJOR < 4 2 | migration = ActiveRecord::Migration 3 | else 4 | migration = ActiveRecord::Migration[4.2] 5 | end 6 | 7 | class AddProjectEnumerationPosition < migration 8 | def self.up 9 | add_column :project_enumerations, :position, :integer 10 | end 11 | 12 | def self.down 13 | remove_column :project_enumerations, :position 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /doc/Project_Enumeration_In_Issue_20191004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/redmine_smile_project_enumerations_custom_field_format/b75f7ff51bdcd79256af67dc3a52ef394cf0daf9/doc/Project_Enumeration_In_Issue_20191004.png -------------------------------------------------------------------------------- /doc/Project_Enumerations_CF_20191004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/redmine_smile_project_enumerations_custom_field_format/b75f7ff51bdcd79256af67dc3a52ef394cf0daf9/doc/Project_Enumerations_CF_20191004.png -------------------------------------------------------------------------------- /doc/Project_Enumerations_Edit_20191004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/redmine_smile_project_enumerations_custom_field_format/b75f7ff51bdcd79256af67dc3a52ef394cf0daf9/doc/Project_Enumerations_Edit_20191004.png -------------------------------------------------------------------------------- /doc/Project_Enumerations_Edit_enumeration_20191015.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smile-SA/redmine_smile_project_enumerations_custom_field_format/b75f7ff51bdcd79256af67dc3a52ef394cf0daf9/doc/Project_Enumerations_Edit_enumeration_20191015.png -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'redmine' 4 | 5 | ################### 6 | # 1/ Initialisation 7 | Rails.logger.info 'o=>' 8 | Rails.logger.info 'o=>Starting Redmine Smile Project Enumerations Custom Field Format plugin for Redmine' 9 | Rails.logger.info "o=>Application user : #{ENV['USER']}" 10 | 11 | 12 | plugin_name = :redmine_smile_project_enumerations_custom_field_format 13 | plugin_root = File.dirname(__FILE__) 14 | 15 | # lib/smile_tools 16 | require plugin_root + '/lib/smile_tools' 17 | 18 | Redmine::Plugin.register plugin_name do 19 | ######################## 20 | # 2/ Plugin informations 21 | name 'Redmine - Smile - Project Enumerations Custom Field Format' 22 | author 'Jérôme BATAILLE, Stéphane PARUNAKIAN' 23 | author_url "mailto:Jerome BATAILLE ?subject=#{plugin_name}" 24 | description 'Adds a new Custom Field Format that stores its values in project enumerations' 25 | url "https://github.com/Smile-SA/#{plugin_name}" 26 | version '1.3.15' 27 | requires_redmine :version_or_higher => '3.4' 28 | 29 | 30 | ####################### 31 | # 2.1/ Plugin home page 32 | settings :default => HashWithIndifferentAccess.new( 33 | ), 34 | :partial => "settings/#{plugin_name}" 35 | 36 | project_module :issue_tracking do 37 | permission :manage_project_enumerations, { 38 | :projects => :settings, 39 | :project_project_list_values => [:index, :new, :create, :edit, :update, :update_each, :destroy], 40 | :project_project_enumerations => [:index, :new, :create, :edit, :update, :update_each, :destroy] 41 | }, 42 | :require => :member 43 | end 44 | end # Redmine::Plugin.register ... 45 | 46 | 47 | ################################# 48 | # 3/ Plugin internal informations 49 | # To keep after plugin register 50 | this_plugin = Redmine::Plugin::find(plugin_name.to_s) 51 | plugin_version = '?.?' 52 | # Root relative to application root 53 | plugin_rel_root = '.' 54 | plugin_id = 0 55 | if this_plugin 56 | plugin_version = this_plugin.version 57 | plugin_id = this_plugin.__id__ 58 | plugin_rel_root = 'plugins/' + this_plugin.id.to_s 59 | end 60 | 61 | 62 | ############### 63 | # 4/ Dispatcher 64 | #Executed each time the classes are reloaded 65 | rails_dispatcher = Rails.configuration 66 | 67 | 68 | def prepend_in(dest, mixin_module) 69 | return if dest.include? mixin_module 70 | 71 | # Rails.logger.info "o=>#{dest}.prepend #{mixin_module}" 72 | dest.send(:prepend, mixin_module) 73 | end 74 | 75 | ############### 76 | # 5/ to_prepare 77 | # Executed after Rails initialization 78 | rails_dispatcher.after_initialize do 79 | Rails.logger.info "o=>" 80 | Rails.logger.info "o=>\\__ #{plugin_name} V#{plugin_version}" 81 | 82 | SmileTools.reset_override_count(plugin_name) 83 | 84 | SmileTools.trace_override " plugin #{plugin_name} V#{plugin_version}", 85 | false, 86 | plugin_name 87 | 88 | 89 | ######################################### 90 | # 5.1/ List of files required dynamically 91 | # Manage dependencies 92 | # To put here if we want recent source files reloaded 93 | # Outside of to_prepare, file changed => reloaded, 94 | # but with primary loaded source code 95 | required = [ 96 | # lib/ 97 | '/lib/project_enumeration_field_format', 98 | '/lib/project_list_value_field_format', 99 | "/lib/#{plugin_name}/hooks", 100 | 101 | # lib/controllers 102 | '/lib/controllers/smile_controllers_projects', 103 | 104 | # lib/helpers 105 | '/lib/helpers/smile_helpers_projects', 106 | 107 | # lib/models 108 | '/lib/models/smile_models_project', 109 | '/lib/models/smile_models_custom_field', 110 | ] 111 | 112 | if Rails.env == "development" 113 | ########################### 114 | # 5.2/ Dynamic requirements 115 | Rails.logger.debug "o=>require_dependency" 116 | required.each{ |d| 117 | # Reloaded each time modified 118 | Rails.logger.debug "o=> #{plugin_rel_root + d}" 119 | require_dependency plugin_root + d 120 | } 121 | required = nil 122 | 123 | # Folders whose contents should be reloaded, NOT including sub-folders 124 | 125 | # ActiveSupport::Dependencies.autoload_once_paths.reject!{|x| x =~ /^#{Regexp.escape(plugin_root)}/} 126 | Rails.application.config.to_prepare do 127 | autoload_plugin_paths = ['/lib/controllers', '/lib/helpers', '/lib/models'] 128 | 129 | Rails.logger.debug 'o=>' 130 | Rails.logger.debug "o=>autoload_paths / watchable_dirs +=" 131 | autoload_plugin_paths.each{|p| 132 | new_path = plugin_root + p 133 | Rails.logger.debug "o=> #{plugin_rel_root + p}" 134 | ActiveSupport::Dependencies.autoload_paths << new_path 135 | rails_dispatcher.watchable_dirs[new_path] = [:rb] 136 | } 137 | end 138 | else 139 | ########################## 140 | # 5.3/ Static requirements 141 | Rails.logger.debug "o=>require" 142 | required.each{ |p| 143 | # Never reloaded 144 | Rails.logger.debug "o=> #{plugin_rel_root + p}" 145 | require plugin_root + p 146 | } 147 | end 148 | # END -- Manage dependencies 149 | 150 | 151 | ############## 152 | # 6/ Overrides 153 | 154 | #*************************** 155 | # **** 6.1/ Controllers **** 156 | Rails.logger.info "o=>----- CONTROLLERS" 157 | prepend_in(ProjectsController, Controllers::SmileControllersProjects::ProjectsOverride::ProjectEnumerations) 158 | 159 | 160 | #*********************** 161 | # **** 6.2/ Helpers **** 162 | Rails.logger.info "o=>----- HELPERS" 163 | prepend_in(ProjectsHelper, Helpers::SmileHelpersProjects::ProjectsOverride::ProjectEnumerations) 164 | 165 | #********************** 166 | # **** 6.3/ Models **** 167 | Rails.logger.info "o=>----- MODELS" 168 | prepend_in(Project, Models::SmileModelsProject::ProjectOverride::ProjectEnumerations) 169 | prepend_in(CustomField, Models::SmileModelsCustomField::CustomFieldOverride::ProjectEnumerations) 170 | 171 | 172 | # keep traces if classes / modules are reloaded 173 | SmileTools.enable_traces(false, plugin_name) 174 | 175 | Rails.logger.info 'o=>/--' 176 | end 177 | -------------------------------------------------------------------------------- /lib/controllers/smile_controllers_projects.rb: -------------------------------------------------------------------------------- 1 | require_dependency "projects_controller" 2 | 3 | module Controllers 4 | module SmileControllersProjects 5 | module ProjectsOverride 6 | module ProjectEnumerations 7 | def self.prepended(base) 8 | project_enumerations_instance_methods = [ 9 | :settings, # 1/ EXTENDED, RM V4.0.0 OK 10 | ] 11 | 12 | smile_instance_methods = base.instance_methods.select{|m| 13 | base.instance_method(m).owner == self 14 | } 15 | 16 | trace_first_prefix = "#{base.name} instance_methods " 17 | trace_prefix = "#{' ' * (base.name.length + 15)} ---> " 18 | last_postfix = '< (SM::CO::ProjectsOverride::ProjectEnumerations)' 19 | 20 | SmileTools::trace_by_line( 21 | smile_instance_methods, 22 | trace_first_prefix, 23 | trace_prefix, 24 | last_postfix, 25 | :redmine_smile_project_enumerations_custom_field_format 26 | ) 27 | end 28 | 29 | # REWRITTEN split by tracker, RM 4.0.0 OK 30 | # EXTENDED to manage Project Enumerations 31 | # Smile specific #763230 Project Custom Fields configuration : split by tracker 32 | def settings 33 | ################ 34 | # Smile specific : includes trackers 35 | @issue_custom_fields = IssueCustomField.includes(:trackers).sorted.to_a 36 | @issue_category ||= IssueCategory.new 37 | @member ||= @project.members.new 38 | @trackers = Tracker.sorted.to_a 39 | 40 | @version_status = params[:version_status] || 'open' 41 | @version_name = params[:version_name] 42 | @versions = @project.shared_versions.status(@version_status).like(@version_name).sorted 43 | @wiki ||= @project.wiki || Wiki.new(:project => @project) 44 | 45 | 46 | ################ 47 | # Smile specific : NEXT extended 48 | 49 | ################# 50 | # 1/ Enumerations 51 | @enumeration_custom_fields_enabled_on_project = CustomField.enabled_on_project(@project).where(:field_format => 'project_enumeration') 52 | 53 | @enumeration_custom_fields_not_enabled_on_project = CustomField.not_enabled_on_project(@project).where(:field_format => 'project_enumeration') 54 | 55 | @project_enumerations = @project.shared_enumerations 56 | 57 | @enumeration_custom_fields_enabled_on_project_options = @enumeration_custom_fields_enabled_on_project.collect do |c| 58 | type_name = c.type_name 59 | name = c.name 60 | if type_name != :label_issue_plural 61 | name = "#{l(type_name)} / #{name}" 62 | end 63 | [name, c.id] 64 | end 65 | 66 | @enumeration_custom_field_id = params[:enumeration_custom_field_id] 67 | unless @enumeration_custom_field_id.blank? 68 | @project_enumerations = @project_enumerations.where("custom_field_id = ?", @enumeration_custom_field_id.to_i) 69 | end 70 | 71 | @enumeration_value = params[:enumeration_value] 72 | unless @enumeration_value.blank? 73 | @project_enumerations = @project_enumerations.where("value LIKE ?", "%#{@enumeration_value}%") 74 | end 75 | 76 | @enumeration_status = params[:enumeration_status] 77 | unless @enumeration_status.blank? 78 | @project_enumerations = @project_enumerations.where("status = ?", @enumeration_status) 79 | end 80 | 81 | @enumeration_sharing = params[:enumeration_sharing] 82 | unless @enumeration_sharing.blank? 83 | @project_enumerations = @project_enumerations.where("sharing = ?", @enumeration_sharing) 84 | end 85 | 86 | 87 | ################ 88 | # 2/ List values 89 | @list_value_custom_fields_enabled_on_project = CustomField.enabled_on_project(@project).where(:field_format => 'project_list_value') 90 | 91 | @list_value_custom_fields_not_enabled_on_project = CustomField.not_enabled_on_project(@project).where(:field_format => 'project_list_value') 92 | 93 | @project_list_values = @project.shared_list_values 94 | 95 | @list_value_custom_fields_enabled_on_project_options = @list_value_custom_fields_enabled_on_project.collect do |c| 96 | type_name = c.type_name 97 | name = c.name 98 | if type_name != :label_issue_plural 99 | name = "#{l(type_name)} / #{name}" 100 | end 101 | [name, c.id] 102 | end 103 | 104 | @list_value_custom_field_id = params[:list_value_custom_field_id] 105 | unless @list_value_custom_field_id.blank? 106 | @project_list_values = @project_list_values.where("custom_field_id = ?", @list_value_custom_field_id.to_i) 107 | end 108 | 109 | @list_value_value = params[:list_value_value] 110 | unless @list_value_value.blank? 111 | @project_list_values = @project_list_values.where("value LIKE ?", "%#{@list_value_value}%") 112 | end 113 | 114 | @list_value_status = params[:list_value_status] 115 | unless @list_value_status.blank? 116 | @project_list_values = @project_list_values.where("status = ?", @list_value_status) 117 | end 118 | 119 | @list_value_sharing = params[:list_value_sharing] 120 | unless @list_value_sharing.blank? 121 | @project_list_values = @project_list_values.where("sharing = ?", @list_value_sharing) 122 | end 123 | end 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/helpers/smile_helpers_projects.rb: -------------------------------------------------------------------------------- 1 | require_dependency "projects_helper" 2 | 3 | ################ 4 | # Smile connent : why re-select all tabs ? 5 | 6 | module Helpers 7 | module SmileHelpersProjects 8 | module ProjectsOverride 9 | module ProjectEnumerations 10 | def self.prepended(base) 11 | project_enumerations_instance_methods = [ 12 | :project_settings_tabs_with_project_enumerations, # 1/ EXTENDED RM 4.0.0 OK 13 | ] 14 | 15 | 16 | # Smile specific : EXTENDED 17 | # Smile comment : module_eval mandatory with helpers, that are included in classes without the module prepended sub-modules 18 | # Smile comment : but no more access to rewritten methods => use of alias method to access to ancestor version 19 | base.module_eval do 20 | # Extended 21 | def project_settings_tabs_with_project_enumerations 22 | tabs = project_settings_tabs_without_project_enumerations 23 | 24 | # 1/ Issue settings 25 | modules_tab = {:name => 'modules', :action => :select_project_modules, 26 | :partial => 'projects/settings/modules', 27 | :label => :label_module_plural} 28 | 29 | # Previous Redmine version (< 4) 30 | index = tabs.index(modules_tab) 31 | if index 32 | # Modules selection merged into info tab 33 | tabs.delete_at(index) 34 | 35 | # Insert new issues tab (moved from info tab) 36 | tabs.insert(index, 37 | {:name => 'issues', :action => :edit_project, :module => :issue_tracking, :partial => 'projects/settings/issues', :label => :label_issue_tracking} 38 | ) 39 | end 40 | 41 | # Smile comment : re-select new added tabs 42 | tabs.select! {|tab| 43 | ################ 44 | # Smile specific : manage controller in allowed_to? 45 | allowed_params = tab[:action] 46 | allowed_params = {:action => allowed_params, :controller => tab[:controller]} if tab[:controller] 47 | User.current.allowed_to?(allowed_params, @project) 48 | } 49 | tabs.select! {|tab| tab[:module].nil? || @project.module_enabled?(tab[:module])} 50 | 51 | # 2/ Project enumerations 52 | return tabs unless User.current.allowed_to?(:manage_project_enumerations, @project) 53 | 54 | options_tab = {:name => 'categories', :action => :manage_categories, 55 | :partial => 'projects/settings/issue_categories', 56 | :label => :label_issue_category_plural} 57 | 58 | index = tabs.index(options_tab) 59 | unless index # Needed for Redmine v3.4.x 60 | options_tab[:url] = {:tab => 'categories', 61 | :version_status => params[:version_status], 62 | :version_name => params[:version_name]} 63 | index = tabs.index(options_tab) 64 | end 65 | 66 | return tabs unless index 67 | 68 | any_enumeration_custom_field = ( 69 | CustomField.where(:field_format => 'project_enumeration').count > 0 70 | ) 71 | 72 | any_list_value_custom_field = ( 73 | CustomField.where(:field_format => 'project_list_value').count > 0 74 | ) 75 | 76 | if any_list_value_custom_field 77 | tabs.insert(index, 78 | {:name => 'project_list_values', :action => :manage_project_enumerations, 79 | :partial => 'projects/settings/project_list_values', 80 | :label => :label_project_list_value_plural}) 81 | end 82 | 83 | if any_enumeration_custom_field 84 | tabs.insert(index, 85 | {:name => 'project_enumerations', :action => :manage_project_enumerations, 86 | :partial => 'projects/settings/project_enumerations', 87 | :label => :label_project_enumeration_plural}) 88 | end 89 | 90 | # Smile comment : re-select new added tabs 91 | tabs.select! {|tab| 92 | ################ 93 | # Smile specific : manage controller in allowed_to? 94 | allowed_params = tab[:action] 95 | allowed_params = {:action => allowed_params, :controller => tab[:controller]} if tab[:controller] 96 | User.current.allowed_to?(allowed_params, @project) 97 | } 98 | tabs.select! {|tab| tab[:module].nil? || @project.module_enabled?(tab[:module])} 99 | 100 | tabs 101 | end 102 | end 103 | 104 | base.instance_eval do 105 | alias_method :project_settings_tabs_without_project_enumerations, :project_settings_tabs 106 | alias_method :project_settings_tabs, :project_settings_tabs_with_project_enumerations 107 | end 108 | 109 | 110 | trace_prefix = "#{' ' * (base.name.length + 19)} ---> " 111 | last_postfix = '< (SM::HO::ProjectsOverride::ProjectEnumerations)' 112 | 113 | smile_instance_methods = base.instance_methods.select{|m| 114 | project_enumerations_instance_methods.include?(m) && 115 | base.instance_method(m).source_location.first =~ SmileTools.regex_path_in_plugin( 116 | 'lib/helpers/smile_helpers_projects', 117 | :redmine_smile_project_enumerations_custom_field_format 118 | ) 119 | } 120 | 121 | missing_instance_methods = project_enumerations_instance_methods.select{|m| 122 | !smile_instance_methods.include?(m) 123 | } 124 | 125 | if missing_instance_methods.any? 126 | trace_first_prefix = "#{base.name} MISS instance_methods " 127 | else 128 | trace_first_prefix = "#{base.name} instance_methods " 129 | end 130 | 131 | SmileTools::trace_by_line( 132 | ( 133 | missing_instance_methods.any? ? 134 | missing_instance_methods : 135 | smile_instance_methods 136 | ), 137 | trace_first_prefix, 138 | trace_prefix, 139 | last_postfix, 140 | :redmine_smile_project_enumerations_custom_field_format 141 | ) 142 | 143 | if missing_instance_methods.any? 144 | raise trace_first_prefix + missing_instance_methods.join(', ') + ' ' + last_postfix 145 | end 146 | end 147 | end 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/models/smile_models_custom_field.rb: -------------------------------------------------------------------------------- 1 | # Smile - add methods to the CustomField model 2 | # 3 | 4 | module Models 5 | module SmileModelsCustomField 6 | module CustomFieldOverride 7 | module ProjectEnumerations 8 | # extend ActiveSupport::Concern 9 | 10 | def self.prepended(base) 11 | base.class_eval do 12 | scope :joins_projects, lambda { 13 | joins("LEFT JOIN #{table_name_prefix}custom_fields_projects#{table_name_suffix} AS cfp ON cfp.custom_field_id = #{CustomField.table_name}.id") 14 | } 15 | 16 | scope :enabled_on_project, lambda { |project| 17 | joins_projects. 18 | where( 19 | '(' + 20 | ( 21 | project && project.id ? 22 | "cfp.project_id = #{project.id} OR " : 23 | '' 24 | ) + 25 | "type <> 'IssueCustomField' OR " + 26 | "is_for_all = #{project.class.connection.quoted_true}" + 27 | ')' 28 | ). 29 | distinct 30 | } 31 | 32 | scope :not_enabled_on_project, lambda { |project| 33 | enabled_project_ids = Project.joins_custom_fields.where(:id => project.id).pluck('cfp.custom_field_id') 34 | where.not('id' => enabled_project_ids). 35 | where(:type => 'IssueCustomField'). 36 | where.not(:is_for_all => true) 37 | } 38 | end 39 | end 40 | end # module ProjectEnumerations 41 | end # module CustomFieldOverride 42 | end # module SmileModelsCustomField 43 | end # module Models 44 | -------------------------------------------------------------------------------- /lib/models/smile_models_project.rb: -------------------------------------------------------------------------------- 1 | # Smile - add methods to the Project model 2 | # 3 | # 1/ module ProjectEnumerations 4 | # - #TODO RM issue id for Change 5 | 6 | #require 'active_support/concern' #Rails 3 7 | 8 | module Models 9 | module SmileModelsProject 10 | module ProjectOverride 11 | #***************** 12 | # 1/ ProjectEnumerations 13 | module ProjectEnumerations 14 | def self.prepended(base) 15 | project_enumeration_methods = [ 16 | :shared_enumerations, # 1/ new method 17 | :shared_list_values, # 2/ new method 18 | ] 19 | 20 | trace_prefix = "#{' ' * (base.name.length + 25)} ---> " 21 | last_postfix = '< (SM::MO::ProjectOverride::ProjectEnumerations)' 22 | 23 | smile_instance_methods = base.instance_methods.select{|m| 24 | base.instance_method(m).owner == self 25 | } 26 | 27 | missing_instance_methods = project_enumeration_methods.select{|m| 28 | !smile_instance_methods.include?(m) 29 | } 30 | 31 | if missing_instance_methods.any? 32 | trace_first_prefix = "#{base.name} MISS instance_methods " 33 | else 34 | trace_first_prefix = "#{base.name} instance_methods " 35 | end 36 | 37 | SmileTools::trace_by_line( 38 | ( 39 | missing_instance_methods.any? ? 40 | missing_instance_methods : 41 | smile_instance_methods 42 | ), 43 | trace_first_prefix, 44 | trace_prefix, 45 | last_postfix, 46 | :redmine_smile_project_enumerations_custom_field_format 47 | ) 48 | 49 | if missing_instance_methods.any? 50 | raise trace_first_prefix + missing_instance_methods.join(', ') + ' ' + last_postfix 51 | end 52 | 53 | base.class_eval do 54 | scope :joins_custom_fields, lambda { 55 | joins("LEFT JOIN #{table_name_prefix}custom_fields_projects#{table_name_suffix} AS cfp ON cfp.project_id = #{Project.table_name}.id") 56 | } 57 | end 58 | 59 | project_enumeration_scopes = [ 60 | :joins_custom_fields, 61 | ] 62 | 63 | missing_scopes = project_enumeration_scopes.select{|s| 64 | ! base.respond_to?(s) 65 | } 66 | 67 | if missing_scopes.any? 68 | trace_first_prefix = "#{base.name} MISS scopes " 69 | else 70 | trace_first_prefix = "#{base.name} scopes " 71 | end 72 | 73 | SmileTools::trace_by_line( 74 | ( missing_scopes.any? ? missing_scopes : project_enumeration_scopes ), 75 | trace_first_prefix, 76 | trace_prefix, 77 | last_postfix, 78 | :redmine_smile_project_enumerations_custom_field_format 79 | ) 80 | 81 | if missing_scopes.any? 82 | raise trace_first_prefix + missing_scopes.join(', ') + ' ' + last_postfix 83 | end 84 | end # def self.prepended(base) 85 | 86 | 87 | # 1/ new method, RM 4.0 OK 88 | # Returns a scope of the Enumerations used by the project 89 | def shared_enumerations 90 | enumeration_custom_fields_enabled_on_project = CustomField.enabled_on_project(self).where(:field_format => 'project_enumeration') 91 | if new_record? 92 | ::ProjectEnumeration. 93 | joins(:project). 94 | preload(:project, :custom_field). 95 | for_enumerations. 96 | where("#{Project.table_name}.status <> ? AND #{::ProjectEnumeration.table_name}.sharing = 'system'", ::Project::STATUS_ARCHIVED). 97 | where(:custom_field_id => enumeration_custom_fields_enabled_on_project). 98 | order_by_custom_field_then_position 99 | else 100 | @shared_enumerations ||= begin 101 | r = root? ? self : root 102 | ::ProjectEnumeration. 103 | joins(:project). 104 | preload(:project, :custom_field). 105 | for_enumerations. 106 | where( 107 | "#{Project.table_name}.id = #{id}" \ 108 | " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" \ 109 | " #{ProjectEnumeration.table_name}.sharing = 'system'" \ 110 | " OR (#{Project.table_name}.lft >= #{r.lft}" \ 111 | " AND #{Project.table_name}.rgt <= #{r.rgt}" \ 112 | " AND #{ProjectEnumeration.table_name}.sharing = 'tree')" \ 113 | " OR (#{Project.table_name}.lft < #{lft}" \ 114 | " AND #{Project.table_name}.rgt > #{rgt}" \ 115 | " AND #{ProjectEnumeration.table_name}.sharing IN ('hierarchy', 'descendants'))" \ 116 | " OR (#{Project.table_name}.lft > #{lft}" \ 117 | " AND #{Project.table_name}.rgt < #{rgt}" \ 118 | " AND #{ProjectEnumeration.table_name}.sharing = 'hierarchy')" \ 119 | "))" 120 | ). 121 | where(:custom_field_id => enumeration_custom_fields_enabled_on_project). 122 | order_by_custom_field_then_position 123 | end 124 | end 125 | end 126 | 127 | # 2/ new method, RM 4.0.3 OK 128 | # Returns a scope of the List Values used by the project 129 | def shared_list_values 130 | list_value_custom_fields_enabled_on_project = CustomField.enabled_on_project(self).where(:field_format => 'project_list_value') 131 | if new_record? 132 | ::ProjectEnumeration. 133 | joins(:project). 134 | preload(:project, :custom_field). 135 | for_list_values. 136 | where("#{Project.table_name}.status <> ? AND #{::ProjectEnumeration.table_name}.sharing = 'system'", ::Project::STATUS_ARCHIVED). 137 | where(:custom_field_id => list_value_custom_fields_enabled_on_project). 138 | order_by_custom_field_then_position 139 | else 140 | @shared_list_values ||= begin 141 | r = root? ? self : root 142 | ::ProjectEnumeration. 143 | joins(:project). 144 | preload(:project, :custom_field). 145 | for_list_values. 146 | where( 147 | "#{Project.table_name}.id = #{id}" \ 148 | " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" \ 149 | " #{ProjectEnumeration.table_name}.sharing = 'system'" \ 150 | " OR (#{Project.table_name}.lft >= #{r.lft}" \ 151 | " AND #{Project.table_name}.rgt <= #{r.rgt}" \ 152 | " AND #{ProjectEnumeration.table_name}.sharing = 'tree')" \ 153 | " OR (#{Project.table_name}.lft < #{lft}" \ 154 | " AND #{Project.table_name}.rgt > #{rgt}" \ 155 | " AND #{ProjectEnumeration.table_name}.sharing IN ('hierarchy', 'descendants'))" \ 156 | " OR (#{Project.table_name}.lft > #{lft}" \ 157 | " AND #{Project.table_name}.rgt < #{rgt}" \ 158 | " AND #{ProjectEnumeration.table_name}.sharing = 'hierarchy')" \ 159 | "))" 160 | ). 161 | where(:custom_field_id => list_value_custom_fields_enabled_on_project). 162 | order_by_custom_field_then_position 163 | end 164 | end 165 | end 166 | end # module ProjectEnumerations 167 | end # module ProjectOverride 168 | end # module SmileModelsProject 169 | end # module Models 170 | -------------------------------------------------------------------------------- /lib/project_enumeration_field_format.rb: -------------------------------------------------------------------------------- 1 | # Smile - redmine_smile_project_enumerations_custom_field_format enhancement 2 | # 3 | # Compatible with Redmine 4.0 4 | # 5 | # module ProjectEnumerationFieldFormat::FieldFormat::ProjectEnumerationFormat 6 | # 7 | # * InstanceMethods 8 | # * possible_values_options 9 | # * possible_values_enumerations 10 | # * protected 11 | # * query_filter_values 12 | # * possible_values_enumerations 13 | # * filtered_enumerations_options 14 | 15 | 16 | module ProjectEnumerationFieldFormat 17 | module FieldFormat 18 | class ProjectEnumerationFormat < Redmine::FieldFormat::RecordList 19 | add 'project_enumeration' 20 | self.form_partial = 'custom_fields/formats/project_enumeration' 21 | field_attributes :version_status 22 | 23 | # + User 24 | self.customized_class_names = %w(Issue TimeEntry Version Document Project User) 25 | 26 | def possible_values_options(custom_field, object=nil) 27 | possible_values_enumerations(custom_field, object).collect{|v| [v.to_s, v.id.to_s] } 28 | end 29 | 30 | def before_custom_field_save(custom_field) 31 | super 32 | if custom_field.version_status.is_a?(Array) 33 | custom_field.version_status.map!(&:to_s).reject!(&:blank?) 34 | end 35 | end 36 | 37 | protected 38 | 39 | def query_filter_values(custom_field, query) 40 | project_enumerations = possible_values_enumerations(custom_field, query.project, true) 41 | ProjectEnumeration.sort_by_status(project_enumerations).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] } 42 | end 43 | 44 | def possible_values_enumerations(custom_field, object=nil, all_statuses=false) 45 | if object.is_a?(Array) 46 | projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq 47 | projects.map {|project| possible_values_enumerations(custom_field, project)}.reduce(:&) || [] 48 | elsif ( object.respond_to?(:project) && object.project ) 49 | scope = object.project.shared_enumerations.joins(:custom_field).where('custom_fields.id = ?', custom_field.id) 50 | filtered_enumerations_options(custom_field, scope, all_statuses) 51 | elsif ( object && object.respond_to?(:project) && custom_field.format.class.customized_class_names.include?(object.class.name) ) 52 | scope = ::ProjectEnumeration. 53 | visible. 54 | joins(:custom_field). 55 | where('custom_fields.id = ?', custom_field.id) 56 | filtered_enumerations_options(custom_field, scope, all_statuses) 57 | elsif object.nil? 58 | scope = ::ProjectEnumeration.visible.where(:sharing => 'system') 59 | filtered_enumerations_options(custom_field, scope, all_statuses) 60 | else 61 | [] 62 | end 63 | end 64 | 65 | def filtered_enumerations_options(custom_field, scope, all_statuses=false) 66 | if !all_statuses && custom_field.version_status.is_a?(Array) 67 | statuses = custom_field.version_status.map(&:to_s).reject(&:blank?) 68 | if statuses.any? 69 | scope = scope.where(:status => statuses.map(&:to_s)) 70 | end 71 | end 72 | scope 73 | end 74 | end 75 | end # FieldFormatOverride 76 | end # module ProjectEnumerationFieldFormatOverride 77 | -------------------------------------------------------------------------------- /lib/project_list_value_field_format.rb: -------------------------------------------------------------------------------- 1 | # Smile - redmine_smile_project_list_values_custom_field_format enhancement 2 | # 3 | # Compatible with Redmine 4.0 4 | # 5 | # module ProjectListValueFieldFormat::FieldFormat::ProjectEnumerationFormat 6 | # 7 | # * InstanceMethods 8 | # * possible_values_options 9 | # * possible_values_list_values 10 | # * protected 11 | # * query_filter_values 12 | # * possible_values_list_values 13 | # * filtered_list_values_options 14 | 15 | 16 | module ProjectListValueFieldFormat 17 | module FieldFormat 18 | class ProjectListValueFormat < Redmine::FieldFormat::RecordList 19 | add 'project_list_value' 20 | self.form_partial = 'custom_fields/formats/project_list_value' 21 | field_attributes :version_status 22 | 23 | # + User 24 | self.customized_class_names = %w(Issue TimeEntry Version Document Project User) 25 | 26 | def possible_values_options(custom_field, object=nil) 27 | possible_values_list_values(custom_field, object).collect{|v| [v.to_s, v.to_s] } 28 | end 29 | 30 | def before_custom_field_save(custom_field) 31 | super 32 | if custom_field.version_status.is_a?(Array) 33 | custom_field.version_status.map!(&:to_s).reject!(&:blank?) 34 | end 35 | end 36 | 37 | def cast_single_value(custom_field, value, customized=nil) 38 | value.to_s 39 | end 40 | 41 | # Model shared with ProjectEnumeration 42 | def target_class 43 | @target_class ||= ProjectEnumeration 44 | end 45 | 46 | protected 47 | 48 | def query_filter_values(custom_field, query) 49 | project_list_values = possible_values_list_values(custom_field, query.project, true) 50 | ProjectEnumeration.sort_by_status(project_list_values).collect{|s| ["#{s.project.name} - #{s.name}", s.to_s, l("version_status_#{s.status}")] } 51 | end 52 | 53 | def possible_values_list_values(custom_field, object=nil, all_statuses=false) 54 | if object.is_a?(Array) 55 | projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq 56 | projects.map {|project| possible_values_list_values(custom_field, project)}.reduce(:&) || [] 57 | elsif ( object.respond_to?(:project) && object.project ) 58 | scope = object.project.shared_list_values.joins(:custom_field).where('custom_fields.id = ?', custom_field.id) 59 | filtered_list_values_options(custom_field, scope, all_statuses) 60 | elsif ( object && !object.respond_to?(:project) && custom_field.format.class.customized_class_names.include?(object.class.name) ) 61 | scope = ::ProjectEnumeration. 62 | visible. 63 | joins(:custom_field). 64 | where('custom_fields.id = ?', custom_field.id) 65 | filtered_enumerations_options(custom_field, scope, all_statuses) 66 | elsif object.nil? 67 | scope = ::ProjectEnumeration.visible.where(:sharing => 'system') 68 | filtered_list_values_options(custom_field, scope, all_statuses) 69 | else 70 | [] 71 | end 72 | end 73 | 74 | def filtered_list_values_options(custom_field, scope, all_statuses=false) 75 | if !all_statuses && custom_field.version_status.is_a?(Array) 76 | statuses = custom_field.version_status.map(&:to_s).reject(&:blank?) 77 | if statuses.any? 78 | scope = scope.where(:status => statuses.map(&:to_s)) 79 | end 80 | end 81 | scope 82 | end 83 | end 84 | end # FieldFormatOverride 85 | end # module ProjectListValueFieldFormatOverride 86 | -------------------------------------------------------------------------------- /lib/redmine_smile_project_enumerations_custom_field_format/hooks.rb: -------------------------------------------------------------------------------- 1 | # This module name must be unique, if not the last Hooks class will be taken in account 2 | module RedmineSmileProjectEnumerationsCustomFieldFormat 3 | class Hooks < Redmine::Hook::ViewListener 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/smile_tools.rb: -------------------------------------------------------------------------------- 1 | # Smile Tools : usefull methods 2 | # 3 | 4 | class SmileTools 5 | @@override_traces = {} 6 | @@override_count = {} 7 | @@override_last_date = {} 8 | @@traces_enabled = {} 9 | @@default_smile_plugin_name = :redmine_smile_enhancements 10 | 11 | # Common to all the Plugins 12 | # 150 : chars available after override count 13 | # 40 : chars for first prefix 14 | @@line_length = 110 15 | cattr_accessor :line_length 16 | 17 | 18 | # Common to all the Plugins 19 | def self.delimiter=(p_delimiter) 20 | @@delimiter = p_delimiter 21 | @@delimiter_length = @@delimiter.size 22 | end 23 | 24 | self::delimiter = '; ' 25 | cattr_reader :delimiter 26 | 27 | 28 | # for the logs : trace the chunks with limiting the length of the line 29 | def self.trace_by_line( 30 | p_chunks, 31 | p_first_prefix, 32 | p_prefix, 33 | p_last_postfix, 34 | p_plugin=@@default_smile_plugin_name 35 | ) 36 | if !p_chunks.is_a?(Array) 37 | # 1) call to trace meth 38 | trace_override("self.trace_by_line p_chunks=#{p_chunks.inspect}) is NOT an Array", false, p_plugin) 39 | 40 | return 41 | end 42 | 43 | current_line_id = 0 44 | last_chunk_index = p_chunks.size - 1 45 | first_prefix_length = p_first_prefix.length 46 | prefix_length = p_prefix.length 47 | first_chunk = true 48 | 49 | lines = [p_first_prefix.dup] 50 | 51 | p_chunks.each_with_index{ |c, i| 52 | # 1/ prefix + chunk 53 | 54 | length_after_add = -first_prefix_length 55 | 56 | length_after_add += lines[current_line_id].length 57 | 58 | if i!=0 59 | length_after_add += @@delimiter_length 60 | end 61 | 62 | length_after_add += c.length 63 | 64 | 65 | # current_line added to lines when : 66 | # - current_line exceeds wanted length 67 | # - this is the last chunk, we must get the last line when below max length 68 | if (length_after_add > @@line_length) 69 | # start new line 70 | lines << '' 71 | 72 | current_line_id += 1 73 | lines[current_line_id] << p_prefix 74 | 75 | first_chunk = true 76 | end 77 | 78 | # add chunk 79 | lines[current_line_id] << ( first_chunk ? '' : @@delimiter) + c.to_s 80 | 81 | first_chunk = false 82 | } 83 | 84 | # 2/ Last postfix 85 | length_after_add = lines[current_line_id].length + @@delimiter_length + p_last_postfix.length - prefix_length 86 | if (length_after_add > @@line_length) 87 | current_line_id += 1 88 | lines[current_line_id] = p_prefix.dup 89 | else 90 | lines[current_line_id] << ' ' 91 | end 92 | 93 | lines[current_line_id] << p_last_postfix 94 | 95 | lines.each{ |t| 96 | # 2) call to trace meth 97 | trace_override(t, true, p_plugin) 98 | } 99 | end 100 | 101 | 102 | def self.trace_override(line, p_count=true, p_plugin=@@default_smile_plugin_name) 103 | @@override_last_date[p_plugin] = Time.now 104 | 105 | # Count on 6 chars, left justified with spaces 106 | # - exceptions : 107 | # alias_meth_chain has a previous instance_methods or methods tag line 108 | # ---> => continuation of a list 109 | unless line.include?('alias_meth_chain') || line.include?('---> <') 110 | override_count_incr( 111 | (line.count(';') + 1), 112 | p_plugin 113 | ) if p_count 114 | 115 | label_override_count = override_count(p_plugin).to_s.ljust(6, ' ') 116 | else 117 | label_override_count = ' ' 118 | end 119 | 120 | #----------------------------- 121 | # 1) Display log traces anyway 122 | Rails.logger.info 'o=>' + label_override_count + line 123 | 124 | plugin_traces_enabled = traces_enabled?(p_plugin) 125 | return unless plugin_traces_enabled 126 | 127 | #----------------------------- 128 | # 2) Override trace in plugin settings 129 | # Display override traces once (NOT if plugin is reloaded in dev.) 130 | # new line 131 | override_trace_add( 132 | "
".html_safe, p_plugin 133 | ) if override_traces(p_plugin).present? 134 | 135 | # override count + line 136 | override_trace_add( 137 | ( label_override_count + ERB::Util.h(line) ).gsub(' ', ' ').gsub(', ', ', '), 138 | p_plugin 139 | ) 140 | end 141 | 142 | def self.override_traces(p_plugin=@@default_smile_plugin_name) 143 | @@override_traces[p_plugin] = '' unless @@override_traces[p_plugin] 144 | 145 | @@override_traces[p_plugin] 146 | end 147 | 148 | def self.override_trace_add(trace, p_plugin) 149 | @@override_traces[p_plugin] = '' unless @@override_traces[p_plugin] 150 | 151 | @@override_traces[p_plugin] += trace 152 | end 153 | 154 | def self.reset_override_count(p_plugin) 155 | @@override_count[p_plugin] = 0 156 | end 157 | 158 | def self.override_count(p_plugin=@@default_smile_plugin_name) 159 | reset_override_count(p_plugin) unless @@override_count[p_plugin] 160 | 161 | @@override_count[p_plugin] 162 | end 163 | 164 | def self.override_count_incr(incr, p_plugin) 165 | reset_override_count(p_plugin) unless @@override_count[p_plugin] 166 | 167 | @@override_count[p_plugin] += incr 168 | end 169 | 170 | def self.enable_traces(enable, p_plugin) 171 | @@traces_enabled[p_plugin] = enable 172 | end 173 | 174 | def self.traces_enabled?(p_plugin) 175 | @@traces_enabled[p_plugin] = true if @@traces_enabled[p_plugin] == nil 176 | @@traces_enabled[p_plugin] 177 | end 178 | 179 | def self.override_last_date(p_plugin=@@default_smile_plugin_name) 180 | @@override_last_date[p_plugin] 181 | end 182 | 183 | def self.remove_sql_in_values(a_string) 184 | a_string.gsub(/ IN \([^\)|^\)]*\)/, ' [IN VALUES REMOVED])') 185 | end 186 | 187 | def self.debug_scope(a_scope, tag='sc', entete='', sql=false, remove_in_values=false) 188 | Rails.logger.debug " =>#{tag} --\\ SCOPE #{a_scope.klass.name}" + (entete.present? ? ' : ' + entete : '') 189 | Rails.logger.debug " =>#{tag} SELECT #{a_scope.select_values.inspect}" if a_scope.select_values.any? 190 | where_values = a_scope.where_values_hash 191 | if where_values.empty? 192 | where_values = a_scope.where_clause.send(:predicates) 193 | end 194 | if where_values.any? 195 | if a_scope.where_values_hash.is_a?(Array) 196 | first_where = true 197 | a_scope.where_values_hash.each_with_index{|w, i| 198 | values_as_string = w.to_s 199 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 200 | Rails.logger.debug " =>#{tag} #{first_where ? 'WHERE' : ' '} #{i} #{values_as_string}" 201 | first_where = false 202 | } 203 | else 204 | values_as_string = where_values.inspect 205 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 206 | Rails.logger.debug " =>#{tag} WHERE #{values_as_string}" 207 | end 208 | end 209 | 210 | if a_scope.includes_values.any? 211 | values_as_string = a_scope.includes_values.inspect 212 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 213 | Rails.logger.debug " =>#{tag} INCLUDES #{values_as_string}" 214 | end 215 | 216 | if a_scope.preload_values.any? 217 | values_as_string = a_scope.preload_values.inspect 218 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 219 | Rails.logger.debug " =>#{tag} PRELOAD #{values_as_string}" 220 | end 221 | 222 | if a_scope.joins_values.any? 223 | values_as_string = a_scope.joins_values.inspect 224 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 225 | Rails.logger.debug " =>#{tag} JOINS #{values_as_string}" 226 | end 227 | 228 | if a_scope.group_values.any? 229 | values_as_string = a_scope.group_values.inspect 230 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 231 | Rails.logger.debug " =>#{tag} GROUPS #{values_as_string}" 232 | end 233 | 234 | if a_scope.order_values.any? 235 | values_as_string = a_scope.order_values.inspect 236 | values_as_string = remove_sql_in_values(values_as_string) if remove_in_values 237 | Rails.logger.debug " =>#{tag} ORDER #{values_as_string}" 238 | end 239 | 240 | Rails.logger.debug " =>#{tag}" if sql 241 | Rails.logger.debug " =>#{tag} #{a_scope.to_sql}" if sql 242 | 243 | Rails.logger.debug " =>#{tag} --/" 244 | end 245 | 246 | def self.regex_path_in_plugin(path, plugin=@@default_smile_plugin_name) 247 | /#{plugin}\/#{Regexp.quote(path)}/ 248 | end 249 | 250 | def self.default_smile_plugin_name 251 | @@default_smile_plugin_name 252 | end 253 | 254 | def self.debug_connexion 255 | Rails.logger.debug "==>conn" 256 | Rails.logger.debug " => db: #{ActiveRecord::Base.connection.current_database}" 257 | end 258 | end # class SmileTools 259 | -------------------------------------------------------------------------------- /scripts/test_it.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ../.. 4 | 5 | #RAILS_ENV=test rake test TEST=plugins/redmine_smile_project_enumerations_custom_field_format/test/functional/issues_controller_test.rb TESTOPTS="-n /test_post_create/" 6 | 7 | RAILS_ENV=test rails redmine:plugins:test NAME=redmine_smile_project_enumerations_custom_field_format 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/custom_fields.yml: -------------------------------------------------------------------------------- 1 | --- 2 | custom_fields_001: 3 | name: Project CF Enum 1 4 | regexp: "" 5 | is_for_all: false 6 | is_filter: true 7 | type: IssueCustomField 8 | id: 1 9 | is_required: true 10 | field_format: project_enumeration 11 | multiple: true 12 | searchable: true 13 | editable: true 14 | position: 1 15 | format_store: |- 16 | --- 17 | :version_status: 18 | - open 19 | :edit_tag_style: '' 20 | custom_fields_002: 21 | name: Project CF Enum 2 22 | regexp: "" 23 | is_for_all: false 24 | is_filter: true 25 | type: IssueCustomField 26 | id: 2 27 | is_required: false 28 | field_format: project_enumeration 29 | multiple: true 30 | searchable: true 31 | editable: true 32 | position: 2 33 | -------------------------------------------------------------------------------- /test/fixtures/custom_fields_projects.yml: -------------------------------------------------------------------------------- 1 | --- 2 | custom_fields_projects_001: 3 | custom_field_id: 1 4 | project_id: 1 5 | custom_fields_projects_002: 6 | custom_field_id: 2 7 | project_id: 1 8 | -------------------------------------------------------------------------------- /test/fixtures/custom_fields_trackers.yml: -------------------------------------------------------------------------------- 1 | --- 2 | custom_fields_trackers_001: 3 | custom_field_id: 1 4 | tracker_id: 1 5 | custom_fields_trackers_002: 6 | custom_field_id: 1 7 | tracker_id: 2 8 | custom_fields_trackers_003: 9 | custom_field_id: 2 10 | tracker_id: 1 11 | custom_fields_trackers_004: 12 | custom_field_id: 2 13 | tracker_id: 2 14 | -------------------------------------------------------------------------------- /test/fixtures/custom_values.yml: -------------------------------------------------------------------------------- 1 | --- 2 | custom_values_issue1_cf1_001: 3 | customized_type: Issue 4 | custom_field_id: 1 5 | customized_id: 1 6 | id: 1 7 | value: 1 8 | custom_values_issue1_cf1_002: 9 | customized_type: Issue 10 | custom_field_id: 1 11 | customized_id: 1 12 | id: 2 13 | value: 2 14 | custom_values_issue2_cf1_003: 15 | customized_type: Issue 16 | custom_field_id: 1 17 | customized_id: 2 18 | id: 3 19 | value: 1 20 | custom_values_issue2_cf2_004: 21 | customized_type: Issue 22 | custom_field_id: 2 23 | customized_id: 2 24 | id: 4 25 | value: 4 26 | custom_values_issue2_cf2_005: 27 | customized_type: Issue 28 | custom_field_id: 2 29 | customized_id: 2 30 | id: 5 31 | value: 5 32 | custom_values_issue2_cf2_006: 33 | customized_type: Issue 34 | custom_field_id: 2 35 | customized_id: 2 36 | id: 6 37 | value: 6 -------------------------------------------------------------------------------- /test/fixtures/project_enumerations.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project_enumerations_cf1_001: 3 | id: 1 4 | project_id: 1 5 | custom_field_id: 1 6 | value: "Cat. 1" 7 | status: open 8 | sharing: "descendants" 9 | project_enumerations_cf1_002: 10 | id: 2 11 | project_id: 1 12 | custom_field_id: 1 13 | value: "Cat. 2" 14 | status: locked 15 | sharing: "descendants" 16 | project_enumerations_cf1_003: 17 | id: 3 18 | project_id: 1 19 | custom_field_id: 1 20 | value: "Cat. 3" 21 | status: closed 22 | sharing: "descendants" 23 | project_enumerations_cf2_004: 24 | id: 4 25 | project_id: 1 26 | custom_field_id: 2 27 | value: "Enum. 1" 28 | status: open 29 | sharing: "descendants" 30 | project_enumerations_cf2_005: 31 | id: 5 32 | project_id: 1 33 | custom_field_id: 2 34 | value: "Enum. 2" 35 | status: open 36 | sharing: "descendants" 37 | project_enumerations_cf2_006: 38 | id: 6 39 | project_id: 1 40 | custom_field_id: 2 41 | value: "Enum. 3" 42 | status: open 43 | sharing: "descendants" 44 | project_enumerations_cf2_007: 45 | id: 7 46 | project_id: 1 47 | custom_field_id: 2 48 | value: "Enum. 4" 49 | status: open 50 | sharing: "descendants" 51 | -------------------------------------------------------------------------------- /test/functional/issues_controller_test.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2017 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | require File.expand_path('../../test_helper', __FILE__) 19 | 20 | class IssuesControllerTest < Redmine::ControllerTest 21 | fixtures :projects, 22 | :users, 23 | :roles, 24 | :members, 25 | :member_roles, 26 | :issues, 27 | :issue_statuses, 28 | :issue_relations, 29 | :trackers, 30 | :projects_trackers, 31 | :enabled_modules, 32 | :enumerations, 33 | :workflows, 34 | :custom_fields, 35 | :custom_values, 36 | :custom_fields_projects, 37 | :custom_fields_trackers, 38 | :time_entries, 39 | :journals, 40 | :journal_details, 41 | :queries 42 | 43 | include Redmine::I18n 44 | 45 | def setup 46 | User.current = nil 47 | 48 | @cf1_value1 = 'Cat. 1' 49 | @cf1_value2 = 'Cat. 2' 50 | @cf1_value3 = 'Cat. 3' 51 | @cf2_value4 = 'Enum. 1' 52 | @cf2_value5 = 'Enum. 2' 53 | @cf2_value6 = 'Enum. 3' 54 | @cf2_value7 = 'Enum. 4' 55 | end 56 | 57 | def test_index 58 | =begin 59 | with_settings :default_language => "en" do 60 | get :index 61 | assert_response :success 62 | 63 | # links to visible issues 64 | assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/ 65 | assert_select 'a[href="/issues/5"]', :text => /Subproject issue/ 66 | # private projects hidden 67 | assert_select 'a[href="/issues/6"]', 0 68 | assert_select 'a[href="/issues/4"]', 0 69 | # project column 70 | assert_select 'th', :text => /Project/ 71 | end 72 | =end 73 | end 74 | 75 | def test_index_with_project_custom_field_filter 76 | =begin 77 | field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string') 78 | CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo') 79 | CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo') 80 | filter_name = "project.cf_#{field.id}" 81 | @request.session[:user_id] = 1 82 | 83 | get :index, :params => { 84 | :set_filter => 1, 85 | :f => [filter_name], 86 | :op => { 87 | filter_name => '=' 88 | }, 89 | :v => { 90 | filter_name => ['Foo'] 91 | }, 92 | :c => ['project'] 93 | } 94 | assert_response :success 95 | 96 | assert_equal [3, 5], issues_in_list.map(&:project_id).uniq.sort 97 | =end 98 | end 99 | 100 | def test_index_with_query_grouped_and_sorted_by_category 101 | =begin 102 | get :index, :params => { 103 | :project_id => 1, 104 | :set_filter => 1, 105 | :group_by => "category", 106 | :sort => "category" 107 | } 108 | assert_response :success 109 | assert_select 'tr.group span.count' 110 | =end 111 | end 112 | 113 | def test_index_with_query_grouped_and_sorted_by_fixed_version 114 | =begin 115 | get :index, :params => { 116 | :project_id => 1, 117 | :set_filter => 1, 118 | :group_by => "fixed_version", 119 | :sort => "fixed_version" 120 | } 121 | assert_response :success 122 | assert_select 'tr.group span.count' 123 | =end 124 | end 125 | 126 | def test_index_with_query_grouped_by_list_custom_field 127 | =begin 128 | get :index, :params => { 129 | :project_id => 1, 130 | :query_id => 9 131 | } 132 | assert_response :success 133 | assert_select 'tr.group span.count' 134 | =end 135 | end 136 | 137 | 138 | def test_show_should_display_update_form 139 | =begin 140 | @request.session[:user_id] = 2 141 | get :show, :params => { 142 | :id => 1 143 | } 144 | assert_response :success 145 | 146 | assert_select 'form#issue-form' do 147 | assert_select 'input[name=?]', 'issue[is_private]' 148 | assert_select 'select[name=?]', 'issue[project_id]' 149 | assert_select 'select[name=?]', 'issue[tracker_id]' 150 | assert_select 'input[name=?]', 'issue[subject]' 151 | assert_select 'textarea[name=?]', 'issue[description]' 152 | assert_select 'select[name=?]', 'issue[status_id]' 153 | assert_select 'select[name=?]', 'issue[priority_id]' 154 | assert_select 'select[name=?]', 'issue[assigned_to_id]' 155 | assert_select 'select[name=?]', 'issue[category_id]' 156 | assert_select 'select[name=?]', 'issue[fixed_version_id]' 157 | assert_select 'input[name=?]', 'issue[parent_issue_id]' 158 | assert_select 'input[name=?]', 'issue[start_date]' 159 | assert_select 'input[name=?]', 'issue[due_date]' 160 | assert_select 'select[name=?]', 'issue[done_ratio]' 161 | assert_select 'input[name=?]', 'issue[custom_field_values][2]' 162 | assert_select 'input[name=?]', 'issue[watcher_user_ids][]', 0 163 | assert_select 'textarea[name=?]', 'issue[notes]' 164 | end 165 | =end 166 | end 167 | 168 | # DONE 169 | def test_update_form_should_not_display_inactive_enumerations 170 | pe = ProjectEnumeration.find(2) 171 | 172 | assert !pe.open? 173 | 174 | @request.session[:user_id] = 2 175 | get :show, :params => { 176 | :id => 1 177 | } 178 | assert_response :success 179 | 180 | assert_select 'select.project_enumeration_cf[name=?]', 'issue[custom_field_values][1][]' do 181 | assert_select 'option', 2 182 | assert_select 'option[value=1]', :text => 'Cat. 1' 183 | assert_select 'option[value=2]', :text => 'Cat. 2' 184 | end 185 | 186 | 187 | pe.status = 'open' 188 | pe.save 189 | pe.reload 190 | 191 | assert pe.open? 192 | 193 | get :show, :params => { 194 | :id => 1 195 | } 196 | assert_response :success 197 | 198 | assert_select 'select.project_enumeration_cf[name=?]', 'issue[custom_field_values][1][]' do 199 | assert_select 'option', 2 200 | assert_select 'option[value=1]', :text => 'Cat. 1' 201 | assert_select 'option[value=2]', :text => 'Cat. 2' 202 | end 203 | end 204 | 205 | # TODO Prio 2 206 | def test_show_should_display_category_field_if_categories_are_defined 207 | =begin 208 | Issue.update_all :category_id => nil 209 | 210 | get :show, :params => { 211 | :id => 1 212 | } 213 | assert_response :success 214 | assert_select '.attributes .category' 215 | =end 216 | end 217 | 218 | # TODO Prio 2 219 | def test_show_should_not_display_category_field_if_no_categories_are_defined 220 | =begin 221 | Project.find(1).issue_categories.delete_all 222 | 223 | get :show, :params => { 224 | :id => 1 225 | } 226 | assert_response :success 227 | assert_select 'table.attributes .category', 0 228 | =end 229 | end 230 | 231 | # DONE 232 | def test_show_with_project_enumeration_custom_field 233 | get :show, :params => { 234 | :id => 2 235 | } 236 | assert_response :success 237 | 238 | assert_select ".cf_1 .value", :text => @cf1_value1 239 | assert_select ".cf_2 .value", :text => "#{@cf2_value4}, #{@cf2_value5}, #{@cf2_value6}" 240 | 241 | 242 | issue2 = Issue.find(2) 243 | issue2.custom_field_values = {2 => [4]} 244 | issue2.save! 245 | 246 | get :show, :params => { 247 | :id => 2 248 | } 249 | assert_response :success 250 | 251 | assert_select ".cf_1 .value", :text => @cf1_value1 252 | assert_select ".cf_2 .value", :text => @cf2_value4 253 | end 254 | 255 | # DONE 256 | def test_show_with_project_enumeration_custom_field_multiple_value_empty 257 | get :show, :params => { 258 | :id => 1 259 | } 260 | assert_response :success 261 | 262 | assert_select ".cf_1 .value", :text => "#{@cf1_value1}, #{@cf1_value2}" 263 | assert_select ".cf_2 .value", :text => '' 264 | 265 | 266 | issue1 = Issue.find(1) 267 | issue1.custom_field_values = {1 => [1, 2]} 268 | issue1.save! 269 | 270 | 271 | get :show, :params => { 272 | :id => 1 273 | } 274 | assert_response :success 275 | 276 | assert_select ".cf_1 .value", :text => "#{@cf1_value1}, #{@cf1_value2}" 277 | end 278 | 279 | # DONE 280 | def test_show_with_project_enumeration_custom_multiple_removed 281 | cf1 = CustomField.find(1) 282 | cf1.update_attribute :multiple, false 283 | 284 | get :show, :params => { 285 | :id => 1 286 | } 287 | assert_response :success 288 | 289 | # Last one kept 290 | assert_select ".cf_1 .value", :text => @cf1_value2 291 | 292 | 293 | issue1 = Issue.find(1) 294 | issue1.custom_field_values = {1 => 2} 295 | issue1.save! 296 | 297 | get :show, :params => { 298 | :id => 1 299 | } 300 | assert_response :success 301 | 302 | assert_select ".cf_1 .value", :text => @cf1_value2 303 | end 304 | 305 | def test_get_new 306 | =begin 307 | @request.session[:user_id] = 2 308 | get :new, :params => { 309 | :project_id => 1, 310 | :tracker_id => 1 311 | } 312 | assert_response :success 313 | 314 | assert_select 'form#issue-form[action=?]', '/projects/ecookbook/issues' 315 | assert_select 'form#issue-form' do 316 | assert_select 'input[name=?]', 'issue[is_private]' 317 | assert_select 'select[name=?]', 'issue[project_id]' 318 | assert_select 'select[name=?]', 'issue[tracker_id]' 319 | assert_select 'input[name=?]', 'issue[subject]' 320 | assert_select 'textarea[name=?]', 'issue[description]' 321 | assert_select 'select[name=?]', 'issue[status_id]' 322 | assert_select 'select[name=?]', 'issue[priority_id]' 323 | assert_select 'select[name=?]', 'issue[assigned_to_id]' 324 | assert_select 'select[name=?]', 'issue[category_id]' 325 | assert_select 'select[name=?]', 'issue[fixed_version_id]' 326 | assert_select 'input[name=?]', 'issue[parent_issue_id]' 327 | assert_select 'input[name=?]', 'issue[start_date]' 328 | assert_select 'input[name=?]', 'issue[due_date]' 329 | assert_select 'select[name=?]', 'issue[done_ratio]' 330 | assert_select 'input[name=?][value=?]', 'issue[custom_field_values][2]', 'Default string' 331 | assert_select 'input[name=?]', 'issue[watcher_user_ids][]' 332 | end 333 | 334 | # Be sure we don't display inactive IssuePriorities 335 | assert ! IssuePriority.find(15).active? 336 | assert_select 'select[name=?]', 'issue[priority_id]' do 337 | assert_select 'option[value="15"]', 0 338 | end 339 | =end 340 | end 341 | 342 | # DONE 343 | def test_get_new_with_list_custom_field 344 | @request.session[:user_id] = 2 345 | get :new, :params => { 346 | :project_id => 1, 347 | :tracker_id => 1 348 | } 349 | assert_response :success 350 | 351 | # To test html generated 352 | # @response.parsed_body 353 | assert_select 'select.project_enumeration_cf[name=?]', 'issue[custom_field_values][1][]' do 354 | assert_select 'option', 1 355 | assert_select 'option[value="1"]', :text => 'Cat. 1' 356 | # value 2 locked, value 3 closed 357 | end 358 | end 359 | 360 | # TODO Prio 2 361 | def test_get_new_with_multi_custom_field 362 | =begin 363 | field = IssueCustomField.find(1) 364 | field.update_attribute :multiple, true 365 | 366 | @request.session[:user_id] = 2 367 | get :new, :params => { 368 | :project_id => 1, 369 | :tracker_id => 1 370 | } 371 | assert_response :success 372 | 373 | assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do 374 | assert_select 'option', 3 375 | assert_select 'option[value=MySQL]', :text => 'MySQL' 376 | end 377 | assert_select 'input[name=?][type=hidden][value=?]', 'issue[custom_field_values][1][]', '' 378 | =end 379 | end 380 | 381 | # DONE 382 | def test_post_create 383 | @request.session[:user_id] = 2 384 | 385 | assert_difference 'Issue.count' do 386 | assert_no_difference 'Journal.count' do 387 | post :create, :params => { 388 | :project_id => 1, 389 | :issue => { 390 | :tracker_id => 1, 391 | :status_id => 2, 392 | :subject => 'This is the test_new issue', 393 | :description => 'This is the description', 394 | :priority_id => 5, 395 | :start_date => '2019-11-13', 396 | :estimated_hours => '', 397 | :custom_field_values => { 398 | '1' => '1'} 399 | } 400 | } 401 | end 402 | end 403 | 404 | assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id 405 | 406 | issue = Issue.find_by_subject('This is the test_new issue') 407 | assert_not_nil issue 408 | assert_equal 2, issue.author_id 409 | assert_equal 1, issue.tracker_id 410 | assert_equal 2, issue.status_id 411 | assert_equal Date.parse('2019-11-13'), issue.start_date 412 | assert_nil issue.estimated_hours 413 | # The important part 414 | v = issue.custom_values.where(:custom_field_id => 1).first 415 | assert_not_nil v 416 | assert_equal '1', v.value 417 | end 418 | 419 | def test_post_create_without_custom_fields_param 420 | =begin 421 | @request.session[:user_id] = 2 422 | assert_difference 'Issue.count' do 423 | post :create, :params => { 424 | :project_id => 1, 425 | :issue => { 426 | :tracker_id => 1, 427 | :subject => 'This is the test_new issue', 428 | :description => 'This is the description', 429 | :priority_id => 5 430 | } 431 | } 432 | end 433 | assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id 434 | =end 435 | end 436 | 437 | # TODO Prio 3 438 | def test_post_create_with_multi_custom_field 439 | =begin 440 | field = IssueCustomField.find_by_name('Database') 441 | field.update_attribute(:multiple, true) 442 | 443 | @request.session[:user_id] = 2 444 | assert_difference 'Issue.count' do 445 | post :create, :params => { 446 | :project_id => 1, 447 | :issue => { 448 | :tracker_id => 1, 449 | :subject => 'This is the test_new issue', 450 | :description => 'This is the description', 451 | :priority_id => 5, 452 | :custom_field_values => { 453 | '1' => ['', 'MySQL', 'Oracle']} 454 | } 455 | } 456 | end 457 | assert_response 302 458 | issue = Issue.order('id DESC').first 459 | assert_equal ['MySQL', 'Oracle'], issue.custom_field_value(1).sort 460 | =end 461 | end 462 | 463 | # TODO Prio 3 464 | def test_post_create_with_empty_multi_custom_field 465 | =begin 466 | field = IssueCustomField.find_by_name('Database') 467 | field.update_attribute(:multiple, true) 468 | 469 | @request.session[:user_id] = 2 470 | assert_difference 'Issue.count' do 471 | post :create, :params => { 472 | :project_id => 1, 473 | :issue => { 474 | :tracker_id => 1, 475 | :subject => 'This is the test_new issue', 476 | :description => 'This is the description', 477 | :priority_id => 5, 478 | :custom_field_values => { 479 | '1' => ['']} 480 | } 481 | } 482 | end 483 | assert_response 302 484 | issue = Issue.order('id DESC').first 485 | assert_equal [''], issue.custom_field_value(1).sort 486 | =end 487 | end 488 | 489 | 490 | # TODO Prio 3 491 | def test_create_should_validate_required_list_fields 492 | =begin 493 | cf1 = IssueCustomField.create!(:name => 'Foo', :field_format => 'list', :is_for_all => true, :tracker_ids => [1, 2], :multiple => false, :possible_values => ['a', 'b']) 494 | cf2 = IssueCustomField.create!(:name => 'Bar', :field_format => 'list', :is_for_all => true, :tracker_ids => [1, 2], :multiple => true, :possible_values => ['a', 'b']) 495 | WorkflowPermission.delete_all 496 | WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf1.id.to_s, :rule => 'required') 497 | WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2, :role_id => 1, :field_name => cf2.id.to_s, :rule => 'required') 498 | @request.session[:user_id] = 2 499 | 500 | assert_no_difference 'Issue.count' do 501 | post :create, :params => { 502 | :project_id => 1, 503 | :issue => { 504 | :tracker_id => 2, 505 | :status_id => 1, 506 | :subject => 'Test', 507 | :start_date => '', 508 | :due_date => '', 509 | :custom_field_values => { 510 | cf1.id.to_s => '', cf2.id.to_s => [''] 511 | } 512 | 513 | } 514 | } 515 | assert_response :success 516 | end 517 | 518 | assert_select_error /Foo cannot be blank/i 519 | assert_select_error /Bar cannot be blank/i 520 | =end 521 | end 522 | 523 | # TODO Prio 2 524 | def test_get_edit 525 | =begin 526 | @request.session[:user_id] = 2 527 | get :edit, :params => { 528 | :id => 1 529 | } 530 | assert_response :success 531 | 532 | assert_select 'select[name=?]', 'issue[project_id]' 533 | # Be sure we don't display inactive IssuePriorities 534 | assert ! IssuePriority.find(15).active? 535 | assert_select 'select[name=?]', 'issue[priority_id]' do 536 | assert_select 'option[value="15"]', 0 537 | end 538 | =end 539 | end 540 | 541 | # TODO Prio 3 542 | def test_get_edit_with_params 543 | =begin 544 | @request.session[:user_id] = 2 545 | get :edit, :params => { 546 | :id => 1, 547 | :issue => { 548 | :status_id => 5, 549 | :priority_id => 7 550 | }, 551 | :time_entry => { 552 | :hours => '2.5', 553 | :comments => 'test_get_edit_with_params', 554 | :activity_id => 10 555 | } 556 | } 557 | assert_response :success 558 | 559 | assert_select 'select[name=?]', 'issue[status_id]' do 560 | assert_select 'option[value="5"][selected=selected]', :text => 'Closed' 561 | end 562 | 563 | assert_select 'select[name=?]', 'issue[priority_id]' do 564 | assert_select 'option[value="7"][selected=selected]', :text => 'Urgent' 565 | end 566 | 567 | assert_select 'input[name=?][value="2.50"]', 'time_entry[hours]' 568 | assert_select 'select[name=?]', 'time_entry[activity_id]' do 569 | assert_select 'option[value="10"][selected=selected]', :text => 'Development' 570 | end 571 | assert_select 'input[name=?][value=test_get_edit_with_params]', 'time_entry[comments]' 572 | =end 573 | end 574 | 575 | # TODO Prio 3 576 | def test_get_edit_with_multi_custom_field 577 | =begin 578 | field = CustomField.find(1) 579 | field.update_attribute :multiple, true 580 | issue = Issue.find(1) 581 | issue.custom_field_values = {1 => ['MySQL', 'Oracle']} 582 | issue.save! 583 | 584 | @request.session[:user_id] = 2 585 | get :edit, :params => { 586 | :id => 1 587 | } 588 | assert_response :success 589 | 590 | assert_select 'select[name=?][multiple=multiple]', 'issue[custom_field_values][1][]' do 591 | assert_select 'option', 3 592 | assert_select 'option[value=MySQL][selected=selected]' 593 | assert_select 'option[value=Oracle][selected=selected]' 594 | assert_select 'option[value=PostgreSQL]:not([selected])' 595 | end 596 | =end 597 | end 598 | 599 | def test_update_form_for_existing_issue 600 | =begin 601 | @request.session[:user_id] = 2 602 | patch :edit, :params => { 603 | :id => 1, 604 | :issue => { 605 | :tracker_id => 2, 606 | :subject => 'This is the test_new issue', 607 | :description => 'This is the description', 608 | :priority_id => 5 609 | } 610 | }, 611 | :xhr => true 612 | assert_response :success 613 | assert_equal 'text/javascript', response.content_type 614 | 615 | assert_include 'This is the test_new issue', response.body 616 | =end 617 | end 618 | 619 | # TODO Prio 3 620 | def test_update_form_should_keep_category_with_same_when_changing_project 621 | =begin 622 | source = Project.generate! 623 | target = Project.generate! 624 | source_category = IssueCategory.create!(:name => 'Foo', :project => source) 625 | target_category = IssueCategory.create!(:name => 'Foo', :project => target) 626 | issue = Issue.generate!(:project => source, :category => source_category) 627 | 628 | @request.session[:user_id] = 1 629 | patch :edit, :params => { 630 | :id => issue.id, 631 | :issue => { 632 | :project_id => target.id, 633 | :category_id => source_category.id 634 | } 635 | } 636 | assert_response :success 637 | 638 | assert_select 'select[name=?]', 'issue[category_id]' do 639 | assert_select 'option[value=?][selected=selected]', target_category.id.to_s 640 | end 641 | =end 642 | end 643 | 644 | # TODO Prio 2 645 | def test_put_update_with_custom_field_change 646 | =begin 647 | @request.session[:user_id] = 2 648 | issue = Issue.find(1) 649 | assert_equal '125', issue.custom_value_for(2).value 650 | 651 | with_settings :notified_events => %w(issue_updated) do 652 | assert_difference('Journal.count') do 653 | assert_difference('JournalDetail.count', 3) do 654 | put :update, :params => { 655 | :id => 1, 656 | :issue => { 657 | :subject => 'Custom field change', 658 | :priority_id => '6', 659 | :category_id => '1', # no change 660 | :custom_field_values => { '2' => 'New custom value' } 661 | } 662 | } 663 | end 664 | end 665 | end 666 | assert_redirected_to :action => 'show', :id => '1' 667 | issue.reload 668 | assert_equal 'New custom value', issue.custom_value_for(2).value 669 | 670 | mail = ActionMailer::Base.deliveries.last 671 | assert_not_nil mail 672 | assert_mail_body_match "Searchable field changed from 125 to New custom value", mail 673 | =end 674 | end 675 | 676 | # TODO Prio 3 677 | def test_put_update_with_multi_custom_field_change 678 | =begin 679 | field = CustomField.find(1) 680 | field.update_attribute :multiple, true 681 | issue = Issue.find(1) 682 | issue.custom_field_values = {1 => ['MySQL', 'Oracle']} 683 | issue.save! 684 | 685 | @request.session[:user_id] = 2 686 | assert_difference('Journal.count') do 687 | assert_difference('JournalDetail.count', 3) do 688 | put :update, :params => { 689 | :id => 1, 690 | :issue => { 691 | :subject => 'Custom field change', 692 | :custom_field_values => { 693 | '1' => ['', 'Oracle', 'PostgreSQL'] 694 | } 695 | 696 | } 697 | } 698 | end 699 | end 700 | assert_redirected_to :action => 'show', :id => '1' 701 | assert_equal ['Oracle', 'PostgreSQL'], Issue.find(1).custom_field_value(1).sort 702 | =end 703 | end 704 | end 705 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | 4 | module Redmine 5 | module PluginFixturesLoader 6 | def self.included(base) 7 | base.class_eval do 8 | def self.plugin_fixtures(*symbols) 9 | ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', symbols) 10 | end 11 | end 12 | end 13 | end 14 | end 15 | 16 | unless ActiveSupport::TestCase.included_modules.include?(Redmine::PluginFixturesLoader) 17 | ActiveSupport::TestCase.send :include, Redmine::PluginFixturesLoader 18 | end -------------------------------------------------------------------------------- /test/unit/custom_value_test.rb: -------------------------------------------------------------------------------- 1 | # Redmine - project management software 2 | # Copyright (C) 2006-2017 Jean-Philippe Lang 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | require File.expand_path('../../test_helper', __FILE__) 19 | 20 | class CustomValueTest < ActiveSupport::TestCase 21 | fixtures :projects, :issues 22 | 23 | plugin_fixtures :custom_fields, :custom_values, :project_enumerations, :custom_fields_projects, :custom_fields_trackers 24 | 25 | def test_project_enumeraions_custom_field_properties 26 | cf1 = IssueCustomField.find_by_id(1) 27 | 28 | assert_not_nil cf1 29 | assert_equal 'Project CF Enum 1', cf1.name 30 | assert_equal 'project_enumeration', cf1.field_format 31 | assert cf1.multiple 32 | assert_empty cf1.possible_values 33 | end 34 | end 35 | --------------------------------------------------------------------------------