Как добавить пользовательские поля к моделям

Это сборник образовательных плагинов, демонстрирующих, как добавить пользовательское поле к различным моделям в Discourse. Они предназначены в качестве учебных материалов для тех, кто хочет научиться создавать плагины для Discourse.

GitHub-Mark Как добавить пользовательское поле к теме
GitHub-Mark Как добавить пользовательское поле к категории

Для кого они предназначены

Эти плагины созданы для тех, кто хочет узнать больше о создании плагинов для Discourse. Прежде чем приступить к работе с этими плагинами, вам следует ознакомиться с руководством для начинающих по созданию плагинов Discourse.

Вы можете использовать эти плагины просто для добавления пользовательских полей в ваш экземпляр Discourse, однако для этого вам всё равно потребуется немного изменить код. Они не предназначены для использования «из коробки» на работающем сервере.

Как они работают

Помимо рабочего кода, каждый плагин содержит пошаговое описание того, что делает код, в виде комментариев. Например:

## 
# 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)

Надеемся, что шаги и комментарии понятны без дополнительных объяснений. Поле references указано, чтобы вы знали, куда обратиться за дополнительной информацией.

Дайте знать, если вам это окажется полезным, если что-то не работает или если заметки непонятны :slight_smile:

28 лайков

Спасибо за создание плагина для нас.
После установки плагина у меня возникла следующая ошибка:

Ой
Программное обеспечение, управляющее этим форумом, столкнулось с неожиданной проблемой. Приносим извинения за неудобства.
Подробная информация об ошибке была записана в журнал, и было сгенерировано автоматическое уведомление. Мы разберёмся с этим.
Дальнейших действий с вашей стороны не требуется. Однако, если ошибка сохраняется, вы можете предоставить дополнительные сведения, включая шаги для воспроизведения ошибки, опубликовав тему в категории обратной связи сайта.

Привет, ты запускаешь это в своей локальной среде разработки? Если да, не мог бы ты отправить мне в личные сообщения логи разработки? Если твоя среда разработки настроена правильно, этот плагин будет работать.

1 лайк

Я читал, но не могу разобраться. Не могли бы вы потратить время и сделать видео-урок по установке этого плагина: от настройки локальной среды разработки до пошагового добавления пользовательского поля… Очень сложно разобраться в этом плагине.
Если возможно, можете ли вы улучшить функцию поиска для типа поля «число»?

Я готов сделать пожертвование за этот плагин, пожалуйста, помогите мне!

Этот ресурс, подготовленный @angus, оказался даже полезнее, чем я изначально думал.

Не только код для добавления пользовательского поля снабжён понятными пояснениями, но и значительная его часть может быть напрямую интегрирована в любой плагин, поскольку код в основном использует переменные вроде FIELD_NAME и FIELD_VALUE, которые можно определить в файле плагина/config/settings.yml (также необходимо убедиться, что структура файлов вашего плагина совпадает с той, что представлена в коде на GitHub, предоставленном @angus). Изучение кода также помогло мне лучше понять некоторые функции и методы Discourse, с которыми я сталкивался ранее, но так и не разобрался до сих пор.

На данный момент код отлично работает для создания и сохранения пользовательских полей тем. Однако у меня возникло два вопроса:

  1. Ошибка в списке тем: Похоже, возникает ошибка, если попытаться загрузить список категорий (то есть список тем в категории), где есть темы, созданные до добавления пользовательского поля. Открывается страница исключения с сообщением об ошибке:
    Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries. Как лучше всего решить эту проблему?

  2. Можно ли применить пользовательское поле только к темам определённых категорий? Например, если у меня есть Категория 1, Категория 2 и Категория 3, и я хочу, чтобы поле ввода отображалось и сохранялось только для тем, относящихся к Категория 3. Возможно ли это?

1 лайк

Мы (Pavilion) планируем сделать что-то подобное в будущем, но пока доступны только код и инструкции. Если вы столкнулись с конкретной проблемой, создайте пост в канале Development и подробно опишите проблему.

Я добавил подпункт в тему о плагине пользовательских полей для тем, демонстрирующий, как предзагружать поля, если вы используете их в списке тем.

Вам понадобится пользовательское поле категории, чтобы указать категории, в которых оно должно отображаться. Я применил к пользовательским полям категорий те же возможности в этом плагине, включая подробные пошаговые инструкции:

Комбинация этих двух образовательных плагинов не решит вашу задачу полностью, но попробуйте продолжить работу, опираясь на это.

2 лайка

Это просто замечательно, @angus. Большое спасибо.

