Redmineプロジェクトで設定の複雑な階層を維持する方法

過去2か月間、彼はCentos-admin.ruのredmine_intouchプラグインに取り組みました



作業を完了した後、開発プロセス中に直面しなければならなかったニュアンスの一部を共有することにしました。



この出版物では、さまざまな設定の柔軟なシステムを実装するために渡す必要があったパスについて説明します。



まず、予約をしたいです。 この記事では、プロジェクト設定をRedmineのプラグインに保存するロジックの実装について説明します。



なぜなら プラグインです。Redmine自体のロジックとの競合を避けるために、この機能が実装されているサードパーティのgemを使用することは非常に望ましくありません。



したがって、この出版物は、複雑な階層を持つ設定ストレージシステムのゼロからの実装に焦点を当てます。



画像



スクリーンショットからわかるように、3つのスポイラーからのデータを何らかの方法で保存する必要があります。各スポイラーには複数のタブがあり、各タブには多くのチェックボックスがあります。



これをすべて保存する方法は?



最初に、他のプラグインで同様の機能がどのように実装されているかを確認することにしました。



会社で使用されているプラ​​グインのソースコードを確認した後、 redmine_contactsで同様の機能を見つけました。 プロジェクトを参照して特定の設定を保存できるContactsSettingモデルがあります。



その結果、次のモデルがプラグインに登場しました。



intouch_setting.rb
class IntouchSetting < ActiveRecord::Base unloadable belongs_to :project attr_accessible :name, :value, :project_id cattr_accessor :available_settings self.available_settings ||= {} def self.load_available_settings %w(alarm new working feedback overdue).each do |notice| %w(author assigned_to watchers).each do |receiver| define_setting "telegram_#{notice}_#{receiver}" end define_setting "telegram_#{notice}_telegram_groups", serialized: true, default: {} define_setting "telegram_#{notice}_user_groups", serialized: true, default: {} end define_setting 'email_cc', default: '' end def self.define_setting(name, options={}) available_settings[name.to_s] = options end # Hash used to cache setting values @intouch_cached_settings = {} @intouch_cached_cleared_on = Time.now # Hash used to cache setting values @cached_settings = {} @cached_cleared_on = Time.now validates_uniqueness_of :name, scope: [:project_id] def value v = read_attribute(:value) # Unserialize serialized settings if available_settings[name][:serialized] && v.is_a?(String) v = YAML::load(v) v = force_utf8_strings(v) end # v = v.to_sym if available_settings[name]['format'] == 'symbol' && !v.blank? v end def value=(v) v = v.to_yaml if v && available_settings[name] && available_settings[name][:serialized] write_attribute(:value, v.to_s) end # Returns the value of the setting named name def self.[](name, project_id) project_id = project_id.id if project_id.is_a?(Project) v = @intouch_cached_settings[hk(name, project_id)] v ? v : (@intouch_cached_settings[hk(name, project_id)] = find_or_default(name, project_id).value) end def self.[]=(name, project_id, v) project_id = project_id.id if project_id.is_a?(Project) setting = find_or_default(name, project_id) setting.value = (v ? v : "") @intouch_cached_settings[hk(name, project_id)] = nil setting.save setting.value end # Checks if settings have changed since the values were read # and clears the cache hash if it's the case # Called once per request def self.check_cache settings_updated_on = IntouchSetting.maximum(:updated_on) if settings_updated_on && @intouch_cached_cleared_on <= settings_updated_on clear_cache end end # Clears the settings cache def self.clear_cache @intouch_cached_settings.clear @intouch_cached_cleared_on = Time.now logger.info "Intouch settings cache cleared." if logger end load_available_settings private def self.hk(name, project_id) "#{name}-#{project_id.to_s}" end def self.find_or_default(name, project_id) name = name.to_s raise "There's no setting named #{name}" unless available_settings.has_key?(name) setting = find_by_name_and_project_id(name, project_id) unless setting setting = new(name: name, project_id: project_id) setting.value = available_settings[name][:default] end setting end def force_utf8_strings(arg) if arg.is_a?(String) arg.dup.force_encoding('UTF-8') elsif arg.is_a?(Array) arg.map do |a| force_utf8_strings(a) end elsif arg.is_a?(Hash) arg = arg.dup arg.each do |k,v| arg[k] = force_utf8_strings(v) end arg else arg end end end
      
      





この機能は機能しましたが、新しい設定を追加する柔軟性が失われました。 とにかく、一目でそのようなコードを理解するのはそれほど簡単ではありません。



