Aggiungi un'impostazione personalizzata per utente in 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 Mi Piace

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 Mi Piace

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 Mi Piace

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 Mi Piace

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 Mi Piace

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 Mi Piace

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 Mi Piace

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 Mi Piace

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 Mi Piace

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

3 Mi Piace

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 Mi Piace

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 Mi Piace

David,

Questo non sembra più funzionare?

Abbiamo utilizzato:

  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

per consentire il salvataggio di un oggetto JSON in un campo personalizzato utente, ma questo ora impedisce la ricostruzione dei siti!

Lo utilizziamo da tempo.

L’errore che otteniamo durante la build è:

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

Ad aggiungere confusione, questo sembra funzionare in sviluppo, ma fallisce solo durante una build di produzione.

Se rimosso, il sito verrà compilato ma il campo personalizzato utente non verrà salvato e fallirà silenziosamente.

Non vedo che sia cambiato negli anni?:

Una nuova versione di Rails lo sta bloccando ora?

2 Mi Piace

@Falco potrebbe essere correlato a Ruby 3.x?

2 Mi Piace

Per tua informazione, ho installato la versione 2.7.1 localmente per lo sviluppo (ops)… ci sto lavorando ora.

3 Mi Piace

Sì, è sicuramente correlato a Ruby 3.1.x. Funziona bene su 2.7.x.

2 Mi Piace

Ho il plugin installato e abilitato con le impostazioni predefinite nel mio ambiente locale con il Ruby corrente, come posso attivare l’errore?

1 Mi Piace

Bene, questo è il problema. rbenv non mi permette di installare Ruby oltre 3.0.2 e in sviluppo (? cosa mi sfugge?) non riesco a innescare l’errore. Ma non appena si tenta di creare un’istanza corrente tests-passed con il Locations Plugin sul branch production_fixes (ignora il nome, è rotto).

1 Mi Piace

È 3.1.3 comunque. Se sei aperto a suggerimenti, asdf funziona benissimo per me.

Fantastico, ci proverò.

2 Mi Piace

Mi dispiace, penso di aver effettivamente risolto l’errore di build su quel branch. Non pensavo avrebbe funzionato, ma questo, a quanto pare, almeno compila, sto solo testando la funzionalità ora:

Sembra che se si usa questo trucco:

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

suggerito da David sopra, funziona?

1 Mi Piace