├── .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 | 
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 | 
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 |
20 | <%
21 | @project_enumerations.each_with_index do |project_enumeration, position|
22 | -%>
23 | -
24 |
25 | <%= hidden_field_tag "project_enumerations[#{project_enumeration.id}][id]", project_enumeration.id %>
26 | <%= hidden_field_tag "project_enumerations[#{project_enumeration.id}][position]", position, :class => 'position' %>
27 |
28 | <%= text_field_tag "project_enumerations[#{project_enumeration.id}][value]", project_enumeration.value, :size => 80 %>
29 | <%= delete_link destroy_project_project_enumerations_path(@project, :id => project_enumeration) %>
30 |
31 |
32 |
36 |
37 |
38 |
42 | <%
43 | if project_enumeration.project_id != @project.id
44 | -%>
45 |
46 |
47 | <%= l(:field_project) %> : <%= @project_enumeration.project.name %>
48 | <%
49 | end
50 | -%>
51 |
52 | <% end %>
53 |
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 |
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 |
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 |
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 | <%= l(:label_custom_field) %> |
62 | <%= l(:label_project_enumeration_value) %> |
63 | <%= l(:field_status) %> |
64 | <%= l(:field_sharing) %> |
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 | <%= custom_field_name_tag(project_enumeration.custom_field) %> |
86 |
87 | <%= project_enumeration_value %> |
88 |
89 | <%= l("version_status_#{project_enumeration.status}") %> |
90 | <%
91 | # TODO add format_enumeration_sharing
92 | -%>
93 | <%= format_version_sharing(project_enumeration.sharing) %> |
94 |
95 |
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 | |
105 |
106 | <% end %>
107 |
108 |
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 |
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 | <%= l(:label_custom_field) %> |
63 | <%= l(:label_project_list_value_value) %> |
64 | <%= l(:field_status) %> |
65 | <%= l(:field_sharing) %> |
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 | <%= custom_field_name_tag(project_list_value.custom_field) %> |
87 |
88 | <%= project_list_value_value %> |
89 |
90 | <%= l("version_status_#{project_list_value.status}") %> |
91 | <%
92 | # TODO add format_list_value_sharing
93 | -%>
94 | <%= format_version_sharing(project_list_value.sharing) %> |
95 |
96 |
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 | |
106 |
107 | <% end %>
108 |
109 |
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 |
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 |
--------------------------------------------------------------------------------