Ajouter un paramètre personnalisé par utilisateur dans un plugin

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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

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 « J'aime »

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

3 « J'aime »

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 « J'aime »

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 « J'aime »

David,

Cela ne semble plus fonctionner ?

Nous avons utilisé :

  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

pour permettre la sauvegarde d’un objet json dans un champ personnalisé d’utilisateur, mais cela empêche désormais la reconstruction des sites !

Nous l’utilisons depuis un certain temps.

L’erreur que nous obtenons pendant la construction est :

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!'

Pour ajouter à la confusion, cela semble fonctionner en développement, mais échoue uniquement lors d’une construction en production.

Si supprimé, le site se construit mais le champ personnalisé de l’utilisateur ne sera pas sauvegardé et échouera silencieusement.

Je ne vois pas que cela ait changé depuis des années ? :

Une nouvelle version de Rails bloque-t-elle cela maintenant ?

2 « J'aime »

@Falco, cela pourrait-il être lié à Ruby 3.x ?

2 « J'aime »

Pour information, j’ai installé la version 2.7.1 localement pour le développement (oups)… je corrige cela maintenant.

3 « J'aime »

Oui, c’est définitivement lié à Ruby 3.1.x. Cela fonctionne bien sur 2.7.x.

2 « J'aime »

J’ai le plugin installé et activé avec les paramètres par défaut dans mon environnement local avec le Ruby actuel, comment puis-je déclencher l’erreur ?

1 « J'aime »

C’est là le problème. rbenv ne me permet pas d’installer Ruby au-delà de 3.0.2 et en développement (? qu’est-ce qui me manque ?) je ne parviens pas à déclencher l’erreur en développement. Mais dès que vous essayez de construire une instance actuelle tests-passed avec le plugin Locations sur la branche production_fixes (ignorez le nom, elle est défectueuse).

1 « J'aime »

C’est 3.1.3 d’ailleurs. Si vous êtes ouvert aux suggestions, asdf fonctionne très bien pour moi.

Cool, je vais essayer.

2 « J'aime »

Désolé, je pense que j’ai en fait résolu l’erreur de build sur cette branche. Je ne pensais pas que cela fonctionnerait, mais cela semble au moins compiler, je teste juste la fonctionnalité maintenant :

Il semble que si vous utilisez cette astuce :

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

suggérée par David ci-dessus, cela fonctionne ?

1 « J'aime »