チケットのカスタムフィールドをステータス別に必須入力チェックする

Redmineでは、チケットの属性として任意の入力項目を「カスタムフィールド」として追加することができる。選択リスト形式や文字列、数字など、様々なフォーマットを指定したり必須にするかどうかなども設定でき、また、どのトラッカーで利用するのかも設定できる。かなりなことは、設定できるが、今一つ足りないのが、ステータス別に必須にするかどうかを設定できないことだ。

例えば、(企業組織ではPDCAを回すために、)バグ管理票なるもので、そのバグの原因や修正区分、そのバグの混入工程や発見工程なんかの属性を管理することがある。これをチケットで管理する場合、カスタムフィールドでこれらの属性を定義することになる。そこで問題となるのが、Redmineのカスタムフィールドで「必須」にするとどのステータスにおいても必須になってしまう仕様だ。

Redmineの場合、必須チェックはステータスと連動させることができない。例えば前述の「原因」項目の場合、チケット登録時(バグ発見時)には不明な場合がほとんどで選択できない。ただ、チケット入力時には不要な情報でも、クローズする時には必須にしたい項目でもある。

このような、プロジェクト管理要素の強い属性は、チケットを処理している時には、(エンジニアにとってあまり重要に感じられないこともあり、)必須にしないと選択し忘れたり、後で入れようと思ったりされてしまう。ただ、数千ものバグを抱えるようなプロジェクトでは、前述の属性を後でまとめて入力するのは不可能に近い。やはりこれらは遅くともチケットクローズ時に確実に付加させてておくべき情報だ。

そのためには、ステータス別にカスタムフィールドの入力チェックをでききるようにしなければならない。以下がその改造。

<Redmineでの設定>
  • データベースマイグレートが必要。(rake db:migrate RAILS_ENV="production")
  • 管理→カスタムフィールで、チケットのカスタムフィールドに対し、デフォルト(プロジェクト登録時)で必須にするかどうかを設定する。プロジェクト登録時に必須/任意の設定は可能。
  • プロジェクトメニューの設定→概要 で、ステータス別に必須にするカスタムフィールドにチェックを入れることで、必須設定する。

<Redmineソースの改造>(v.1.3.0ベース)
  • 管理→カスタムフィールド の設定画面に、デフォルトで必須にするステータスを選択するチェックボックスを表示する。
  • プロジェクトメニューの設定→概要 の画面に、チケットのカスタムフィールド毎に、ステータス別に必須/任意設定するチェックボックスを表示する。
  • チケット登録・更新時に、ステータスに応じて必須設定されたカスタムフィールドの入力チェックを行う。
  • custom_fieldsテーブルに、デフォルトで入力必須にするかどうかを管理するカラムを追加。
  • プロジェクト別にステータス別必須カスタムフィールドを管理する新規テーブルを作成。
   end

   def create
+    return unless validate_required_fields(@issue.status.id)
     call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
     if @issue.save
       attachments = Attachment.attach_files(@issue, params[:attachments])
       call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
:
:
   def update
     update_issue_from_params
+    return unless validate_required_fields(params[:issue][:status_id].to_i)
     if @issue.save_issue_with_child_records(params, @time_entry)
       render_attachment_warning_if_needed(@issue)
       flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?


     attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
     attributes
   end
:
:
+  def validate_required_fields(issue_status)
+    if params.nil? || params.empty? || ((params.include? 'issue') == false) || ((params[:issue].include? 'custom_field_values') == false)
+      return true
+    end
+    required_custom_fields = get_required_custom_field
+    issue_custom_fields = IssueCustomField.find(:all)
+
+    #check issue_status.to_s if this is required_custom_fields's Hash key or not.
+    tmpkeys = required_custom_fields.keys
+    tmpflg = 0
+    for i in 0 .. tmpkeys.length-1
+      if tmpkeys[i] == issue_status.to_s
+        tmpflg = 1
+        break
+      end
+    end
+    if tmpflg == 0
+      # issue_status.to_s is not Hash key
+      return true
+    end
+
+    if required_custom_fields[issue_status.to_s].size == 0 || issue_custom_fields == nil
+      return true
+    end
+    required_custom_fields[issue_status.to_s].each do |required|
+      issue_custom_fields.each do |issue_custom_field|
+        if required == issue_custom_field.name
+          unless (params[:issue][:custom_field_values].include? issue_custom_field.id.to_s)
+            break
+          end
+          if params[:issue][:custom_field_values][issue_custom_field.id.to_s] == ""
+            tmp = t 'activerecord.errors.messages.blank'
+            flash.now[:error] = required + " " + tmp
+            @priorities = IssuePriority.all
+            if @action_name == "update"
+              render :template => 'issues/edit', :layout => !request.xhr?
+            elsif @action_name == "create"
+              render :template => 'issues/new', :layout => !request.xhr?
+            end
+            return false
+          end
+        end
+      end
+    end
+    return true
+  end

 end
     @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
     @trackers = Tracker.all
     @project = Project.new(params[:project])
