Перейти к содержанию

Пример расширений для упоминания пользователей в обсуждении

С помощью расширений можно дополнять логику выполнения упоминания пользователей в обсуждении. В каждом сценарии расширения используется объект context (IForumUserMentionExtensionContext), в котором, в зависимости от сценария, заполняются различные свойства. Для реализации логики упоминания пользователей предназначен метод IForumUserMentionExtension.ProcessMention.

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

Задача для реализации

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

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

Таблица AbDiscussion - строковая, следующей структуры:

Имя колонки Тип колонки Описание
ID Reference(Typified) Not Null ссылка на Instances Идентификатор карточки
Name String(Max) Not Null Имя карточки

Таблица AbMentionStat - коллекционная, следующей структуры:

Имя колонки Тип колонки Описание
ID Reference(Typified) Not Null ссылка на Instances Идентификатор карточки
RowID Guid Not Null Идентификатор записи в таблице
UserID Reference(Typified) Not Null ссылка на PersonalRoles Идентификатор упомянутого пользователя
UserName String(128) Not Null Имя упомянутого пользователя
Count Int32 Not Null Количество упоминаний пользователя

В таблице AbMentionStat в разделе Индексы необходимо добавить индекс на колонки ID и UserID.

Далее необходимо создать тип карточки AbDiscussion в группе AbTest.

В раздел Секции нужно включить таблицы AbDiscussion и AbMentionStat со всеми полями. На рисунке ниже представлен раздел Секции:

В разделе Вкладки нужно добавить один блок, в который включить следующие контролы:

  1. Название - контрол строка, который ссылается на поле AbDiscussion.Name.
  2. Обсуждение - контрол обсуждения с типом по умолчанию (Default).
  3. Количество упоминаний - контрол таблица, которая ссылается на секцию AbMentionStat. Для этой таблицы необходимо установить флаг Только для чтения. В разделе Колонки и форма вкладка Форма не требует изменений, так как пользователи не должны редактировать таблицу вручную. На вкладке Колонки нужно добавить две колонки:
    • Пользователь - колонка, ссылающаяся на поле AbMentionStat.User.
    • Количество упоминаний - колонка, ссылающаяся на поле AbMentionStat.Count.

На рисунках ниже представлен результат создания типа карточки и контролов в нем:

Important

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

Реализация подсчета упоминаний пользователей

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

#nullable enable

using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using LinqToDB; using Tessa.Forums.Mentions; using Tessa.Platform.Data;