Видео, безусловно, полезно — я полностью за то, чтобы делать вещи максимально простыми, но, на мой взгляд, оно не обязательно для получения ключевой ценности из этих материалов, подготовленных @angus. Эти ресурсы предоставляют код, необходимый для достижения конкретной цели, о которой идёт речь в материале (создание пользовательского поля темы или категории). Видео, скорее всего, просто показало бы, как @angus или кто-то другой объясняет процесс внедрения ресурса, но это достаточно прямолинейно, и мы, вероятно, можем просто изложить это здесь.

Для ясности: эти ресурсы — не плагины, которые можно просто добавить на свой сайт как готовое решение для кастомизации форума. Вместо этого они эффективно дают вам понимание, необходимое для написания собственного кода пользовательских полей в вашем плагине.


Вот как я использовал эти ресурсы:

Вам нужно добавить имя и тип поля, которое вы хотите создать, в config/settings. Код в этих ресурсах использует переменные, определённые там. Таким образом, после этого вам фактически не потребуется много настраивать код, чтобы он заработал в вашем собственном плагине — переменные в plugin.rb и других файлах ссылаются на config/settings, и всё должно работать.

После обновления config/settings вы можете просто следовать коду, добавляя его в свой плагин:

  • Начните с кода в plugin.rb и добавьте его в plugin.rb вашего собственного плагина, чтобы создать пользовательское поле.

  • Затем перейдите к инициализатору (в assets/javascripts/discourse/[custom-field-initializer]), чтобы получить код, который инициализирует пользовательское поле и позволяет сохранить его на сервер.

  • Затем создайте форму в слое представления, где пользователь (или ваше приложение, если оно добавляет поле автоматически) сможет ввести значение для пользовательского поля, здесь (assets/discourse/connectors/[plugin-outlet-name]/[ваш специальный шаблон].hbs).

  • @angus настроил это так, что вы добавляете формы для пользовательских полей в плагин-аутлет, который будет вставлен в шаблон Discourse. Настройки для этой формы находятся здесь (assets/javascripts/discourse/lib/[custom-field-name].js.es6), поэтому, вероятно, вам стоит также настроить их, чтобы форма работала корректно.

@angus, пожалуйста, поправьте меня, если я что-то сказал неправильно.

Как только я разобрался с настройкой пользовательского поля, пройдя шаги выше, я начал немного больше кастомизировать процесс (например, более креативно подойдя к работе формы), но это было чрезвычайно полезной отправной точкой, которая сэкономила мне часы работы.

После прохождения всех шагов у меня возникло несколько вопросов (как я спрашивал ранее), но получение ответов в Development кажется самым эффективным способом двигаться дальше.

3 лайка

Отличное описание! Да, именно так их и предполагается использовать :+1:

1 лайк

Редакция: Изначально я разместил свой вопрос о том, как получать элементы на основе пользовательского поля, здесь, но решил, что вопрос достаточно отличается, чтобы заслужить отдельный пост. Поэтому я опубликовал его отдельно здесь.

2 лайка

Я столкнулся с необычным поведением композера после примера с пользовательскими полями темы.

Когда я нажимаю кнопку «Создать тему» (например, на странице отображения категории — но в любом месте сайта), композер не открывается, и я получаю следующую ошибку:

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

Сначала эта ошибка появилась, когда я попытался добавить новую кнопку «Создать тему» на новую страницу, но с тех пор, даже после удаления этой кнопки и любого связанного с ней кода, ошибка сохраняется.

Как мне кажется, проблему вызывает следующий код из инициализатора topic-custom-field-initializer:

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

Когда я удаляю этот код, кнопки создания темы снова работают (композер открывается корректно). Как только я возвращаю код, ошибка композера появляется снова.

Ранее этот код был в моём плагине и не вызывал проблем. Но теперь он приводит к ошибке композера (даже если я удалил любой код, связанный с композером или кнопками создания темы, в моём плагине).

Конечно, этот код важен — он сериализует пользовательское поле. Но, похоже, он конфликтует с композером. Есть ли идеи, как это исправить?

Мне удалось выяснить причину — я пытался добавить два отдельных пользовательских поля в инициализатор пользовательских полей темы. По какой-то причине это вызывало конфликты. Вероятно, существует способ корректно добавить два пользовательских поля в этот файл, но мой код, который просто повторял один и тот же фрагмент для двух разных полей, создавал проблемы. Всё заработало снова, как только я удалил второе пользовательское поле из этого файла.

С этим каркасным кодом, если я захочу добавить несколько полей, должно ли каждое поле быть отдельным плагином?

Я очень рад, что нашёл этот урок, и хотел бы узнать, сколько доработок, если таковые вообще потребуются, нужно будет внести в эти шаблоны, чтобы они работали с пользовательскими полями?