代替手段は何ですか?



上記の機能の実装中に、そのような設定が最も便利にハッシュに保存されるという考えは私を去りませんでした。 しかし、最近まで、私はRedmineテーブルに変更を加えないようにしました。 この場合、 プロジェクトテーブルにテキストフィールドを1つだけ追加する必要がありました。



しかし、すべてに制限があります。 そして、より便利にプラグインを開発し続けたいという願望は、それを上回りました。



プロジェクトテーブルにintouch_settingsフィールドを追加しました。 彼は、 設定フィールドが他のプラグインのプロジェクトに追加される場合に備えて、プラグインの名前からプレフィックスを付けた名前を取りました。



そして、アメニティが始まりました。 プロジェクトのパッチに追加する必要がありました



 store :intouch_settings, accessors: %w(telegram_settings email_settings)
      
      





その後、 アクセサでさらに3つのフィールドが追加されました。 便利でわかりやすい!



そして、プラグインに設定テンプレートを追加する必要があったとき、この保存方法は非常に成功したことが判明しました!



この多様性をどのように形作るのでしょうか?



Railsで利用できるtryメソッドが役立ちます。



たとえば、記事の冒頭のスクリーンショットに表示されているテーブルを生成するコードフラグメントを示します。



 <% IssueStatus.order(:position).each do |status| %> <tr> <th> <%= status.name %> </th> <% IssuePriority.order(:position).each do |priority| %> <td> <% Intouch.active_protocols.each do |protocol| %> <%= check_box_tag "intouch_settings[#{protocol}_settings][author][#{status.id}][]", priority.id, @project.send("#{protocol}_settings").try(:[], 'author'). try(:[], status.id.to_s).try(:include?, priority.id.to_s) %> <%= label_tag l "intouch.protocols.#{protocol}" %><br> <% end %> </td> <% end %> </tr> <% end %>
      
      





プラグインの作業が完了すると、同様の構造がintouch_settingsフィールドに保存され始めました:

intouch_settings
 {"settings_template_id"=>"2", "telegram_settings"=> {"author"=>{"1"=>["2", "5"], "2"=>["2", "5"], "3"=>["5"], "5"=>["1", "2", "3", "4", "5"]}, "assigned_to"=> {"1"=>["1", "2", "3", "4", "5"], "2"=>["1", "2", "3", "4", "5"], "3"=>["1", "2", "3", "4", "5"], "4"=>["1", "2", "3", "4", "5"], "5"=>["1", "2", "3", "4", "5"], "6"=>["1", "2", "3", "4", "5"]}, "watchers"=>{"1"=>["5"], "2"=>["5"], "3"=>["5"], "5"=>["1", "2", "3", "4", "5"]}, "groups"=> {"1"=>{"1"=>["2", "5"], "2"=>["2", "5"], "3"=>["5"], "5"=>["1", "2", "3", "4", "5"]}, "2"=> {"1"=>["1", "2", "3", "4", "5"], "2"=>["1", "2", "3", "4", "5"], "3"=>["1", "2", "3", "4", "5"], "4"=>["1", "2", "3", "4", "5"], "5"=>["1", "2", "3", "4", "5"], "6"=>["1", "2", "3", "4", "5"]}}, "working"=>{"author"=>"1", "assigned_to"=>"1", "watchers"=>"1", "groups"=>["1"]}, "feedback"=>{"author"=>"1", "assigned_to"=>"1", "watchers"=>"1", "groups"=>["1"]}, "unassigned"=>{"author"=>"1", "watchers"=>"1", "groups"=>["1"]}, "overdue"=>{"author"=>"1", "assigned_to"=>"1", "watchers"=>"1", "groups"=>["1", "2"]}}, "reminder_settings"=> {"1"=>{"active"=>"1", "interval"=>"1"}, "2"=>{"active"=>"1", "interval"=>"1"}, "3"=>{"active"=>"1", "interval"=>"1"}, "4"=>{"active"=>"1", "interval"=>"1"}, "5"=>{"active"=>"1", "interval"=>"1"}}, "email_settings"=> {"unassigned"=>{"user_groups"=>["5", "9"]}, "overdue"=>{"assigned_to"=>"1", "watchers"=>"1", "user_groups"=>["5", "9"]}}, "assigner_groups"=>["5", "9"]}
      
      





そして結論として



設定システムの実装では、他の何かを書くことも可能ですが、私は、出版物で言われたことで十分だと思います。 ソースコードを確認することを特にお勧めします。 プラグインコードはGitHubリポジトリにあります



All Articles