+    @required_custom_field = get_required_custom_field
   end

   verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
:
:
                                   :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
                                   :order => 'name')
     @source_project = Project.find(params[:id])
+    @issue_status = IssueStatus.find( :all ,:order => 'position')
+    @required_custom_field = get_required_custom_field
     if request.get?
       @project = Project.copy_from(@source_project)
       if @project
:
:
         @project.safe_attributes = params[:project]
         if validate_parent_id && @project.copy(@source_project, :only => params[:only])
           @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
+          save_required_custom_field
           flash[:notice] = l(:notice_successful_create)
           redirect_to :controller => 'projects', :action => 'settings', :id => @project
         elsif !@project.new_record?
:
:
     @trackers = Tracker.all
     @repository ||= @project.repository
     @wiki ||= @project.wiki
+    @required_custom_field = get_required_custom_field
   end

   def edit
:
:
       @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
       respond_to do |format|
         format.html {
+          save_required_custom_field
           flash[:notice] = l(:notice_successful_update)
           redirect_to :action => 'settings', :id => @project
         }
:
:
     end
     true
   end
+
+  def save_required_custom_field
+    hash_others = Hash.new
+    tmp_array = []
+    hash_others = {}
+    @issue_status = IssueStatus.find( :all ,:order => 'position')
+    for tmp_issue_status in @issue_status
+      tmp_array = []
+      if params[:required_custom_field] != nil
+        if params[:required_custom_field].include? tmp_issue_status.id.to_s
+          tmp_array = params[:required_custom_field][tmp_issue_status.id.to_s].keys
+        end
+      end
+      hash_others[tmp_issue_status.id.to_s] = tmp_array
+    end
+
+    required_custom_field = RequiredCustomField.find(:first, :conditions => ["project_id = ?", @project.id])
+    if required_custom_field == nil
+      required_custom_field = RequiredCustomField.new(:project_id => @project.id,
+                                                      :others => hash_others)
+    else
+      required_custom_field.others = hash_others
+    end
+    if required_custom_field.save == false
+      required_custom_field.connection.rollback_db_transaction
+    end
+  end
+
 end
     sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
     l("label_version_sharing_#{sharing}")
   end
+
+  def required_custom_field_checkbox_tag(project, custom_field)
+    s = ''
+    @issue_status.each do |status|
+      custom_field.is_required ? checked = true : checked = false
+      unless @required_custom_field[status.id.to_s] == nil
+        @required_custom_field[status.id.to_s].each do |cf|
+          checked = true if cf == custom_field.name
+        end
+      end
+      s << "\n"
+      s << check_box_tag('required_custom_field[' + status.id.to_s + '][' + custom_field.name + ']',
+                         custom_field.id,
+                         checked,
+                         :disabled => custom_field.is_required?) + "\n\n"
+    end
+    s
+  end
+
+  def get_required_custom_field
+    required_custom_field = RequiredCustomField.find(:first, :conditions => ["project_id = ?", @project.id])
+    @issue_status = IssueStatus.find( :all ,:order => 'position')
+    required_custom_field_other = Hash.new
+    if required_custom_field.nil?
+      # Registering new project, set default required custom feilds.
+      @issue_status.each do |status|
+        cfnames = []
+        @issue_custom_fields.each do |cf|
+            cfnames << cf.name if !cf.required_issue_status_ids.nil? && YAML.load(cf.required_issue_status_ids).include?(status.id.to_s)
+          end
+        required_custom_field_other[status.id.to_s] = cfnames
+      end
+    else
+      # On existing project's setting
+      @issue_status.each do |status|
+        required_custom_field_other[status.id.to_s] = required_custom_field[status.id.to_s]
+      end
+    end
+    required_custom_field_other
+  end
 end
+class RequiredCustomField < ActiveRecord::Base
+  serialize :others
+  def [](attr_name)
+    if attribute_present? attr_name
+      super
+    else
+      others ? others[attr_name] : nil
+    end
+  end
+end
     <% end %>
     <%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
     </fieldset>
     &nbsp;
