あらためて感心したRedmineのデータ利用方法(UserPrefernce 編)


原田です。当社では福利厚生の一つとして「チケットレストラン」を導入し社員に提供しています。私も昼食購入時に利用していますが、いつも同じものを購入するので健康面から見るとよろしくないと思っています。たまにはお腹を満たすだけでなくおいしいものを食べたいですね。

Redmineはさまざまな情報を管理しています

当社のサービス紹介やブログ等をお読みの方の多くはRedmineに興味を持たれ、ご利用・ご検討いただいていると思います。 Redmineの改善作業を行っていますと実際に入力した情報(データ)をどのように取り扱っているのか非常に興味を惹かれます。 今回は少し異質(特徴的)なデータの持ち方をしている UserPreference についてご紹介します。

sqlite> .header on
sqlite> .mode column
sqlite> pragma table_info('user_preferences');
cid         name        type        notnull     dflt_value  pk
----------  ----------  ----------  ----------  ----------  ----------
0           id          integer     1                       1
1           user_id     integer     1           0           0
2           others      text        0           NULL        0
3           hide_mail   boolean     0           1           0
4           time_zone   varchar     0           NULL        0

user_preferencesテーブルの構造

UserPreferenceとは

UserPreferenceとは、Redmineの中で定義されたモデルのひとつです。そのUserPreferenceが初めて登場したのは2006年12月4日の リビジョン 62 でした。この時はマイページに表示したいブロック(カレンダーや文書等)と配置場所(レイアウト)を利用者(ユーザ)ごとに管理(保持)するために UserPreference が作成されたようです。これ以降利用者(User)ごとのさまざまな(任意の)設定情報を格納しています。


UserPrefernceは当初はマイページの選択されたブロックを保持するために使われていた

My Redmine

他のモデル(テーブル)と異なる点

ユーザに関する任意の設定情報は主に UserPreference(実テーブル:user_preferences)の others 列(カラム)で管理しています。この others は ActiveRecord::AttributeMethods::Serialization.serialize を使って宣言します。実際のデータはHashですがこれを YAML形式 に文字列化(シリアライズ)して格納します。Hashなので特定のデータ型に縛られない点が他のモデル(テーブル)とは異なります。

UserPreference#others 宣言部(app/models/user_preference.rb)

class UserPreference < ActiveRecord::Base
  include Redmine::SafeAttributes

  belongs_to :user
  serialize :others

UserPreference の格納データ

$ RAILS_ENV=development bundle exec rails console
Loading development environment (Rails 5.2.3)
[1] pry(main)> user_preference = User.find(2).pref
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."type" IN ('User', 'AnonymousUser') AND "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  UserPreference Load (0.1ms)  SELECT  "user_preferences".* FROM "user_preferences" WHERE "user_preferences"."user_id" = ? LIMIT ?  [["user_id", 2], ["LIMIT", 1]]
=> #"0",
   :comments_sorting=>"asc",
   :warn_on_leaving_unsaved=>"1",
   :textarea_font=>"",
   :my_page_layout=>{"left"=>["issuesassignedtome"], "right"=>["issuesreportedbyme"], "top"=>["issuesupdatedbyme"]},
   :my_page_settings=>{},
   :recently_used_project_ids=>"1"},
 hide_mail: true,
 time_zone: "">
[2] pry(main)>

others への値の設定・取得は、othersに直接ではなくアクセサメソッドを介して行っています。アクセサメソッドを使うことで設定可能な属性(キー)を制限することができます。

アクセサメソッドの一部(app/models/user_preference.rb)

  def [](attr_name)
    if has_attribute? attr_name
      super
    else
      others ? others[attr_name] : nil
    end
  end

  def []=(attr_name, value)
    if has_attribute? attr_name
      super
    else
      h = (read_attribute(:others) || {}).dup
      h.update(attr_name => value)
      write_attribute(:others, h)
      value
    end
  end

  def no_self_notified; (self[:no_self_notified] == true || self[:no_self_notified] == '1'); end

アクセサメソッドの使用例

