ラベル 管理 の投稿を表示しています。 すべての投稿を表示
ラベル 管理 の投稿を表示しています。 すべての投稿を表示

新規ユーザー登録時のメールにカスタムメッセージを追加する

新規ユーザー登録時に、対象ユーザーにメールが送信されるが、その内容は最低限の情報となる。
システムの運用や初期設定、利用ガイドのロケーション等々、ユーザーの初期登録時に伝えるべき情報があるが、別で知らせるのも面倒。
そこで、メール送信されるメッセージを予めシステムに登録しておける仕組みを実装してみた。

<機能>

  • 新規ユーザー登録時に、予め登録しておいたメッセージが追加されてメール送信される。

    <設定/準備>

    • データベースのmigrateは不要。(settingsテーブルに1レコードとして保持されるようにした。)
    • 管理 - 設定 - メール通知 の一番下に追加されている設定項目 "Email content on user registration" に、追加するメッセージを記述する。

    <コード変更箇所>

    ロールの利用を制限する

    ユーザーとプロジェクトのアクセスレベルによってプロジェクトへのアクセス制限機能を拡張する」の記事で、ユーザーとプロジェクトにアクセスレベルを設けて、アクセスを制御する機能を紹介した。
    今回は、ロールにもアクセスレベルを設けて、
    1. ユーザーが使えるロール(プロジェクト設定のメンバーでアサインできるロール)
    2. プロジェクトで有効な(利用できる)ロール
    を設定できる機能を追加してみた。 なお、対象のRedmineは、2.5.1。 また、「ユーザーとプロジェクトのアクセスレベルによってプロジェクトへのアクセス制限機能を拡張する」が適用されている(ユーザーのアクセスレベル属性が追加されている)ことが前提。

    <機能>

    1. ユーザーが使えるロールを設定する機能
      ユーザーへプロジェクト設定「メンバーの管理」権限を与える運用で、そのユーザーに利用させたくないロール(そのユーザーよりも強力なロール)がある場合に、ユーザーが利用できるロールをユーザーのアクセスレベルに応じて制限できるようにする機能。
      ユーザー個別に設定したアクセスレベルとロール個別に設定したアクセスレベルを比較して、ユーザーレベル未満のロールレベルのロールだけを使えるようにする仕組み。
      もちろん高レベルのロールを作らなければ良いのだが、それができない状況で「メンバーの管理」権限を一般ユーザーに任せる場合に有用となる。
    2. プロジェクトで有効なロールを設定する機能
      プロジェクトによって利用させたい/利用させたくないロールが別れる場合に、上記1だけでは対応できないため、プロジェクト毎に利用するロールを決めてしまおうという機能。
      プロジェクト毎に使えるロールを決めてしまうので、単純にプロジェクトで使わないロールを予め外すていおくことで、誤ったロールの使い方も防グことができる。

    <Redmineでの設定>

    1. ユーザーが使えるロールを設定する
      ・ユーザーのアクセスレベルを設定する
      管理→ユーザー→ログイン でユーザー属性編集画面を開き、「アクセスレベル」を0~9で設定する。
      利用を制限したいユーザーを低めに設定する。
      ・ロールのアクセスレベルを設定する
      管理→ロールと権限→ロール でロール属性編集画面を開、「アクセスレベル」を0~9で設定する。
      利用を制限したいロールを高めに設定する。

      ログインユーザーのアクセスレベルより高いアクセスレベルのロールは、どんなに頑張っても、メンバー設定で割り当てることができなくなる。
    2. プロジェクトで有効なロールを設定する
      ・管理→ロールと権限 で、「ロールの選択」権限を、プロジェクトで有効なロールを設定することができるロールに付与する。
      (もちろん、メンバーの管理を移管するユーザーに割り当てるロールには、「ロールの選択」権限は付与しない。)
      ・管理→設定→「新規プロジェクトにおいてデフォルトで有効になるロール」 で新規プロジェクト作成時のデフォルト値を設定する。
      ・プロジェクトメニューの「設定」の「ロール」で、そのプロジェクトで利用するロールのチェックをONにする。

      設定したプロジェクトのメンバー設定画面では、特定のロールしか利用できなくなる。

    <差分コード>

    1. ユーザーが使えるロールを設定する機能
      Github
      (DBのmigrateが必要。)
    2. プロジェクトで有効なロールを設定する機能
      Github

    チケットのカスタムフィールドのステータス別必須入力チェック v.1.4.4版

    redmine-2.1.0がリリースされ、新機能の一つとして、「ステータス別のチケット項目の必須/リードオンリー設定」がある。
    似たような機能(「チケットのカスタムフィールドをステータス別に必須入力チェックする」)について、以前、redmine-1.3.0への適用として紹介した。
    redmine-1.4系では実装される予定は無いと思うので、ここで改めて紹介しておく。

    redmine-2.1.0の機能との違いは次の通り。
    • Redmineシステムとしてデフォルトの設定を行い、プロジェクト毎にカスタマイズする使い方を想定。
    • プロジェクト毎に、必須項目を設定できる。
    • カスタムフィールドに対してしか設定できない。
    • リードオンリーの設定はない。
    • ロール別の設定はない。

    <Redmineでの設定>

    • データベースのmigrateが必要。(rake db:migrate RAILS_ENV="production" を実行)
    • Redmineシステムとしてのデフォルト設定を行う。
      「管理」→「カスタムフィールド」
      各カスタムフィールドの編集画面の、「必須」の項目で、そのカスタムフィールドについてデフォルトで必須にするステータスにチェックを入れる。
      「すべて」をチェックすると全てのステータスで必須となり、全プロジェクトで必須となる(従来と同じ挙動)。
      「すべて」のチェックを外すと、プロジェクト毎に必須項目を設定できるようになる。
    • プロジェクトメニューの「設定」→「情報」の「必須」で、
      ステータス別に必須にするカスタムフィールドにチェックを入れる。

    <Redmine-1.4.4からのコード修正内容>


    プロジェクトの終了ステータス追加・リードオンリー化

    Redmineのプロジェクトのステータスは有効・アーカイブ(見られなくする)、それと削除しかない。
    Redmineをある程度の規模で、長期的に使っていると、終了プロジェクトの扱いに苦慮することになるでしょう。
    プロジェクトが終了しても、データを振り返るケースは多いので、アーカイブの選択肢はほとんどないだろうし、削除はなおさらだ。
    かといって、全て有効にしていると、見かけ有効なプロジェクトがどんどん増えていくし。
    やはり、終了ステータス、もしくはリードオンリーのようなステータスがほしくなる。

    そこで、次の機能を追加してみた。

    • プロジェクトのステータスに 終了を追加する。
    • プロジェクト一覧に表示するステータスを選択可能にする(表示一覧の改善・抽出含む)
    • プロジェクトセレクタに終了プロジェクトは表示させない(終了プロジェクトにアクセスするのはトップメニューのプロジェクトから)
    • プロジェクト横断表示時のチケット一覧には、終了プロジェクトのチケットは表示しない
    • プロジェクト横断表示時のチケット一覧フィルタで、プロジェクトに終了プロジェクトを表示しない
    • チケット更新時のプロジェクトフィールドの選択肢に終了プロジェクトを表示しない
    • 終了プロジェクトはリポジトリログを取得しない
    • 終了プロジェクトでログイン者から除去する権限を設定可能にする。(Redmine利用者はログインが必須で運用する前程)

    なお、Redmineのチケットにある通り、redmine-2.1.0 で、ようやくプロジェクトの終了ステータスの機能が入るらしい。
    終了状態のプロジェクトで有効にする権限は、ソースコード(lib/redmine.rb)で書かれており、システムで固定のようだ。
    1.4系には適用されるのだろうか?

    <Redmineでの設定>

    • 終了プロジェクトでログイン者から除去する権限を設定
      「管理」→「設定」→「プロジェクト」→「ステータスが終結のプロジェクトで取り除く権限」で、終了プロジェクトで取り除く権限にチェックを入れる。(デフォルトでは主に更新系のものにチェックが入っているが、必要に応じて設定する。)

    <注意点>

    • Anonymousの運用は考慮していない。当方のRedmineはログインが必須になっている運用。(「管理」→「設定」→「認証」→「認証が必要」にチェック)
    • 「取り除く権限」は終了にした全プロジェクト共通。

    <Redmine-1.4.4からのコード修正内容>



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

    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