Нет, вам просто нужно добавить дополнительный код для каждого нового поля. В большинстве случаев достаточно просто продублировать существующий код, например:

add_preloaded_topic_list_custom_field(FIELD_NAME_1)
add_preloaded_topic_list_custom_field(FIELD_NAME_2)

Первое место, где следует искать пользовательские поля профиля, — это /admin/customize/user_fields, где предоставляется интерфейс для их добавления. Если вам нужен более детальный контроль, процесс будет очень похож на работу с темами и категориями, но для пользовательских полей элементы интерфейса (frontend) фактически не требуются.

На самом деле мы (Pavilion) думаем о создании плагина для пользовательских полей (аналогичного ACF для WordPress), который изначально будет выглядеть примерно так же, как интерфейс администрирования пользовательских полей в плагине Custom Wizard.

Кстати, некоторые люди уже используют плагин Custom Wizard в качестве менеджера пользовательских полей. Он отображает все пользовательские поля вашего экземпляра (из любого источника) и позволяет добавлять поле любого типа к любой модели, которая их поддерживает.

Однако он не добавляет поддержку интерфейса (frontend), например, как это показано в образовательном плагине Topic Custom Field (и это не сработало бы в контексте плагина Custom Wizard), поэтому мы думаем о том, чтобы выделить эту функциональность в отдельный плагин.

3 лайка

@angus, мне это очень понравится.

Особенно если будет добавлена поддержка фронтенда.

Мне бы очень хотелось иметь простой способ для администраторов добавлять пользовательские поля к разным классам, позволять пользователям заполнять их (например, для тем, сообщений, профилей пользователей) и иметь возможность отображать их на фронтенде.

Главная проблема, с которой я сталкиваюсь с текущими пользовательскими полями пользователей, — это разнообразие типов полей. Сейчас их, кажется, всего четыре, а мне бы хотелось иметь те же возможности, что и в плагине Custom Wizards.

В идеале я хочу создать довольно продвинутый каталог пользователей с возможностью поиска, фильтрации и сортировки, содержащий множество пользовательских полей разных типов. Я попробую использовать Custom Wizards, чтобы посмотреть, подойдет ли он для начала, и надеюсь, что вы все же инвестировать в плагин Custom Fields.

Спасибо!

@angus

Прежде всего, большое спасибо за этот плагин.

Не могли бы вы предоставить рабочий пример этого плагина (пользовательское поле к теме), учитывающий несколько пользовательских полей? Мне удалось успешно добавить одно пользовательское поле и внести несколько изменений без проблем.

Я пробовал дублировать код, модифицировать его и добавлять дополнительный плагин и т.д.

У кого-нибудь есть репозиторий с кодом или пример, которым вы готовы поделиться со мной? Любая помощь будет крайне признательна.

1 лайк

Привет @Joe_Stanton,

Я делал это несколько раз, и мой подход заключался в хранении пользовательских полей в массиве объектов, где каждый объект содержит имя и тип поля.

Например:

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])

    # Сериализация в список тем
    add_to_serializer(:topic_list_item, field[:name].to_sym) do
      object.send(field[:name])
    end
  end

Аналогично на стороне клиента для сериализации полей:


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

  // Сериализация пользовательских полей:
  CUSTOM_FIELDS.forEach((field) => {
    api.serializeOnCreate(field.name);
    api.serializeToDraft(field.name);
    api.serializeToTopic(field.name, `topic.${field.name}`);
  });
2 лайка

Спасибо @keegan!

Думаю, я правильно настроил файл plugin.rb, цикл выглядит верно.

Однако у меня возникают трудности с файлом topic-custom-field-initializer.js. Вот мой код для обоих файлов. Есть ли какие-то подсказки по файлу initializer.js? Я думаю, что я уже почти у цели: при создании новой темы у меня появляется 1 из 3 полей — поле listingDetails, но поля isClassifiedListing и 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 лайк

Я не тестировал это, но, полагаю, причина того, что вы видите только 1/3 полей, заключается в том, что происходит циклическая регистрация не уникального класса коннектора, что приводит к переопределению предыдущего.

В целом, для клиентской стороны, вместо перебора пользовательских полей и объявления методов API, я предлагаю определить компоненты для каждого поля отдельно или, как минимум, отдельные действия, так как вам, вероятно, потребуется различная логика для каждого поля.

Единственную часть, которую стоит перебирать и объявлять в цикле, это:

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

Для остальных компонентов, вероятно, лучше создать отдельную логику для каждого случая.

3 лайка

@keegan, сработало! Большое спасибо за все советы. Без вашей помощи я бы не справился.

4 лайка