-    <p><%= f.check_box :is_required %></p>
+    <p><%= f.check_box :is_required %>
+    <%= l(:label_all) %>: <i><%= l(:text_required_all) %></i><br />
+    <%=l(:label_applied_status)%>: <i><%= l(:text_required_specific) %></i><br />
+    <% for status in IssueStatus.find(:all ,:order => 'position') -%>
+        <%= check_box_tag "custom_field[required_issue_status_ids][]",
+            status.id.to_s,
+            (YAML.load(@custom_field.required_issue_status_ids).include?(status.id.to_s) unless @custom_field.required_issue_status_ids.nil?),
+            :onchange => 'if (this.checked){$("custom_field_is_required").checked = false;}'
+        %>
+        <%= h(status.name) %><br />
+    <% end -%>
+    </p>
+    &nbsp;
     <p><%= f.check_box :is_for_all %></p>
     <p><%= f.check_box :is_filter %></p>
     <p><%= f.check_box :searchable %></p>
 </fieldset>
 <% end %>
 <% end %>
+
+<fieldset class="box"><legend><%=l(:field_is_required)%></legend>
+  <table class="list">
+    <thead>
+      <tr>
+        <th><%=l(:label_custom_field)%></th>
+        <% for tmp_issue_status in @issue_status %>
+        <th><%=tmp_issue_status%></th>
+        <% end %>
+      </tr>
+    </thead>
+    <tbody>
+      <% @issue_custom_fields.each do |custom_field| %>
+        <tr class="<%= cycle("odd", "even") %>">
+          <td class="name" align="left"><%= custom_field.name %></td>
+          <%= required_custom_field_checkbox_tag(@project, custom_field) %>
+        </tr>
+      <% end %>
+    </tbody>
+  </table>
+</fieldset>
+
 <!--[eoform:project]-->
   text_scm_command_version: Version
   text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
   text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
+  text_required_all: "Will be required field in all satatuses, and be unchangeable from requierd field in the project setting."
+  text_required_specific: "Will be default required field in checked statuses, and be changeable for requierd or not in the project setting."

   default_role_manager: Manager
   default_role_developer: Developer
   text_scm_command_version: バージョン
   text_scm_config: バージョン管理システムのコマンドをconfig/configuration.ymlで設定できます。設定後、Redmineを再起動してください。
   text_scm_command_not_available: バージョン管理システムのコマンドが利用できません。管理画面にて設定を確認してください。
+  text_required_all: "すべてのステータスで必須項目とし、プロジェクトの設定では変更できないようになります。"
+  text_required_specific: "チェックしたステータスをデフォルトの必須項目とし、プロジェクトの設定で変更できるようになります。"

   default_role_manager: 管理者
   default_role_developer: 開発者
+class AddCustomFieldsRequiredIssueStatusIds < ActiveRecord::Migration
+  def self.up
+    add_column :custom_fields, :required_issue_status_ids, :text
+  end
+
+  def self.down
+    remove_column :custom_fields, :required_issue_status_ids
+  end
+end
+class CreateRequiredCustomFields < ActiveRecord::Migration
+  def self.up 
+    create_table :required_custom_fields do |t|
+      t.column "project_id", :integer, :default => 0, :null => false
+      t.column "others", :text
+    end
+    add_index :required_custom_fields, :project_id
+  end
+
+  def self.down
+    drop_table :required_custom_fields
+  end
+end

4 件のコメント:

  1. 匿名3/11/2013

    通りすがりのものですが
    Redmine 2.2ではカスタムクエリー以外に、「管理⇒ワークフロー」の「フィールドに対する権限」タブでステータスごとの入力制御が行えます。
    ※それ以外のバージョンは使っていないのでわかりません。

    返信削除
    返信
    1. コメントありがとうございます。
      ステータス別入力制御はv.2.1から実装された機能ですが、プロジェクト別に設定できません。
      私の所では数百プロジェクトで運用しており、プロジェクト別に設定できることが最大要件となっております。
      これ以外にもいろいろ事情があって、v2系に移行できなくなっており、v1.4系を使い続けることになると思います。

      V.1.4系での当機能の実装については次の記事になります。
      http://redminist.blogspot.jp/2012/09/v144.html

      削除
  2. 匿名3/26/2013

    ちょっと検索して通りがかったものですm(_ _)m
    Redmine2.3でも同じようなカスタマイズは可能ですか。
    カスタムフィールドの配置に苦戦です・・・

    返信削除
  3. コメントありがとうございます。
    本家Redmine2.3(2.2から)は類似の機能が実装されていますが、実装のアプローチがこれとは全く違っています。
    なので、難しいのではないでしょうか。。。
    http://www.redmine.org/issues/3521
    本家はワークフローに追加定義していますが、当方は別途プロジェクトIDからなる管理テーブルを起こしています。

    返信削除