├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.en.md ├── README.md ├── app ├── hooks │ └── issue_view_hook.rb ├── patches │ ├── issue_custom_field_patch.rb │ └── issue_patch.rb └── views │ ├── custom_fields │ └── formats │ │ └── _serial_number.html.erb │ └── issues │ └── _remove_serial_number_field.html.erb ├── config ├── locales │ ├── en.yml │ ├── fr.yml │ ├── ja.yml │ └── tr.yml └── routes.rb ├── doc └── images │ ├── issues.png │ ├── usage.en.png │ └── usage.png ├── init.rb ├── lib ├── format.rb └── serial_number_field.rb └── test ├── functional ├── custom_fields_controller_test.rb └── issues_controller_test.rb ├── test_helper.rb └── unit └── issue_test.rb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: redmine-plugin-test-action 10 | uses: two-pack/redmine-plugin-test-action@v2.0.2 11 | with: 12 | plugin_name: redmine_serial_number_field 13 | redmine_version: v4.1 14 | ruby_version: v2.6 15 | database: postgresql 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.loadpath 3 | /config/additional_environment.rb 4 | /config/email.yml 5 | /coverage 6 | /db/*.db 7 | /db/*.sqlite3 8 | /db/schema.rb 9 | /files/* 10 | /lib/redmine/scm/adapters/mercurial/redminehelper.pyc 11 | /lib/redmine/scm/adapters/mercurial/redminehelper.pyo 12 | /log/*.log* 13 | /log/mongrel_debug 14 | /public/dispatch.* 15 | /public/plugin_assets 16 | /tmp/* 17 | /tmp/cache/* 18 | /tmp/pdf/* 19 | /tmp/sessions/* 20 | /tmp/sockets/* 21 | /tmp/test/* 22 | /tmp/thumbnails/* 23 | /vendor/cache 24 | /vendor/rails 25 | *.rbc 26 | 27 | /.bundle 28 | /Gemfile.local 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 2 | 3 | - Added ISO format #8 (@rambowm) 4 | - Dropped Redmine 4.0 support 5 | - Redmine 4.1 support 6 | - Fixed automatic serial number when copying issue 7 | - チケットコピー時に自動採番されない #6 (@subaru019) 8 | - [Review comments for this plugin on redmine.org](https://www.redmine.org/plugins/redmine_serial_number_field) (Jorge Garcia, Quan VN, qn wang) 9 | 10 | ## 2.0.0 11 | 12 | - Support Redmine 4.0 (@nanego) 13 | - Support French locale (@nanego) 14 | - Drop support Redmine version 3.4 lower (@maeda-m) 15 | 16 | ## 1.0.0 17 | 18 | - Changed the range to count sequential numbers from project unit to whole sharing as custom field unit #4 (@neocosmic2) 19 | 20 | ## 0.9.0 21 | 22 | - Support Turkish locale #3 (@atopcu) 23 | - Allow only sequence numbers #2 (@y-arai) 24 | - Drop support Redmine versions 2.6 and 3.1 (@maeda-m) 25 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'fiscali' 2 | 3 | group :test do 4 | gem 'rails-controller-testing' 5 | gem 'byebug' 6 | end 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matsukei Co.,Ltd 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.en.md: -------------------------------------------------------------------------------- 1 | # Redmine Serial Number Field 2 | 3 | [日本語 »](https://github.com/matsukei/redmine_serial_number_field/blob/master/README.md) 4 | 5 | Add a format to be serial number in the specified format as a issue custom field. 6 | 7 | ## Features 8 | 9 | * "Automatic serial number" is available as a format for custom fields for issues. 10 | * After creating a new custom field, you can not edit the "Regular expression". 11 | * Every user who can create a issue has automatic number assignment authority. 12 | * Automatically assign serial numbers in the specified format at issue registration or update(Including bulk operations). 13 | * Assign a serial number for each custom field. If the same custom field is used for multiple projects, It will be consecutive numbers in those projects. 14 | * Custom field items are displayed when viewing issues. However, it will not be displayed when registering or updating. 15 | * Basic options for custom fields are also available, such as issue filter criteria and search target. 16 | 17 | ### Notes 18 | 19 | #### When you change tracker or project of numbered issue 20 | 21 | * If the changed tracker does not have the same custom field, the serial number assigned will be deleted. 22 | * If the tracker after change has the same custom field, the serial numbers numbered will not change. 23 | 24 | #### When you set permissions for custom fields in workflow 25 | 26 | * Setting a custom field for automatic number assignment to read only will not work properly. 27 | 28 | ## Usage 29 | 30 | 1. Create a new custom field for the issue. 31 | 2. Change the item "Format" to "Automatic serial number". 32 | 3. Specify the serial number format in the item "Regular expression". 33 | 4. If you wish to use it as a filter or search condition, please check as appropriate. 34 | 5. Specify the tracker and project you want to number automatically. 35 | 6. Done! 36 | * If you create a new issue it will be automatically numbered. 37 | * Issues already created will be numbered as they are updated. 38 | 39 | ## Screenshot 40 | 41 | *Administration > Custom fields > Issues > Automatic serial number* 42 | 43 |  44 | 45 | *Issues* 46 | 47 |  48 | 49 | ## Supported versions 50 | 51 | * Redmine 4.1 (Ruby 2.6) 52 | 53 | ## Format specifications 54 | 55 | |Column used in year format |Year format|fiscal year(4/1 - 3/31)|e.g. Regular expression |e.g Result (2015-03-31)| 56 | |---------------------------|-----------|-----------------------|-------------------------|-----------------------| 57 | |Issue#created_on |`yyyy` |No |`{yyyy}-{0000}` |`2015-0001` | 58 | |^ |`yy` |No |`{yy}-{0000}` |`15-0001` | 59 | |^ |`YYYY` |Yes |`{YYYY}-{0000}` |`2014-0001` | 60 | |^ |`YY` |Yes |`{YY}-{0000}` |`14-0001` | 61 | |^ |`ISO` |No |`{ISO}-{0000}` |`20150331-0001` | 62 | 63 | * OK 64 | * `{000000}` #=> `000001` 65 | * `ABC-{yy}-{00}` #=> `ABC-15-01` 66 | * NG 67 | * When the end is not the serial number format 68 | * e.g. `ABC-{000}-{yy}` 69 | * When format not including year format or serial number format is included. 70 | * e.g. `{abc}-{yy}-{000}` 71 | 72 | ## Install 73 | 74 | 1. git clone or copy an unarchived plugin to `plugins/redmine_serial_number_field` on your Redmine path. 75 | 2. `$ cd your_redmine_path/` 76 | 3. `$ bundle install` 77 | 4. Please restart Redmine 78 | 79 | ## License 80 | 81 | [The MIT License](https://opensource.org/licenses/MIT) 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine Serial Number Field 2 | 3 | [English »](https://github.com/matsukei/redmine_serial_number_field/blob/master/README.en.md) 4 | 5 | Redmine に自動で連番を付加するカスタムフィールドを提供するプラグインです。 6 | 7 | ## Features 8 | 9 | * チケットに対するカスタムフィールドの書式として「自動採番」が利用可能になります。 10 | * カスタムフィールドの新規作成後は、正規表現を編集することは、できません。 11 | * チケットが登録できる、全てのユーザーは、自動採番の権限があります。 12 | * 指定した正規表現に基づいて、チケット登録や更新時(一括操作も含む)に自動的に採番します。 13 | * カスタムフィールド単位での採番となります。同じカスタムフィールドを複数のプロジェクトで使用している場合は、それらのプロジェクトで連続した採番になります。 14 | * カスタムフィールド項目は、チケットの閲覧時には表示します。しかし、登録および更新時には表示しません。 15 | * チケットのフィルタ条件、検索対象など、カスタムフィールドの基本的なオプションも利用可能です。 16 | 17 | ### Notes 18 | 19 | #### 採番済みチケットのトラッカーやプロジェクトを変更した場合 20 | 21 | * 変更後のトラッカーが同じカスタムフィールドを持っていない場合、採番された連番は削除されます。 22 | * 変更後のトラッカーが同じカスタムフィールドを持っている場合、採番された連番は変化しません。 23 | 24 | #### ワークフローのカスタムフィールドに対する権限を設定した場合 25 | 26 | * 自動採番のカスタムフィールドを読み取り専用に設定すると動作がおかしくなる場合があります。 27 | 28 | ## Usage 29 | 30 | 1. チケットの新しいカスタムフィールドを作成します。 31 | 2. 項目「書式」を自動採番に変更します。 32 | 3. 項目「正規表現」に年表記書式や連番書式を指定します。 33 | 4. フィルタや検索条件として使いたい場合は、適宜チェックを入れます。 34 | 5. 自動採番をしたいトラッカーとプロジェクトを指定します。 35 | 6. 完了です。 36 | * 新たにチケットを作成すれば自動で採番されます。 37 | * 既に作成されたチケットは何かしら更新すれば採番されます。 38 | 39 | ## Screenshot 40 | 41 | *Administration > Custom fields > Issues > 自動採番* 42 | 43 |  44 | 45 | *Issues* 46 | 47 |  48 | 49 | ## Supported versions 50 | 51 | * Redmine 4.1 (Ruby 2.6) 52 | 53 | ## Format specifications 54 | 55 | |採番対象の日付 |年表記書式 |年度(4/1 - 3/31) |例: 正規表現 |例: 結果(2015-03-31) | 56 | |---------------------------|-----------|-----------------------|-------------------------|-----------------------| 57 | |Issue#created_on |`yyyy` |No |`{yyyy}-{0000}` |`2015-0001` | 58 | |^ |`yy` |No |`{yy}-{0000}` |`15-0001` | 59 | |^ |`YYYY` |Yes |`{YYYY}-{0000}` |`2014-0001` | 60 | |^ |`YY` |Yes |`{YY}-{0000}` |`14-0001` | 61 | |^ |`ISO` |No |`{ISO}-{0000}` |`20150331-0001` | 62 | 63 | * OK 64 | * `{000000}` #=> `000001` 65 | * `ABC-{yy}-{00}` #=> `ABC-15-01` 66 | * NG 67 | * 末尾が連番書式でない場合 68 | * e.g. `ABC-{000}-{yy}` 69 | * 年表記書式、連番書式でない場合 70 | * e.g. `{abc}-{yy}-{000}` 71 | 72 | ## Install 73 | 74 | 1. `your_redmine_path/plugins/redmine_serial_number_field/` に clone もしくはダウンロードしたソースを配置します 75 | 2. `$ cd your_redmine_path/` 76 | 3. `$ bundle install` 77 | 4. Redmineを再起動してください 78 | 79 | ## License 80 | 81 | [The MIT License](https://opensource.org/licenses/MIT) 82 | -------------------------------------------------------------------------------- /app/hooks/issue_view_hook.rb: -------------------------------------------------------------------------------- 1 | module SerialNumberField 2 | class IssueViewHooks < Redmine::Hook::ViewListener 3 | render_on :view_issues_form_details_bottom, :partial => 'remove_serial_number_field' 4 | render_on :view_issues_bulk_edit_details_bottom, :partial => 'remove_serial_number_field' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/patches/issue_custom_field_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'issue_custom_field' 2 | 3 | module SerialNumberField 4 | module IssueCustomFieldPatch 5 | extend ActiveSupport::Concern 6 | 7 | def validate_custom_field 8 | super 9 | 10 | invalid_message = l('activerecord.errors.messages.invalid') 11 | if errors[:regexp].include?(invalid_message) && field_format == SerialNumberField::Format::NAME 12 | regexp_error_messages = errors[:regexp].clone 13 | 14 | errors.delete(:regexp) 15 | regexp_error_messages.each do |regexp_error_message| 16 | errors[:regexp] = regexp_error_message unless regexp_error_message == invalid_message 17 | end 18 | end 19 | end 20 | 21 | end 22 | end 23 | 24 | SerialNumberField::IssueCustomFieldPatch.tap do |mod| 25 | IssueCustomField.send :prepend, mod unless IssueCustomField.include?(mod) 26 | end 27 | -------------------------------------------------------------------------------- /app/patches/issue_patch.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'issue' 2 | 3 | module SerialNumberField 4 | module IssuePatch 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | after_save :assign_serial_number! 9 | end 10 | 11 | def assign_serial_number! 12 | serial_number_fields.each do |cf| 13 | next if assigned_serial_number?(cf) && !copy? 14 | 15 | target_custom_value = serial_number_custom_value(cf) 16 | new_serial_number = cf.format.generate_value(cf, self) 17 | 18 | if target_custom_value.present? 19 | target_custom_value.update_attributes!( 20 | :value => new_serial_number) 21 | end 22 | end 23 | end 24 | 25 | def assigned_serial_number?(cf) 26 | serial_number_custom_value(cf).try(:value).present? 27 | end 28 | 29 | def serial_number_custom_value(cf) 30 | CustomValue.where(:custom_field_id => cf.id, 31 | :customized_type => 'Issue', 32 | :customized_id => self.id).first 33 | end 34 | 35 | def serial_number_fields 36 | editable_custom_fields.select do |value| 37 | value.field_format == SerialNumberField::Format::NAME 38 | end 39 | end 40 | 41 | end 42 | end 43 | 44 | SerialNumberField::IssuePatch.tap do |mod| 45 | Issue.send :include, mod unless Issue.include?(mod) 46 | end 47 | -------------------------------------------------------------------------------- /app/views/custom_fields/formats/_serial_number.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= f.text_field(:regexp, :size => 50, :required => true, :disabled => !custom_field.new_record?) %> 3 | e.g. created_on: 2015-03-31 4 | {yyyy}-{0000} #=> 2015-0001 5 | {yy}-{0000} #=> 15-0001 6 | {YYYY}-{0000} #=> 2014-0001 7 | {YY}-{0000} #=> 14-0001 8 | {ISO}-{0000} #=> 20150331-0001 9 |
10 | 11 | <%= f.hidden_field :is_required, :value => '0' %> 12 | <%= f.hidden_field :visible, :value => '1' %> 13 | <%= hidden_field_tag 'custom_field[role_ids][]', '' %> 14 | 15 | 21 | -------------------------------------------------------------------------------- /app/views/issues/_remove_serial_number_field.html.erb: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | label_serial_number: Automatic serial number 3 | 4 | activerecord: 5 | errors: 6 | messages: 7 | end_must_numeric_format_in_serial_number: "is end must be a serial number format" 8 | invalid_format_in_serial_number: "is contains an invalid format" 9 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | label_serial_number: Numéro auto-incrémenté 3 | 4 | activerecord: 5 | errors: 6 | messages: 7 | end_must_numeric_format_in_serial_number: "doit terminer par le numéro auto-incrémenté" 8 | invalid_format_in_serial_number: "format invalide" 9 | -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | label_serial_number: 自動採番 3 | 4 | activerecord: 5 | errors: 6 | messages: 7 | end_must_numeric_format_in_serial_number: "の末尾は連番書式でなければなりません。" 8 | invalid_format_in_serial_number: "に無効な書式が含まれています。" 9 | -------------------------------------------------------------------------------- /config/locales/tr.yml: -------------------------------------------------------------------------------- 1 | tr: 2 | label_serial_number: Otomatik Seri No 3 | 4 | activerecord: 5 | errors: 6 | messages: 7 | end_must_numeric_format_in_serial_number: "Sonunda Numara formatı olmak zorunda" 8 | invalid_format_in_serial_number: "Geçersiz format içeriyor" 9 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # Plugin's routes 2 | # See: http://guides.rubyonrails.org/routing.html 3 | -------------------------------------------------------------------------------- /doc/images/issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsukei/redmine_serial_number_field/f1b14aa3780d3f7e8b0ed0667be7dbd641a94322/doc/images/issues.png -------------------------------------------------------------------------------- /doc/images/usage.en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsukei/redmine_serial_number_field/f1b14aa3780d3f7e8b0ed0667be7dbd641a94322/doc/images/usage.en.png -------------------------------------------------------------------------------- /doc/images/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matsukei/redmine_serial_number_field/f1b14aa3780d3f7e8b0ed0667be7dbd641a94322/doc/images/usage.png -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | Redmine::Plugin.register :redmine_serial_number_field do 2 | name 'Redmine Serial Number Field' 3 | author 'Matsukei Co.,Ltd' 4 | description 'Add a format to be serial number in the specified format as a issue custom field.' 5 | version '3.0.0' 6 | requires_redmine version_or_higher: '4.1' 7 | url 'https://github.com/matsukei/redmine_serial_number_field' 8 | author_url 'http://www.matsukei.co.jp/' 9 | end 10 | 11 | require_relative 'lib/format' 12 | require_relative 'lib/serial_number_field' 13 | -------------------------------------------------------------------------------- /lib/format.rb: -------------------------------------------------------------------------------- 1 | module SerialNumberField 2 | class Format < Redmine::FieldFormat::Base 3 | NAME = 'serial_number' 4 | 5 | add NAME 6 | self.searchable_supported = true 7 | self.customized_class_names = %w(Issue) 8 | self.form_partial = 'custom_fields/formats/serial_number' 9 | 10 | FORMAT_WRAPPER = /\{(.+?)\}/ 11 | DATE_FORMATS = { 12 | :'yyyy' => { :strftime => '%Y', :financial_year => false }, 13 | :'yy' => { :strftime => '%y', :financial_year => false }, 14 | :'YYYY' => { :strftime => '%Y', :financial_year => true }, 15 | :'YY' => { :strftime => '%y', :financial_year => true }, 16 | :'ISO' => { :strftime => '%Y%m%d', :financial_year => false } 17 | } 18 | 19 | def validate_custom_field(custom_field) 20 | value = custom_field.regexp 21 | errors = [] 22 | 23 | errors << [:regexp, :end_must_numeric_format_in_serial_number] unless value =~ /\{0+\}\Z/ 24 | replace_format_value(custom_field) do |format_value| 25 | unless format_value =~ /\A0+\Z/ 26 | errors << [:regexp, :invalid_format_in_serial_number] unless date_format_keys.include?(format_value) 27 | end 28 | end 29 | 30 | errors.uniq 31 | end 32 | 33 | def generate_value(custom_field, issue) 34 | datetime = issue.created_on.to_datetime || DateTime.now 35 | value = max_custom_value(custom_field, datetime) 36 | 37 | if value.present? 38 | value.next 39 | else 40 | generate_first_value(custom_field, datetime) 41 | end 42 | end 43 | 44 | private 45 | 46 | def date_format_keys 47 | DATE_FORMATS.stringify_keys.keys 48 | end 49 | 50 | def max_custom_value(custom_field, datetime) 51 | matcher = generate_matcher(custom_field, datetime) 52 | custom_values = custom_field.custom_values.map(&:value) 53 | # custom_values #=> e.g. ['2014-001', '2014-002', '14-0001', ...] 54 | custom_values.select { |value| value =~ matcher }.sort.last 55 | end 56 | 57 | def replace_format_value(custom_field) 58 | if block_given? 59 | custom_field.regexp.gsub(FORMAT_WRAPPER) do |format_value_with_brace| 60 | # format_value_with_brace #=> e.g. '{yy}', '{0000}' 61 | # $1.clone #=> e.g. 'yy', '0000' 62 | yield($1.clone) 63 | end 64 | end 65 | end 66 | 67 | # e.g. /2015-\d{3}/ 68 | def generate_matcher(custom_field, datetime) 69 | matcher_str = replace_format_value(custom_field) do |format_value| 70 | if date_format_keys.include?(format_value) 71 | generate_date_value(format_value, datetime) 72 | else 73 | # TODO: 74 | # 75 | # CustomField of A.regexp #=> '{yy}-0{000}' 76 | # matcher of A: /15-0\d{3}/ 77 | # CustomField of B.regexp #=> '{yy}-{0000}' 78 | # matcher of B: /15-\d{4}/ 79 | # 80 | # if CustomValue is '15-0999', 81 | # CustomValue matches matcher of both A and B 82 | generate_number_matcher(format_value) 83 | end 84 | end 85 | 86 | Regexp.new(matcher_str) 87 | end 88 | 89 | # e.g. '\d{3}' 90 | def generate_number_matcher(format_value) 91 | ['\d', '{', format_value.size.to_s, '}'].join 92 | end 93 | 94 | def generate_first_value(custom_field, datetime) 95 | replace_format_value(custom_field) do |format_value| 96 | if date_format_keys.include?(format_value) 97 | generate_date_value(format_value, datetime) 98 | else 99 | generate_number_value(format_value) 100 | end 101 | end 102 | end 103 | 104 | # e.g. '2015' 105 | def generate_date_value(format_value, datetime) 106 | parse_conf = DATE_FORMATS[format_value.to_sym] 107 | if parse_conf.key?(:financial_year) && parse_conf[:financial_year] 108 | # TODO initializers 109 | DateTime.fiscal_zone = :japan 110 | datetime = datetime.beginning_of_financial_year 111 | end 112 | 113 | datetime.strftime(parse_conf[:strftime]) 114 | end 115 | 116 | # e.g. '0001' 117 | def generate_number_value(format_value) 118 | '1'.rjust(format_value.size, '0') 119 | end 120 | 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/serial_number_field.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module SerialNumberField 4 | def self.root 5 | @root ||= Pathname.new File.expand_path('..', File.dirname(__FILE__)) 6 | end 7 | end 8 | 9 | # Load patches for Redmine 10 | Rails.configuration.to_prepare do 11 | Dir[SerialNumberField.root.join('app/patches/**/*_patch.rb')].each { |f| require_dependency f } 12 | end 13 | 14 | Dir[SerialNumberField.root.join('app/hooks/**/*_hook.rb')].each { |f| require_dependency f } 15 | -------------------------------------------------------------------------------- /test/functional/custom_fields_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class SerialNumberField::CustomFieldsControllerTest < ActionController::TestCase 4 | tests CustomFieldsController 5 | 6 | fixtures :custom_fields, :custom_values, 7 | :custom_fields_projects, :custom_fields_trackers, 8 | :roles, :users, 9 | :members, :member_roles, 10 | :groups_users, 11 | :trackers, :projects_trackers, 12 | :enabled_modules, 13 | :projects, :issues, 14 | :issue_statuses, 15 | :issue_categories, 16 | :enumerations, 17 | :workflows 18 | 19 | def setup 20 | @request.session[:user_id] = 1 21 | end 22 | 23 | def test_new_should_serial_number_format_has_only_issue 24 | CustomFieldsHelper::CUSTOM_FIELDS_TABS.each do |tab| 25 | type = tab[:name] 26 | format_name = SerialNumberField::Format::NAME 27 | expect_selected = 'IssueCustomField' == type ? 1 : 0 28 | 29 | get :new, :params => { 30 | :type => type, 31 | :custom_field => { 32 | :field_format => format_name 33 | } 34 | } 35 | assert_response :success 36 | 37 | assert_select 'form#custom_field_form' do 38 | assert_select 'select[name=?]', 'custom_field[field_format]' do 39 | assert_select 'option[value=?][selected=selected]', format_name, expect_selected 40 | end 41 | end 42 | end 43 | end 44 | 45 | def test_new_serial_number_format 46 | get :new, :params => { 47 | :type => 'IssueCustomField', 48 | :custom_field => { 49 | :field_format => SerialNumberField::Format::NAME 50 | } 51 | } 52 | assert_response :success 53 | 54 | assert_select 'form#custom_field_form' do 55 | assert_select 'input[name=?]', 'custom_field[name]' 56 | assert_select 'textarea[name=?]', 'custom_field[description]' 57 | assert_select 'input[name=?]:not([disabled])', 'custom_field[regexp]' 58 | # Trackers 59 | assert_select 'input[type=checkbox][name=?]', 'custom_field[project_ids][]', Project.count 60 | assert_select 'input[type=hidden][name=?]', 'custom_field[project_ids][]', 1 61 | # Projects 62 | assert_select 'input[type=checkbox][name=?]', 'custom_field[tracker_ids][]', Tracker.count 63 | assert_select 'input[type=hidden][name=?]', 'custom_field[tracker_ids][]', 1 64 | # Delete the screen input item later 65 | assert_select 'input[type=hidden][value="0"][name=?]', 'custom_field[is_required]' 66 | assert_select 'input[type=hidden][value="1"][name=?]', 'custom_field[visible]' 67 | assert_select 'input[type=hidden][value=""][name=?]', 'custom_field[role_ids][]' 68 | end 69 | end 70 | 71 | def test_create_serial_number_field 72 | valid_regexp_values.each_with_index do |valid_regexp, i| 73 | field_name = "auto_number_#{i.next}" 74 | field = new_record(IssueCustomField) do 75 | post :create, :params => { 76 | :type => "IssueCustomField", 77 | :custom_field => { 78 | :field_format => "serial_number", 79 | :name => field_name, 80 | :description => "", 81 | :regexp => valid_regexp, 82 | :is_required =>"0", 83 | :is_filter => "1", 84 | :searchable => "1", 85 | :visible => "1", 86 | :tracker_ids => ["1", ""], 87 | :is_for_all => "0", 88 | :project_ids => ["1", "3", ""] 89 | } 90 | } 91 | end 92 | assert_redirected_to "/custom_fields/#{field.id}/edit" 93 | 94 | assert_equal field_name, field.name 95 | assert_equal valid_regexp, field.regexp 96 | assert_equal [1], field.trackers.map(&:id).sort 97 | assert_equal [1, 3], field.projects.map(&:id).sort 98 | end 99 | end 100 | 101 | def test_create_serial_number_field_with_failure 102 | invalid_regexp_values.each_with_index do |invalid_regexp, i| 103 | assert_no_difference 'CustomField.count' do 104 | post :create, :params => { 105 | :type => "IssueCustomField", 106 | :custom_field => { 107 | :field_format => "serial_number", 108 | :name => "auto_number_#{i.next}", 109 | :regexp => invalid_regexp, 110 | :is_required =>"0", 111 | :visible => "1" 112 | } 113 | } 114 | end 115 | assert_response :success 116 | 117 | assert_select_error /regular expression is /i 118 | end 119 | end 120 | 121 | def test_edit_serial_number_field 122 | custom_field = create_default_serial_number_field 123 | 124 | get :edit, :params => { 125 | :id => custom_field.id 126 | } 127 | 128 | assert_response :success 129 | 130 | assert_select 'input[name=?][value=?]', 'custom_field[name]', custom_field.name 131 | assert_select 'input[name=?][value=?][disabled=disabled]', 'custom_field[regexp]', custom_field.regexp 132 | end 133 | 134 | def test_update_serial_number_field 135 | custom_field = create_default_serial_number_field 136 | 137 | valid_regexp_values.each_with_index do |valid_regexp, i| 138 | put :update, :params => { 139 | :id => custom_field.id, 140 | :custom_field => { 141 | :regexp => valid_regexp 142 | } 143 | } 144 | assert_redirected_to "/custom_fields/#{custom_field.id}/edit" 145 | 146 | custom_field.reload 147 | assert_equal valid_regexp, custom_field.regexp 148 | end 149 | end 150 | 151 | def test_update_serial_number_field_with_failure 152 | custom_field = create_default_serial_number_field 153 | 154 | invalid_regexp_values.each_with_index do |invalid_regexp, i| 155 | put :update, :params => { 156 | :id => custom_field.id, 157 | :custom_field => { 158 | :regexp => invalid_regexp 159 | } 160 | } 161 | assert_response :success 162 | 163 | assert_select_error /regular expression is /i 164 | end 165 | end 166 | 167 | end 168 | -------------------------------------------------------------------------------- /test/functional/issues_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class SerialNumberField::IssuesControllerTest < ActionController::TestCase 4 | tests IssuesController 5 | 6 | fixtures :projects, 7 | :users, :email_addresses, :user_preferences, 8 | :roles, :members, :member_roles, 9 | :issues, :issue_statuses, :issue_relations, 10 | :versions, :trackers, :projects_trackers, 11 | :issue_categories, :enabled_modules, 12 | :enumerations, :attachments, :workflows, 13 | :custom_fields, :custom_values, 14 | :custom_fields_projects, :custom_fields_trackers, 15 | :time_entries, :journals, :journal_details, 16 | :queries, :repositories, :changesets 17 | 18 | include Redmine::I18n 19 | 20 | def setup 21 | @default_custom_field = create_default_serial_number_field 22 | @for_all_custom_field = create_for_all_serial_number_field 23 | @request.session[:user_id] = 2 24 | end 25 | 26 | def test_get_new 27 | get :new, :params => { 28 | :project_id => 1, 29 | :tracker_id => 1 30 | } 31 | assert_response :success 32 | 33 | assert_select 'form#issue-form' do 34 | # Delete the screen input item later 35 | assert_select 'input[name=?]', "issue[custom_field_values][#{@default_custom_field.id}]" 36 | assert_select 'input[name=?]', "issue[custom_field_values][#{@for_all_custom_field.id}]" 37 | end 38 | end 39 | 40 | def test_post_create_and_show_and_get_edit_update_with_current_created_and_post_copy 41 | # create 42 | assert_difference 'Issue.count' do 43 | assert_no_difference 'Journal.count' do 44 | post :create, :params => { 45 | :project_id => 1, 46 | :issue => { 47 | :tracker_id => 1, 48 | :status_id => 2, 49 | :subject => 'This is the test_new issue', 50 | } 51 | } 52 | end 53 | end 54 | assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id 55 | 56 | issue = Issue.find_by_subject('This is the test_new issue') 57 | assert_added_serial_number(issue.id, 'MCC-0001', @default_custom_field) 58 | assert_added_serial_number(issue.id, 'MCC-0001', @for_all_custom_field) 59 | 60 | # show 61 | get :show, :params => { 62 | :id => issue.id 63 | } 64 | assert_response :success 65 | 66 | assert_select "div.cf_#{@default_custom_field.id}.attribute" do 67 | assert_select 'div.label' do 68 | assert_select 'span', text: @default_custom_field.name 69 | end 70 | assert_select 'div.value', text: /MCC-0001/ 71 | end 72 | 73 | assert_select "div.cf_#{@for_all_custom_field.id}.attribute" do 74 | assert_select 'div.label' do 75 | assert_select 'span', text: @for_all_custom_field.name 76 | end 77 | assert_select 'div.value', text: /MCC-0001/ 78 | end 79 | 80 | # edit 81 | get :edit, :params => { 82 | :id => issue.id 83 | } 84 | assert_response :success 85 | 86 | assert_select 'form#issue-form' do 87 | # Delete the screen input item later 88 | assert_select "input[name=?][value='MCC-0001']", "issue[custom_field_values][#{@default_custom_field.id}]" 89 | assert_select "input[name=?][value='MCC-0001']", "issue[custom_field_values][#{@for_all_custom_field.id}]" 90 | end 91 | 92 | # update 93 | assert_difference 'Journal.count' do 94 | put :update, :params => { 95 | :id => issue.id, 96 | :issue => { 97 | :notes => 'just trying' 98 | } 99 | } 100 | end 101 | assert_added_serial_number(issue.id, 'MCC-0001', @default_custom_field) 102 | assert_added_serial_number(issue.id, 'MCC-0001', @for_all_custom_field) 103 | 104 | # copy 105 | assert_difference 'Issue.count' do 106 | post :create, :params => { 107 | :project_id => 1, 108 | :issue => { 109 | :tracker_id => 1, 110 | :status_id => 2, 111 | :subject => 'This is the test_copy issue', 112 | :custom_field_values => { 113 | @default_custom_field.id => 'MCC-0001' 114 | } 115 | }, 116 | :copy_from => issue.id, 117 | :link_copy => 1 118 | } 119 | end 120 | copied_issue = Issue.find_by_subject('This is the test_copy issue') 121 | assert_redirected_to :controller => 'issues', :action => 'show', :id => copied_issue.id 122 | 123 | assert_not_equal(issue.id, copied_issue.id) 124 | assert_added_serial_number(copied_issue.id, 'MCC-0002', @default_custom_field) 125 | assert_added_serial_number(copied_issue.id, 'MCC-0002', @for_all_custom_field) 126 | end 127 | 128 | def test_show_with_already_created 129 | get :show, :params => { 130 | :id => 1 131 | } 132 | assert_response :success 133 | 134 | assert_select "div.cf_#{@default_custom_field.id}.attribute" do 135 | assert_select 'div.label' do 136 | assert_select 'span', text: @default_custom_field.name 137 | end 138 | assert_select 'div.value', text: '' 139 | end 140 | 141 | assert_select "div.cf_#{@for_all_custom_field.id}.attribute" do 142 | assert_select 'div.label' do 143 | assert_select 'span', text: @for_all_custom_field.name 144 | end 145 | assert_select 'div.value', text: '' 146 | end 147 | 148 | end 149 | 150 | def test_get_edit_with_already_created 151 | get :edit, :params => { 152 | :id => 1 153 | } 154 | assert_response :success 155 | 156 | assert_select 'form#issue-form' do 157 | # Delete the screen input item later 158 | assert_select "input[name=?]", "issue[custom_field_values][#{@default_custom_field.id}]" 159 | assert_select "input[name=?]", "issue[custom_field_values][#{@for_all_custom_field.id}]" 160 | end 161 | 162 | end 163 | 164 | def test_put_update_with_already_created 165 | # Added serial nubmer #1 project_id: 1, tracker_id: 1 166 | assert_difference 'Journal.count' do 167 | put :update, :params => { 168 | :id => 1, 169 | :issue => { 170 | :subject => 'just trying #1' 171 | } 172 | } 173 | end 174 | assert_added_serial_number(1, 'MCC-0001', @default_custom_field) 175 | assert_added_serial_number(1, 'MCC-0001', @for_all_custom_field) 176 | 177 | # Added serial nubmer #3 project_id: 1, tracker_id: 1 178 | assert_difference 'Journal.count' do 179 | put :update, :params => { 180 | :id => 3, 181 | :issue => { 182 | :subject => 'just trying #3' 183 | } 184 | } 185 | end 186 | assert_added_serial_number(3, 'MCC-0002', @default_custom_field) 187 | assert_added_serial_number(3, 'MCC-0002', @for_all_custom_field) 188 | 189 | # Changed tracker(have serial number) #3 project_id: 1, tracker_id: 3 190 | assert_difference 'Journal.count' do 191 | put :update, :params => { 192 | :id => 3, 193 | :issue => { 194 | :tracker_id => 3 195 | } 196 | } 197 | end 198 | assert_added_serial_number(3, 'MCC-0002', @default_custom_field) 199 | assert_added_serial_number(3, 'MCC-0002', @for_all_custom_field) 200 | 201 | # Changed tracker(not have serial number) #3 project_id: 1, tracker_id: 2 202 | assert_difference 'Journal.count' do 203 | put :update, :params => { 204 | :id => 3, 205 | :issue => { 206 | :tracker_id => 2 207 | } 208 | } 209 | end 210 | assert_none_serial_number(3, @default_custom_field) 211 | assert_added_serial_number(3, 'MCC-0002', @for_all_custom_field) 212 | 213 | # Changed project(not have serial number) #1 project_id: 3, tracker_id: 3 214 | assert_difference 'Journal.count' do 215 | put :update, :params => { 216 | :id => 1, 217 | :issue => { 218 | :project_id => 3, 219 | :tracker_id => 3 220 | } 221 | } 222 | end 223 | assert_none_serial_number(1, @default_custom_field) 224 | assert_added_serial_number(1, 'MCC-0001', @for_all_custom_field) 225 | 226 | # Changed project(have serial number) #3 project_id: 1, tracker_id: 3 227 | assert_difference 'Journal.count' do 228 | put :update, :params => { 229 | :id => 3, 230 | :issue => { 231 | :project_id => 1, 232 | :tracker_id => 3 233 | } 234 | } 235 | end 236 | assert_added_serial_number(3, 'MCC-0001', @default_custom_field) 237 | assert_added_serial_number(3, 'MCC-0002', @for_all_custom_field) 238 | 239 | # Changed project(have serial number) #1 project_id: 2, tracker_id: 3 240 | assert_difference 'Journal.count' do 241 | put :update, :params => { 242 | :id => 1, 243 | :issue => { 244 | :project_id => 2, 245 | :tracker_id => 3 246 | } 247 | } 248 | end 249 | assert_added_serial_number(1, 'MCC-0002', @default_custom_field) 250 | assert_added_serial_number(1, 'MCC-0001', @for_all_custom_field) 251 | 252 | # Changed project(have serial number) #2 project_id: 1, tracker_id: 3 253 | assert_difference 'Journal.count' do 254 | put :update, :params => { 255 | :id => 2, 256 | :issue => { 257 | :tracker_id => 3 258 | } 259 | } 260 | end 261 | assert_added_serial_number(2, 'MCC-0003', @default_custom_field) 262 | assert_added_serial_number(2, 'MCC-0003', @for_all_custom_field) 263 | 264 | # Changed project(have serial number) #1 project_id: 1, tracker_id: 3 265 | assert_difference 'Journal.count' do 266 | put :update, :params => { 267 | :id => 1, 268 | :issue => { 269 | :project_id => 1, 270 | :tracker_id => 3 271 | } 272 | } 273 | end 274 | assert_added_serial_number(1, 'MCC-0002', @default_custom_field) 275 | assert_added_serial_number(1, 'MCC-0001', @for_all_custom_field) 276 | 277 | 278 | # Changed project(have serial number) #4 project_id: 2, tracker_id: 2 279 | assert_difference 'Journal.count' do 280 | put :update, :params => { 281 | :id => 4, 282 | :issue => { 283 | :project_id => 2, 284 | :tracker_id => 2 285 | } 286 | } 287 | end 288 | assert_none_serial_number(4, @default_custom_field) 289 | assert_added_serial_number(4, 'MCC-0004', @for_all_custom_field) 290 | 291 | # Changed project(not have serial number) #2 project_id: 3, tracker_id: 3 292 | assert_difference 'Journal.count' do 293 | put :update, :params => { 294 | :id => 2, 295 | :issue => { 296 | :project_id => 3, 297 | :tracker_id => 3 298 | } 299 | } 300 | end 301 | assert_none_serial_number(2, @default_custom_field) 302 | assert_added_serial_number(2, 'MCC-0003', @for_all_custom_field) 303 | 304 | # Changed project(have serial number) #2 project_id: 1, tracker_id: 3 305 | assert_difference 'Journal.count' do 306 | put :update, :params => { 307 | :id => 2, 308 | :issue => { 309 | :project_id => 1, 310 | :tracker_id => 3 311 | } 312 | } 313 | end 314 | assert_added_serial_number(2, 'MCC-0003', @default_custom_field) 315 | assert_added_serial_number(2, 'MCC-0003', @for_all_custom_field) 316 | end 317 | 318 | def test_get_bulk_edit 319 | get :bulk_edit, :params => { 320 | :ids => [1, 3] 321 | } 322 | assert_response :success 323 | 324 | assert_select 'form#bulk_edit_form' do 325 | # Delete the screen input item later 326 | assert_select "input[name=?][value='__none__']", "issue[custom_field_values][#{@default_custom_field.id}]" 327 | assert_select "input[name=?][value='__none__']", "issue[custom_field_values][#{@for_all_custom_field.id}]" 328 | end 329 | 330 | end 331 | 332 | def test_bulk_update 333 | issue_ids = [1, 2, 4, 5, 7, 8] 334 | expected_default_serial_numbers = [ 335 | 'MCC-0001', nil, 'MCC-0002', nil, 'MCC-0003', 'MCC-0004' 336 | ] 337 | expected_for_all_serial_numbers = [ 338 | 'MCC-0001', 'MCC-0002', 'MCC-0003','MCC-0004', 'MCC-0005', 'MCC-0006' 339 | ] 340 | 341 | post :bulk_update, :params => { 342 | :ids => issue_ids.shuffle, 343 | :notes => "Bulk editing" 344 | } 345 | 346 | issue_ids.each_with_index do |issue_id, i| 347 | issue = Issue.find(issue_id) 348 | journal = issue.journals.reorder('created_on DESC').first 349 | assert_equal "Bulk editing", journal.notes 350 | 351 | expected_value = expected_default_serial_numbers[i] 352 | if expected_value.nil? 353 | assert_none_serial_number(issue_id, @default_custom_field) 354 | else 355 | assert_added_serial_number(issue_id, expected_value, @default_custom_field) 356 | end 357 | assert_added_serial_number(issue_id, expected_for_all_serial_numbers[i], @for_all_custom_field) 358 | end 359 | 360 | end 361 | 362 | end 363 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | 4 | def create_default_serial_number_field 5 | custom_field = IssueCustomField.create!({ 6 | :field_format => "serial_number", 7 | :name => 'default_sn', 8 | :regexp => 'MCC-{0000}', 9 | :is_required =>"0", 10 | :is_filter => "1", 11 | :searchable => "1", 12 | :visible => "1", 13 | :is_for_all => "0", 14 | :role_ids => [] 15 | }) 16 | 17 | # eCookbook 18 | Project.find(1).issue_custom_fields << custom_field 19 | # OnlineStore 20 | Project.find(2).issue_custom_fields << custom_field 21 | # Bug 22 | Tracker.find(1).custom_fields << custom_field 23 | # Support request 24 | Tracker.find(3).custom_fields << custom_field 25 | 26 | return custom_field 27 | end 28 | 29 | def create_for_all_serial_number_field 30 | custom_field = IssueCustomField.create!({ 31 | :field_format => "serial_number", 32 | :name => 'for_all_sn', 33 | :regexp => 'MCC-{0000}', 34 | :is_required =>"0", 35 | :is_filter => "1", 36 | :searchable => "1", 37 | :visible => "1", 38 | :is_for_all => "1", 39 | :role_ids => [] 40 | }) 41 | Tracker.all.each { |tracker| tracker.custom_fields << custom_field } 42 | 43 | return custom_field 44 | end 45 | 46 | def valid_regexp_values 47 | [ 48 | '{yyyy}-{0000}', '{yy}-{0000}', 49 | '{YYYY}-{0000}', '{YY}-{0000}', 50 | '{ISO}-{000}', 51 | '{0000}', '#{yyyy}-{00000}', 52 | 'OCG-{yy}-{00000}', '日本語{YYYY}-{00000}', 53 | ' {YY}-{00000}', '!{00000}' 54 | ] 55 | end 56 | 57 | def invalid_regexp_values 58 | [ 59 | '', ' ', ' ', 'ABC-{000}-{yy}', 60 | '{abc}-{yy}-{000}', '{yy}-{00000}-OCG', 61 | '{YYYY}-{00000}日本語', 'hogehoge' 62 | ] 63 | end 64 | 65 | def assert_added_serial_number(issue_id, expected_value, custom_field) 66 | issue = Issue.find(issue_id) 67 | custom_value = issue.custom_values.where( 68 | :custom_field_id => custom_field.id).first 69 | 70 | assert_not_nil custom_value 71 | assert_equal expected_value, custom_value.value 72 | assert_include custom_field, issue.available_custom_fields 73 | end 74 | 75 | def assert_none_serial_number(issue_id, custom_field) 76 | issue = Issue.find(issue_id) 77 | custom_value = issue.custom_values.where( 78 | :custom_field_id => custom_field.id).first 79 | 80 | assert_nil custom_value 81 | assert_not_include custom_field, issue.available_custom_fields 82 | end 83 | -------------------------------------------------------------------------------- /test/unit/issue_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class IssueTest < ActiveSupport::TestCase 4 | fixtures :projects, 5 | :users, :email_addresses, :user_preferences, 6 | :roles, :members, :member_roles, 7 | :issues, :issue_statuses, :issue_relations, 8 | :versions, :trackers, :projects_trackers, 9 | :issue_categories, :enabled_modules, 10 | :enumerations, :attachments, :workflows, 11 | :custom_fields, :custom_values, 12 | :custom_fields_projects, :custom_fields_trackers, 13 | :time_entries, :journals, :journal_details, 14 | :queries, :repositories, :changesets 15 | 16 | include Redmine::I18n 17 | 18 | def setup 19 | set_language_if_valid 'en' 20 | @default_custom_field = create_default_serial_number_field 21 | end 22 | 23 | def teardown 24 | User.current = nil 25 | end 26 | 27 | def test_create_with_changed_serial_number_halfway 28 | issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create_1') 29 | assert issue.save 30 | assert_added_serial_number(issue.id, 'MCC-0001', @default_custom_field) 31 | 32 | # changed regexp(forced) 33 | @default_custom_field.update_attributes(regexp: 'ABC-{0001}') 34 | 35 | issue = Issue.new(:project_id => 1, :tracker_id => 3, :author_id => 3, :subject => 'test_create_2') 36 | assert issue.save 37 | assert_added_serial_number(issue.id, 'MCC-0002', @default_custom_field) 38 | end 39 | 40 | end 41 | --------------------------------------------------------------------------------