[2] pry(main)> user_preference.no_self_notified
=> false
[3] pry(main)> user_preference.no_self_notified = '1'
=> "1"
[4] pry(main)> user_preference.no_self_notified
=> true
[5] pry(main)> user_preference
=> #<UserPreference:0x00007f87acac3b28
 id: 2,
 user_id: 2,
 others:
  {:no_self_notified=>"1",
   :comments_sorting=>"asc",
   :warn_on_leaving_unsaved=>"1",
   :textarea_font=>"",
   :my_page_layout=>{"left"=>["issuesassignedtome"], "right"=>["issuesreportedbyme"], "top"=>["issuesupdatedbyme"]},
   :my_page_settings=>{},
   :recently_used_project_ids=>"1"},
 hide_mail: true,
 time_zone: "">
[6] pry(main)>

他のモデルでも同じ機能を持てばRedmineの使い勝手は良くなるはず

UserPreference はとても便利なモデルです。同様にシリアライズ(文字列化)したデータを格納するモデルは他にもあります(Role, CustomField, Query等)が、さまざまなデータ型を格納し利用しているのは UserPreference だけではないかと思います。UserPreferenceのおかげでRedmineの使い勝手が向上している画面(機能)もあります。


使い勝手が向上している一例
プロジェクトセレクタにブックマークと最近使用したプロジェクトを表示

個人的には Redmine.org - Feature #4016: Make app settings overridable at project level で UserPreference と同等の機能を持つ(ProjectPreference のような)モデルが組み込まれればプロジェクトごとの設定項目を拡張することも容易になる (画面テーマなどもプロジェクト単位で設定が可能になる)はずです。これによりRedmineはさらに使い勝手が良くなると思います。

Redmineには特徴的なモデルや機能(処理)が他にもありそうです。今後もRedmine改善作業を行いながら新たな技術や実装方法を見つけ出したいと思います(宝探しをしているような気持ちもありますけど)。


Redmine demo

こちらの記事もオススメです!
ファーエンドテクノロジーによるRedmine開発状況(2019年1〜4月)
ファーエンドテクノロジーによる2019年1月〜4月のRedmineの開発状況です。注目している新機能やイチ押し機能をご紹介!
ShowNetの裏側を見た!
InteropのShowNetテクニカルツアー&セッションに参加。NOC内部など裏側にも入り、スタッフの情熱を肌で感じました。
「Redmine 4.1 新機能選抜総選挙」で紹介できなかった新機能 10選
redmine.tokyoでRedmine4.1の新機能16個を紹介。そのほか紹介できなかった便利な新機能10個をピックアップして紹介します。
初めて行った韓国で驚いたこと・印象に残ったこと
GWに旅行で韓国へ。料理は量が多くて安い、電車賃も安い韓国と日本との違いを楽しみました。
なんでこの機能Redmineにないの??の裏事情
Redmineで「こんな機能あったらいいのにな」と思う機能があります。どういった理由で作業が止まっているのでしょうか。
ファーエンドテクノロジーからのお知らせ(2019/09/11更新)
(9/20開催)台湾のOSSイベント&深圳留学報告会 飲みながら台湾や深圳について楽しく語る会 in松江
2019年9月20日(金)、台湾にて開催されたオープンソースのイベント「COSCUP 2019」と弊社代表の前田の中国語学留学に関する報告会を松江市で開催します。
「FAREND NEWS」2019年第5号 発行
広報紙「FAREND NEWS」2019年第5号を発行しました。弊社サービスの運用・サポートに携わっているスタッフや弊社の取り組みをご紹介します。
オープンソースカンファレンス 2019 Shimane(9/28開催)でセミナー発表・ブース出展
オープンソースカンファレンス 2019 Shimaneでセミナー発表・ブース出展を行います。
RubyWorld Conference 2019 (11/7・8開催) にPlatinumスポンサーとして協賛
11月7日(木)〜8日(金)に島根県松江市で開催される「RubyWorld Conference 2019」にPlatinumスポンサーとして協賛します。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け