Cómo agregar campos personalizados a modelos

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 Me gusta

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 me gusta

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 me gusta

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 Me gusta

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 Me gusta

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

1 me gusta

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 Me gusta

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 este código esquelético, si quisiera añadir varios campos, ¿tendría que ser cada campo un plugin independiente?

Estoy muy emocionado de haber encontrado este tutorial y me pregunto, ¿cuánto, si es que necesito hacer algún ajuste a estas plantillas para que funcionen para campos de usuario personalizados?

No, solo necesitas agregar código adicional para el campo adicional. En la mayoría de los casos, simplemente duplicando el código existente, por ejemplo:

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

El primer lugar para buscar campos de usuario personalizados es /admin/customize/user_fields, que te proporciona una interfaz de usuario para agregarlos. Si deseas un control más granular, el proceso es muy similar al de temas y categorías, pero en realidad no necesitas los elementos del frontend con campos de usuario.

De hecho, nosotros (Pavilion) estamos pensando en crear un plugin de campos personalizados (análogo a ACF para WordPress) que inicialmente se parecería un poco a la interfaz de administración de campos personalizados en el plugin Custom Wizard.

De hecho, algunas personas ya utilizan el plugin Custom Wizard como un administrador de campos personalizados. Enumera todos los campos personalizados en tu instancia (de cualquier fuente) y te permite agregar un campo de cualquier tipo a cualquier modelo que los admita.

No agrega soporte de frontend, por ejemplo, como el que se muestra en el plugin educativo Topic Custom Field (y eso no funcionaría en el contexto del plugin Custom Wizard), por lo que estamos pensando en separar eso en un plugin independiente.

3 Me gusta

@angus, creo que me encantaría esto.

Especialmente si añade el soporte de frontend.

Me encantaría tener una forma fácil para que los administradores añadan campos personalizados a diferentes clases, permitan a los usuarios rellenarlos (por ejemplo, en temas, publicaciones, perfiles de usuario) y tengan una forma para que el frontend los muestre.

Lo principal que no consigo con los campos de usuario personalizados actuales es una variedad de tipos de campos. Ahora mismo está limitado a 4, creo, y me encantaría tener las opciones que están disponibles con el plugin Custom Wizards.

Idealmente, quiero construir un directorio de usuarios bastante avanzado, con capacidad de búsqueda, filtrado y ordenación, con muchos campos personalizados de muchos tipos. Experimentaré con Custom Wizards para ver si funcionará por ahora y espero que inviertan en el plugin Custom Fields.

¡Gracias!

@angus

En primer lugar, muchas gracias por este plugin.

¿Tienes posiblemente un ejemplo funcional de este plugin (campo personalizado a tema) para tener en cuenta varios campos personalizados? Pude agregar con éxito un campo personalizado e hice un par de modificaciones sin problemas.

He intentado duplicar el código, modificar y agregar un complemento adicional, etc.

¿Alguien tiene un repositorio de código o un ejemplo que esté dispuesto a compartir conmigo? Cualquier ayuda sería muy apreciada.

1 me gusta

Hola @Joe_Stanton,

Lo he hecho un par de veces y la forma en que lo he hecho ha sido almacenar los campos personalizados en un array con un objeto con un nombre y tipo para el campo personalizado.

Por ejemplo:

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

Luego recorro los campos para aplicar la lógica que se menciona en este tema. Puedes ver un ejemplo de su uso en un plugin en el que estoy trabajando aquí. Sin embargo, el código relevante está a continuación.

Ejemplo

En el lado del servidor:

  # Registro de campos personalizados
  fields.each do |field|
    # Registrar los campos
    register_topic_custom_field_type(field[:name], field[:type].to_sym)

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

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

    # Actualizar en la creación del tema
    on(:topic_created) do |topic, opts, user|
      topic.send("#{field[:name]}=".to_sym, opts[field[:name].to_sym])
      topic.save!
    end

    # Actualizar en la edición del tema
    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

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

    # Pre-cargar los campos
    add_preloaded_topic_list_custom_field(field[:name])

    # Serializar a la lista de temas
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

De manera similar en el lado del cliente para serializar los campos:


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

  // Serializar Campos Personalizados:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 Me gusta

Gracias @keegan!

Creo que configuré correctamente el archivo plugin.rb, el bucle parece correcto.

Sin embargo, estoy teniendo problemas con el archivo topic-custom-field-initializer.js. Aquí está mi código para ambos archivos. ¿Alguna sugerencia para el archivo initializer.js? Creo que estoy muy cerca, al crear un nuevo tema, obtengo 1/3 de los campos, el campo listingDetails, pero todavía me faltan isClassifiedListing y 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 me gusta

No lo he probado, pero creo que la razón por la que solo ves 1/3 de los campos es que está iterando y registrando una clase de conector no única y sobrescribiendo la anterior.

En general, para el lado del cliente, en lugar de iterar a través de los campos personalizados y declarar los métodos de la API. Sugiero que definas componentes para cada campo por separado, o al menos acciones separadas, ya que probablemente necesitarás tener una lógica diferente asociada con cada campo.

La única parte sobre la que iteraría y declararía es esta:

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

El resto de los componentes, probablemente sea mejor crear una lógica separada para cada caso.

3 Me gusta

@keegan ¡eso funcionó! muchas gracias por todas las ideas. No podría haberlo hecho sin tu ayuda.

4 Me gusta