在插件中添加一个自定义的每用户设置

I just went through this process and experienced a bunch of trial and error, so I thought I’d document my findings to help the next developer to come along.

The things I needed:

  • Register your custom field type (mine was boolean, default is string)

    # plugin.rb
    User.register_custom_field_type 'my_preference', :boolean
    
  • Register that the custom field should be editable by users. Syntax matches that of params.permit(...)

    # plugin.rb
    register_editable_user_custom_field :my_preference # Scalar type (string, integer, etc.)
    register_editable_user_custom_field [:my_preference , my_preference : []] # For array type
    register_editable_user_custom_field [:my_preference,  my_preference : {}] # for json type
    
  • Add them to the fields serialized with the CurrentUserSerializer

    # plugin.rb
    DiscoursePluginRegistry.serialized_current_user_fields << 'my_preference'
    
  • Create a component to display your user preference

    // assets/javascripts/discourse/templates/components/my-preference.hbs
    <label class="control-label">My custom preferences!</label>
    {{preference-checkbox labelKey="my_plugin.preferences.key" checked=user.custom_fields.my_preference}}
    
  • Connect that component to one of the preferences plugin outlets (mine was under ‘interface’ in the user preferences)

    # assets/javascripts/discourse/connectors/user-preferences-interface/my-preference.hbs
    {{my-preference user=model}}
    
  • Ensure ‘custom fields’ are saved on that preferences tab

    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()
               }
             }
           })
    
         })
      }
    }
    

This document is version controlled - suggest changes on github.

33 个赞

Nice! I attempted the same in https://github.com/mozilla/discourse-post-read-email 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 https://github.com/discourse/discourse/commit/4382fb5facb035f5b414c6c7257dc828327a57c7, 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 个赞

This is a very helpful post laying out a straight forward example of setting up a user custom field. Is there anything similar for helping guide through a simple topic custom field? I am trying to work through it here:

But I haven’t gotten it to work quite yet. I would really appreciate any help on it.

1 个赞

David,

这个似乎不再起作用了?

我们一直在使用:

  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 个赞

请注意,我本地安装了 2.7.1 用于开发(哎呀)……现在就修复。

3 个赞

是的,这绝对与 Ruby 3.1.x 相关。它在 2.7.x 上运行良好。

2 个赞

我的插件已安装并启用,在本地环境中使用当前的 Ruby 和默认设置,如何触发错误?

1 个赞

问题就在这里。rbenv 不允许我安装 3.0.2 之后的 Ruby 版本,而在开发环境中(我错过了什么?)我无法触发错误。但是,一旦你尝试在 production_fixes 分支上使用 Locations 插件构建一个当前的 tests-passed 实例(忽略名称,它已损坏)。

1 个赞

顺便说一下,是 3.1.3。如果你愿意听取建议,asdf 对我来说效果很好。

好的,我会试试。

2 个赞

抱歉,我想我实际上已经解决了那个分支的构建错误。我没想过它会起作用,但这似乎至少可以构建,现在只是在测试功能:

似乎如果你使用这个技巧:

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

正如 David 在上面建议的那样,它有效吗?

1 个赞