プラグインでユーザーごとのカスタム設定を追加する

このプロセスを通過し、試行錯誤を繰り返した経験から、次にやってくる開発者のために私の知見をまとめておくことにしました。

必要な手順は以下の通りです:

  • カスタムフィールドタイプを登録する(私の場合は boolean、デフォルトは string)

    # plugin.rb
    User.register_custom_field_type 'my_preference', :boolean
    
  • カスタムフィールドがユーザーによって編集可能であることを登録する。構文は params.permit(...) に一致します。

    # plugin.rb
    register_editable_user_custom_field :my_preference # スカラー型(string、integer など)
    register_editable_user_custom_field [:my_preference, my_preference: []] # 配列型の 경우
    register_editable_user_custom_field [:my_preference, my_preference: {}] # JSON 型の 경우
    
  • これらを CurrentUserSerializer でシリアライズされるフィールドに追加する

    # plugin.rb
    DiscoursePluginRegistry.serialized_current_user_fields << 'my_preference'
    
  • ユーザー設定を表示するコンポーネントを作成する

    // assets/javascripts/discourse/templates/components/my-preference.hbs
    <label class="control-label">マイカスタム設定!</label>
    {{preference-checkbox labelKey="my_plugin.preferences.key" checked=user.custom_fields.my_preference}}
    
  • そのコンポーネントを、設定プラグインの出口(私の場合はユーザー設定内の ‘interface’)に接続する

    # assets/javascripts/discourse/connectors/user-preferences-interface/my-preference.hbs
    {{my-preference user=model}}
    
  • その設定タブで「カスタムフィールド」が保存されるようにする

    import { withPluginApi } from 'discourse/lib/plugin-api'
    
    export default {
      name: 'post-read-email',
      initialize () {
         withPluginApi('0.8.22', api => {
    
           api.modifyClass('controller:preferences/emails', {
             actions: {
               save () {
                 this.get('saveAttrNames').push('custom_fields')
                 this._super()
               }
             }
           })
    
         })
      }
    }
    

このドキュメントはバージョン管理されています。変更案は GitHub で提案してください。

「いいね!」 33

Nice! I attempted the same in GitHub - mozilla/discourse-post-read-email: INACTIVE - http://mzl.la/ghe-archive - A discourse plugin to give users the option of marking posts as read when emailed and arrived at almost the same result.

My only differences were I didn’t hunt down User.register_custom_field_type and so used my own ugly workaround. (I’ll switch to register_custom_field_type when I get the chance.)

And I think I came up with a slightly neater solution for saving the field, I patch the preferences controller to save custom fields alongside everything else, so the field is saved when the “Save” button is clicked, rather than when it’s toggled:

import { withPluginApi } from 'discourse/lib/plugin-api'

export default {
  name: 'post-read-email',
  initialize () {
     withPluginApi('0.8.22', api => {

       api.modifyClass('controller:preferences/emails', {
         actions: {
           save () {
             this.saveAttrNames.push('custom_fields')
             this._super()
           }
         }
       })

     })
  }
}

This should work for all preferences controllers, as they all seem to use saveAttrNames.

「いいね!」 10

As a follow-up here, it turns out that inline-edit-checkbox is available only in the adminjs package, meaning this is currently Bad Advice™. I’ve resorted to using the method suggested above alongside the preference-checkbox component

{{preference-checkbox labelKey="my_plugin.preferences.key" checked=model.custom_fields.my_field}}

which works for all users.

Also, @LeoMcA, I had to modify your preferences hack slightly to work with the interface page since saveAttrNames was a computed property there.

this.get('saveAttrNames').push('custom_fields')
「いいね!」 5

Per DEV: Allow plugins to whitelist specific user custom_fields for editi… · discourse/discourse@4382fb5 · GitHub, custom fields must now be added to a whitelist to allow editing by users. All that is needed is a single line in plugin.rb:

register_editable_user_custom_field :my_field

@gdpelican @LeoMcA I have updated the OP with this extra step, and also pulled in the comments from your second and third posts. Please feel free to update anything else you feel is necessary.

「いいね!」 8

Perfect, thanks for this - I had fixing discourse-post-read-email on my todolist after last week’s security commit, and this makes it a whole lot easier!

Question (which may belong in a seperate post):

Will this serialize the custom field as an array, even if it only has a single element in it? I’ve been having to use the following pattern in a seperate plugin, so that:

user.custom_fields["field1"] = [ :item ]
user.save_custom_fields

becomes:

Array(user.custom_fields["field1"])
> [ :item ]

rather than:

user.custom_fields["field1"]
> :item
「いいね!」 2

This change only deals with saving custom fields, so I don’t think it will affect how they are serialized.

That said, I do know that there are a lot of weird edge cases relating to custom fields which we are hoping to address in a few weeks time (after 2.1 is released). What you describe above looks like one of those weird cases that we need to improve.

「いいね!」 6

Note that if the user custom field is a JSON string, you need to include the keys, or an empty hash (if the keys are dynamic), for it to be permitted, e.g.

register_editable_user_custom_field geo_location: {}
「いいね!」 3

hm, in a slight pickle actually. If the custom field is JSON, in order to save it, you need to pass a hash, i.e.