namespace Tessa.Extensions.Default.Server.AbTest { public class CountStatMentionExtension : ForumUserMentionExtension { /// <summary> /// Регулярное выражение для поиска упоминания в сообщении. /// </summary> private static readonly Regex mentionRegex = new("data-mention-id=\"(?<userID>[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12})\"", RegexOptions.Compiled);

/// <inheritdoc/> public override async Task BeforeRequest(IForumUserMentionExtensionContext context) { if (!context.ValidationResult.IsSuccessful()) { return; }

var matches = mentionRegex.Matches(context.MessageInfo.HtmlText!) .Select(x => Guid.Parse(x.Groups["userID"].Value)) .ToArray();

var mentionsMessageCount = new Dictionary<Guid, int>();

foreach (var userID in context.UserIDs) { mentionsMessageCount[userID] = matches.Count(x => x == userID); }

await UpsertStatAsync(context.CardID, mentionsMessageCount, context.DbScope, context.CancellationToken); }

private static async Task UpsertStatAsync( Guid cardID, Dictionary<Guid, int> mentionsCount, IDbScope dbScope, CancellationToken cancellationToken = default) { await using var _ = dbScope.Create(); var db = dbScope.Db;

var factory = await dbScope.GetBuilderFactoryAsync(cancellationToken); var query = factory.Cached(nameof(CountStatMentionExtension), "UpsertStat", b => factory.Dbms switch { Dbms.PostgreSql => GenerateUpsertQueryPg(factory), Dbms.SqlServer => GenerateUpsertQueryMs(factory), _ => throw new InvalidOperationException($"{nameof(CountStatMentionExtension)}. Unknown Dbms while generating upsert query.") });

var cardIDParam = db.Parameter("cardID", cardID); var rowIDParam = db.Parameter("rowID", DataType.Guid); var userIDParam = db.Parameter("userID", DataType.Guid); var countParam = db.Parameter("count", DataType.Int32);

foreach (var mention in mentionsCount) { rowIDParam.Value = Guid.NewGuid(); userIDParam.Value = mention.Key; countParam.Value = mention.Value;

db.SetCommand(query, cardIDParam, rowIDParam, userIDParam, countParam); await db.ExecuteNonQueryAsync(cancellationToken); } }

private static string GenerateUpsertQueryPg(IQueryBuilderFactory builder) => builder.InsertInto("AbMentionStat", "ID", "RowID", "UserID", "UserName", "Count") .Select() .P("cardID") .P("rowID") .C("PersonalRoles", "ID", "Name") .P("count") .From("PersonalRoles") .Where() .C("ID").Equals().P("userID") .AppendLine().Append("ON CONFLICT (").C("ID").C("UserID").Append(") DO") .AppendLine().Append("UPDATE SET").AppendLine() .C("Count").Assign().E(e => e.C("AbMentionStat", "Count").Add(b => b.P("count"))) .Where() .C("AbMentionStat", "UserID").Equals().P("userID") .And().C("AbMentionStat", "ID").Equals().P("cardID") .Build();

private static string GenerateUpsertQueryMs(IQueryBuilderFactory builder) => builder.Update("AbMentionStat") .C("Count").Assign().E(e => e.C("AbMentionStat", "Count").Add(b => b.P("count"))) .Where() .C("AbMentionStat", "UserID").Equals().P("userID") .And().C("AbMentionStat", "ID").Equals().P("cardID") .AppendLine().Append("IF @@ROWCOUNT = 0") .InsertInto("AbMentionStat", "ID", "RowID", "UserID", "UserName", "Count") .Select() .P("cardID") .P("rowID") .C("PersonalRoles", "ID", "Name") .P("count") .From("PersonalRoles") .Where() .C("ID").Equals().P("userID") .Build(); } }

Пример регистрации типа карточки и расширения в пакете

Вначале нужно создать константу для идентификатора реализованного типа карточки во вспомогательном классе:

public static class AbCardTypes { /// <summary> /// Card type identifier for "AbDiscussion": {369FE1A5-213C-49CD-B2CD-B37BE4D736BC}. /// </summary> public static readonly Guid AbDiscussionTypeID = new Guid(0x369fe1a5, 0x213c, 0x49cd, 0xb2, 0xcd, 0xb3, 0x7b, 0xe4, 0xd7, 0x36, 0xbc);

/// <summary> /// Card type name for "AbDiscussion". /// </summary> public const string AbDiscussionTypeName = "AbDiscussion"; }

Для регистрации типа карточки в пакете необходимо получить зависимость ICardBundleRegistry и вызвать метод Register, в который передать идентификатор типа карточки и имя пакета.

Note

Если имя пакета не указано, то метод удаляет регистрацию для указанного типа карточки.

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

Ниже приведен пример регистрации:

using Tessa.Cards; using Tessa.Extensions.Default.Shared.AbTest; using Tessa.Forums.Mentions; using Unity; using Unity.Lifetime;

namespace Tessa.Extensions.Default.Server.AbTest { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterType<CountStatMentionExtension>(new ContainerControlledLifetimeManager()); }

public override void RegisterExtensions(IExtensionContainer extensionContainer) { extensionContainer .RegisterExtension<IForumUserMentionExtension, CountStatMentionExtension>(x => x .WithOrder(ExtensionStage.AfterPlatform) .WhenBundles("AbTest") .WhenCardTypes(AbCardTypes.AbDiscussionTypeID)) ; }

public override void FinalizeRegistration() { this.UnityContainer .Resolve<ICardBundleRegistry>() .Register(AbCardTypes.AbDiscussionTypeID, "AbTest"); } } }

Этот код регистрирует созданный тип карточки в пакете с именем AbTest в методе FinalizeRegistration. В методе RegisterExtensions выполняется регистрация расширения для подсчета упоминаний пользователей в пакете AbTest с помощью метода WhenBundles.

Результат работы расширения

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

После нескольких сообщений в обсуждении карточка будет выглядеть следующим образом:

В таблице Количество упоминаний содержится общая статистическая информация по упоминаниям пользователей в сообщениях.

Back to top