Come aggiungere campi personalizzati ai modelli

This is a collection of education plugins that demonstrate how to add a custom field to different models in Discourse. They are intended as learning tools for those looking to learn how to build discourse plugins.

GitHub-Mark How to add a custom field to a topic
GitHub-Mark How to add a custom field to a category

Who they’re for

These plugins are for people looking to learn more about creating Discourse plugins. Before you start working with these plugins, you should complete the beginners guide to creating discourse plugins.

You could use these plugins just to add custom fields on your Discourse instance, however you would still need to change the code slightly to do so. It’s not designed for plugin-and-play use on a live server.

How they work

As well as containing working code, each plugin contains a step-by-step description of what the code is doing in the form of comments. For example

## 
# type:        step
# number:      1
# title:       Register the field
# description: Where we tell discourse what kind of field we're adding.
#              You can register a string, integer, boolean or json field.
# references:  lib/plugins/instance.rb,
#              app/models/concerns/has_custom_fields.rb
##
register_topic_custom_field_type(FIELD_NAME, FIELD_TYPE.to_sym)

Hopefully the steps and comments are self explanatory. The references are there to show you where to look if you want to learn more.

Let me know if you find it useful, if something isn’t working, or the notes are unclear :slight_smile:

28 Mi Piace

Thank for build plugin cho our,
i have this error later install plugin:

Oops
The software powering this discussion forum encountered an unexpected problem. We apologize for the inconvenience.
Detailed information about the error was logged, and an automatic notification generated. We’ll take a look at it.
No further action is necessary. However, if the error condition persists, you can provide additional detail, including steps to reproduce the error, by posting a discussion topic in the site’s feedback category.

Hey, are you running this in your local development environment? If so, could you PM your development logs? If your dev environment is set up correctly, this plugin will work.

1 Mi Piace

I read, but i can’t catch it. Can you spending time make a video tutorial install this plugin from make local development environment to step by step add a custom field …
So difficult to access this plugin.
If possible, can you upgrade search feature for field type is number?

I can donate for this plugin, please support help me!

This resource that @angus has put together is even more helpful than I first realized.

Not only is the code for adding a custom field there with clear explanations, but a lot of it can be plugged directly into any plugin, because the code mostly uses variables like FIELD_NAME and FIELD_VALUE, which you can define in plugin/config/settings.yml (you also need to make sure that your plugin file structure is the same as in the github code @angus has provided). Going through the code has also given me a greater understanding of some discourse functions and methods I have seen before, but never really understood until now.

So far, the code works great to create and save topic custom fields. There are two questions that have been coming up for me:

  1. Topic List Error: It seems to throw an error in the case I try to load a category list (ie, a topic list for a category) where there are topics created prior to adding the custom field. It shows the exception page, and lists this error:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. What’s the recommended way to solve this one?

  2. Is there a way for me to apply the custom field only for topics in certain categories? So, let’s say I have Category 1, Category 2, and Category 3, and I only want the custom field input to show up and only want the field to be saved if the topic is part of Category 3. Is there a way to do that?

1 Mi Piace

We (Pavilion) will be doing something like that in the future, but for now it’s just the code and the steps. If you’re stuck on a specific issue, create a post in Dev and describe the issue in some detail.

I’ve added a sub-step to the topic custom fields plugin demonstrating how to preload fields if you’re using them in the topic list.

https://github.com/pavilionedu/discourse-topic-custom-fields/commit/bd34679589bcf27d63e922533856569495a77a76

You’ll need a category custom field to identify categories it should be displayed in. I’ve given category custom fields the same treatment in this plugin, complete with the same step-by-step instructions:

https://github.com/pavilionedu/discourse-category-custom-fields

Combining these two education plugins won’t take you all the way to your goal, but see if you can make it from there.

2 Mi Piace

That is fantastic, @angus. Thank you very much.

A video is always nice–I’m all for making things as straightforward as possible, but I don’t think it’s required to get the key value from these resources that @angus has put together. These resources give the code that you need to accomplish the specific goal the resource is about (having a working topic custom field or a category custom field). A video would probably just be @angus or someone else talking through how to implement the resource, but that is straightforward, and we can probably just lay it out here.

To be clear, these resources are not plugins that you just add to your site as a plug and play that customizes your forum. Rather, they efficiently give you the understanding you need to code your own custom fields in your plugin.


This is how I’ve used these resources:

You will need to add in the name and type of field that you want in config/settings. The code in these resources uses variables that are defined there. So you actually don’t need to do much customizing of the code to get it to work in your own plugin after that–the variables in plugin.rb and elsewhere refer to config/settings, and then should work.

After updating config/settings, you can just follow along with the code, adding it to your plugin:

  • Start with the code in plugin.rb, and add that to your own plugin’s plugin.rb in order to create the custom field

  • Then go to the initializer (at assets/javascripts/discourse/[custom-field-initiliazer]) to get the code that will initialize the custom field and allow you to save it to the server

  • Then create the form in the view layer that will be where the user (or your app, if the app adds the field automatically) can enter the value for the custom field, here (assets/discourse/connectors/[plugin-outlet-name]/[your special template].hbs.

  • @angus has set these up so you would add the forms for the custom fields in a plugin outlet that will be inserted in the discourse template. Settings for this form are here, (assets/javascripts/discourse/lib/[custom-field-name].js.es6, so you probably want to customize that too to make the form work.

@angus feel free to correct anything I’ve said here.

Once I got the feel for setting up the custom field by going through the steps above, I then started customizing things a bit further (for example, getting more creative with how the form works), but this was an extremely helpful starting point that saved me hours of work.

After going through it, I did have some questions (like I asked earlier), but getting responses in dev seems like the most helpful way to go about things from there.

3 Mi Piace

Great description! Yup, that’s how they’re intended to be used :+1:

1 Mi Piace

Edit: I originally posted my question regarding how to retrieve items based on a custom field here, but decided the question was sufficiently different to warrant its own post. So I posted separately here.

2 Mi Piace

I’m experiencing strange behavior with the composer following the topic custom fields example.

When I hit the “create topic” button (for example, on the category show page–but anywhere on the site), the composer fails to open, and I get this error:

Uncaught Error: Assertion Failed: The key provided to set must be a string or number, you passed undefined
    at assert (index.js:172)
    at set (index.js:2802)
    at Class.set (observable.js:176)
    at composer.js:769
    at Array.forEach (<anonymous>)
    at Class.open (composer.js:768)
    at composer.js:898
    at invokeCallback (rsvp.js:493)
    at rsvp.js:558
    at rsvp.js:19

I first started seeing this error when I tried to add a new ‘create topic’ button on a new page, but since then, even I remove that new button and even if I remove any related code, the error persists.

In some way, I think the following code–from the topic-custom-field-initializer–is causing the problem:

api.serializeOnCreate(fieldName);
api.serializeToDraft(fieldName);
api.serializeToTopic(fieldName, `topic.${fieldName}`);

When I remove this code, the create topic buttons work again (properly opening the composer). When I put the code back in, the composer error comes back.

I have had this code in my plugin previously without problem. But now it causes the composer error (even if I have removed any composer or create-topic button -related code in my plugin).

Of course, this code is important–it serializes the custom field. But it seems that it is conflicting with composer. Any ideas on how to fix?

I was able to figure out the cause–I was trying to add two separate custom fields to the topic custom fields initializer. For some reason that was causing some interference. There probably is a way to properly add two custom fields to that file, but my code, just repeating the same code for two separate custom fields, was causing problems. It worked again fine when I removed the second custom field from that file.

Con questo codice scheletro, se volessi aggiungere più campi, ogni campo dovrebbe essere un plugin autonomo?

Sono davvero entusiasta di aver trovato questo tutorial e mi chiedo quanto, se del caso, dovrei modificare questi modelli per farli funzionare per i campi utente personalizzati?

No, devi solo aggiungere codice aggiuntivo per il campo aggiuntivo. Nella maggior parte dei casi, basta duplicare il codice esistente, ad esempio

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

Il primo posto dove cercare i campi utente personalizzati è /admin/customize/user_fields che ti fornisce un’interfaccia utente per aggiungerli. Se vuoi un controllo più granulare, il processo è molto simile a quello degli argomenti e delle categorie, ma in realtà non hai bisogno degli elementi frontend con i campi utente.

In realtà, noi (Pavilion) stiamo pensando di creare un plugin per campi personalizzati (analogo ad ACF per WordPress) che inizialmente assomiglierebbe all’interfaccia di amministrazione dei campi personalizzati nel plugin Custom Wizard.

In realtà, alcune persone utilizzano già il plugin Custom Wizard come gestore di campi personalizzati. Elenca tutti i campi personalizzati sulla tua istanza (da qualsiasi fonte) e ti consente di aggiungere un campo di qualsiasi tipo a qualsiasi modello che li supporti.

Non aggiunge il supporto frontend, ad esempio come quello mostrato nel plugin educativo Topic Custom Field (e questo non funzionerebbe nel contesto del plugin Custom Wizard), motivo per cui stiamo pensando di separarlo in un plugin separato.

3 Mi Piace

@angus, penso che mi piacerebbe molto.

Soprattutto se aggiunge il supporto al frontend.

Mi piacerebbe avere un modo semplice per gli amministratori di aggiungere campi personalizzati a diverse classi, consentire agli utenti di compilarli (ad esempio, in argomenti, post, profili utente) e avere un modo per il frontend di mostrarli.

La cosa principale che non riesco a ottenere con gli attuali campi utente personalizzati è una varietà di tipi di campo. Al momento sono limitati a 4, credo, e mi piacerebbe avere le opzioni disponibili con il plugin Custom Wizards.

Idealmente, voglio costruire una directory utenti abbastanza avanzata, ricercabile/filtrabile/ordinabile, con molti campi personalizzati di molti tipi, sperimenterò con Custom Wizards per vedere se funzionerà per ora e spero che investirete nel plugin Custom Fields.

Grazie!

@angus

Prima di tutto, grazie mille per questo plugin.

Hai per caso un esempio funzionante di questo plugin (campo personalizzato all’argomento) per gestire più campi personalizzati? Sono riuscito ad aggiungere con successo un campo personalizzato e ho apportato un paio di modifiche senza problemi.

Ho provato a duplicare il codice, modificarlo e aggiungere un plugin aggiuntivo, ecc.

Qualcuno ha un repository di codice o un esempio che è disposto a condividere con me? Qualsiasi aiuto sarebbe molto apprezzato.

1 Mi Piace

Ciao @Joe_Stanton,

L’ho fatto un paio di volte e il modo in cui l’ho fatto è stato quello di memorizzare i campi personalizzati in un array con un oggetto con un nome e un tipo per il campo personalizzato.

Ad esempio:

fields = [
  { name: 'isClassifiedListing', type: 'boolean' },
  { name: 'listingStatus', type: 'string' },
  { name: "listingDetails", type: 'json' }
]

Poi scorro i campi per applicare la logica menzionata in questo argomento. Puoi vedere un esempio di come viene utilizzato in un plugin su cui sto lavorando qui. Tuttavia, il codice pertinente è riportato di seguito.

Esempio

Sul lato server:

  # Registrazione dei campi personalizzati
  fields.each do |field|
    # Registra i campi
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

    # Metodi Getter
    add_to_class(:topic, field[:name].to_sym) do
      if !custom_fields[field[:name]].nil?
        custom_fields[field[:name]]
      else
        nil
      end
    end

    # Metodi Setter
    add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
    end

    # Aggiornamento alla creazione del topic
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Aggiornamento alla modifica del topic
    PostRevisor.track_topic_field(field[:name].to_sym) do |tc, value|
      tc.record_change(field[:name], tc.topic.send(field[:name]), value)
      tc.topic.send("#{field[:name]}=".to_sym, value.present? ? value : nil)
    end

    # Serializza al Topic
    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

    # Precarica i Campi
    add_preloaded_topic_list_custom_field(field[:name])

    # Serializza alla lista dei topic
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

Analogamente sul lato client per serializzare i campi:


 const CUSTOM_FIELDS = [
  { name: "isClassifiedListing", type: "boolean" },
  { name: "listingStatus", type: "string" },
  { name: "listingDetails", type: "json" },
];

  // Serializza Campi Personalizzati:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 Mi Piace

Grazie @keegan!

Penso di aver configurato correttamente il file plugin.rb, il ciclo sembra corretto.

Tuttavia, sto avendo difficoltà con il file topic-custom-field-initializer.js. Ecco il mio codice per entrambi i file. Qualche suggerimento per il file initializer.js? Penso di essere molto vicino, quando creo un nuovo argomento, ottengo 1/3 dei campi, il campo listingDetails ma mi mancano ancora isClassifiedListing e listingStatus

enabled_site_setting :topic_custom_field_enabled
register_asset 'stylesheets/common.scss'

after_initialize do
  fields = [
  { name: 'isClassifiedListing', type: 'boolean' },
  { name: 'listingStatus', type: 'string' },
  { name: "listingDetails", type: 'json' }
]

 fields.each do |field|

  register_topic_custom_field_type(field[:name], field[:type].to_sym)

   add_to_class(:topic, field[:name].to_sym) do
      if !custom_fields[field[:name]].nil?
        custom_fields[field[:name]]
      else
        nil
      end
    end

   add_to_class(:topic, "#{field[:name]}=") do |value|
      custom_fields[field[:name]] = value
   end

   on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
   end

    PostRevisor.track_topic_field(field[:name].to_sym) do |tc, value|
      tc.record_change(field[:name], tc.topic.send(field[:name]), value)
      tc.topic.send("#{field[:name]}=".to_sym, value.present? ? value : nil)
    end

    add_to_serializer(:topic_view, field[:name].to_sym) do
      object.topic.send(field[:name])
    end

  add_preloaded_topic_list_custom_field(field[:name])

    # Serialize to the topic list
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end

end

end

Initializer.js

import { withPluginApi } from 'discourse/lib/plugin-api';
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from '@ember/object/computed';
import { isDefined, fieldInputTypes } from '../lib/topic-custom-field';

export default {
  name: "topic-custom-field-intializer",
  initialize(container) {


    const CUSTOM_FIELDS = [
      { name: "isClassifiedListing", type: "boolean" },
      { name: "listingStatus", type: "string" },
      { name: "listingDetails", type: "json" },
    ];

    CUSTOM_FIELDS.forEach(field => {

    withPluginApi('0.11.2', api => {

      api.registerConnectorClass('composer-fields', 'composer-topic-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;

          if (!isDefined(model[field.name]) && model.topic && model.topic[field.name]) {
            model.set(field.name, model.topic[field.name]);
          }

          let props = {
            fieldName: field.name,
            fieldValue: model.get(field.name)
          }
          component.setProperties(Object.assign(props, fieldInputTypes(field.type)));
        },

        actions: {
          onChangeField(fieldValue) {
            this.set(`model.${field.name}`, fieldValue);
          }
        }
      });

      api.registerConnectorClass('edit-topic', 'edit-topic-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;

          let props = {
            fieldName: field.name,
            fieldValue: model.get(field.name)
          }
          component.setProperties(Object.assign(props, fieldInputTypes(field.type)));
        },

        actions: {
          onChangeField(fieldValue) {
            this.set(`buffered.${field.name}`, fieldValue);
          }
        }
      });

      api.serializeOnCreate(field.name);
      api.serializeToDraft(field.name);
      api.serializeToTopic(field.name, `topic.${field.name}`);

      api.registerConnectorClass('topic-title', 'topic-title-custom-field-container', {
        setupComponent(attrs, component) {
          const model = attrs.model;
          const controller = container.lookup('controller:topic');

          component.setProperties({
            fieldName: field.name,
            fieldValue: model.get(field.name),
            showField: !controller.get('editingTopic') && isDefined(model.get(field.name))
          });

          controller.addObserver('editingTopic', () => {
            if (this._state === 'destroying') return;
            component.set('showField', !controller.get('editingTopic') && isDefined(model.get(field.name)));
          });

          model.addObserver(field.name, () => {
            if (this._state === 'destroying') return;
            component.set('fieldValue', model.get(field.name));
          });
        }
      });

      api.modifyClass('component:topic-list-item', {
        customFieldName: field.name,
        customFieldValue: alias(`topic.${field.name}`),

        @discourseComputed('customFieldValue')
        showCustomField: (value) => (isDefined(value))
      });

    });


    });
  }
}

1 Mi Piace

Non l’ho testato, ma credo che il motivo per cui vedi solo 1/3 dei campi sia che sta iterando e registrando una classe di connettore non univoca, sovrascrivendo quella precedente.

In generale, per il lato client, invece di iterare sui campi personalizzati e dichiarare i metodi API, suggerisco di definire componenti per ciascun campo separatamente, o almeno azioni separate, poiché probabilmente dovrai avere una logica diversa associata a ciascun campo.

L’unica parte su cui itererei e dichiarerei è questa:

  api.serializeOnCreate(field.name);
      api.serializeToDraft(field.name);
      api.serializeToTopic(field.name, `topic.${field.name}`);

Per il resto dei componenti, è probabilmente meglio creare una logica separata per ciascun caso.

3 Mi Piace

@keegan ha funzionato! grazie mille per tutti i suggerimenti. Non ce l’avrei fatta senza il tuo aiuto.

4 Mi Piace