register_editable_user_custom_field geo_location: {}

will permit

{"custom_fields"=>{"geo_location"=>{"lat"=>"-37.7989239", "lon"=>"144.8929753", "address"=>"Barkly Street, Footscray, City of Maribyrnong, Greater Melbourne, Victoria, 3011, Australia", "countrycode"=>"au", "city"=>"", "state"=>"Victoria", "country"=>"Australia", "postalcode"=>"3011", "boundingbox"=>["-37.7989854", "-37.7988961", "144.8928258", "144.8931743"], "type"=>"tertiary"}}>

However, if the param is empty (e.g. the user clears the field), the custom_field is interpreted as a string

{"custom_fields"=>{"geo_location"=>"{}"}}

and is not permitted.

There isn’t an easy way around this in the current structure, i.e. the way user_params are added to in the users_controller

permitted << { custom_fields: User.editable_user_custom_fields }

Unless I’m missing something, perhaps some additional provision needs to be made for user_custom_fields that are typecast as JSON?

「いいね!」 1

I had a similar problem with arrays, not sure if the same will work for JSON. To allow an empty array, you have to permit ‘scalar’ values as well as an array:

register_editable_user_custom_field :geo_location
register_editable_user_custom_field geo_location: []

Or if you’re feeling fancy it can be combined into one line:

register_editable_user_custom_field [ :geo_location, geo_location: [] ]

This is the same behaviour as params.permit(...), so I hesitate to call it a bug. Maybe we can call it a ‘quirk’ :wink:

Let me know if that approach works for JSON - if not we can work out another solution

「いいね!」 6

:facepalm: of course. Just add another. It’s been a long day. Thanks!

「いいね!」 3

Visiting this again, it feels like there are too many steps here for something that the core plugin system has a command for. On the backend, I have to write the following to make this go:

# plugin.rb
register_editable_user_custom_field :my_setting
User.register_custom_field_type 'my_setting', :boolean
DiscoursePluginRegistry.serialized_current_user_fields << 'my_setting'

but I feel like I should be able to do this:

# plugin.rb
register_editable_user_custom_field :my_setting, :boolean

You could even avoid people running into that nasty array snag by making the plugin system support the following cases:

register_editable_user_custom_field :my_setting, :array
register_editable_user_custom_field :my_setting, :object

@david

「いいね!」 10

これは、ユーザーのカスタムフィールドを設定するシンプルで分かりやすい例を示した非常に役立つ投稿です。トピックのカスタムフィールドについても、同様に手順を案内するものはありますか?私はここで取り組んでいます:

まだうまくいっていません。何かお手伝いいただければ幸いです。

「いいね!」 1

デビッド、

これはもう機能しないようですね?

私たちはこれを使用しています:

  register_editable_user_custom_field [:geo_location,  geo_location: {}] if defined? register_editable_user_custom_field
  register_editable_user_custom_field geo_location: {} if defined? register_editable_user_custom_field

これにより、ユーザーカスタムフィールドにJSONオブジェクトを保存できるようになりますが、サイトの再構築ができなくなっています!

これはしばらく使用しています。

ビルド中に発生するエラーは次のとおりです。

ArgumentError: wrong number of arguments (given 0, expected 1)
/var/www/discourse/lib/plugin/instance.rb:170:in `register_editable_user_custom_field'
/var/www/discourse/plugins/discourse-locations/plugin.rb:95:in `block in activate!'

さらに混乱させることに、これは開発環境では機能するようですが、本番ビルドでは失敗します。

削除すると、サイトはビルドされますが、ユーザーカスタムフィールドは保存されず、サイレントに失敗します。

これは長年変更されていないように思えますか?:

Railsの新しいバージョンがこれをブロックしているのでしょうか?

「いいね!」 2

@Falco これはRuby 3.xに関連している可能性がありますか?

「いいね!」 2

FYI、開発用にローカルに2.7.1をインストールしています(しまった)…今修正しています。

「いいね!」 3

はい、間違いなくRuby 3.1.x関連です。2.7.xでは正常に動作します。

「いいね!」 2

プラグインをインストールし、現在のRubyでローカル環境のデフォルト設定で有効にしましたが、エラーをトリガーするにはどうすればよいですか?

「いいね!」 1

それが問題なのです。rbenv3.0.2 より新しい Ruby のインストールを許可しておらず、開発環境(何が足りないのでしょうか?)ではエラーを発生させることができません。しかし、production_fixes ブランチ(名前は無視してください、壊れています)で Locations Plugin を使用して現在の tests-passed インスタンスをビルドしようとすると、問題が発生します。

「いいね!」 1

ちなみに3.1.3です。提案を受け入れていただけるなら、asdfは私にとってうまく機能します。

了解しました、試してみます。

「いいね!」 2

申し訳ありませんが、そのブランチのビルドエラーは解消されたようです。うまくいくとは思っていませんでしたが、少なくともビルドはできるようです。現在、機能テスト中です。

Davidが上記で提案した、このトリックを使用すると機能するようです。

 register_editable_user_custom_field [:geo_location,  geo_location: {}] if defined? register_editable_user_custom_field
「いいね!」 1