# Урок 20. Пользовательские настройки

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

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](/wt-practice/multiplayer_mode/lesson_dynamic_access_rights.md#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](/wt-practice/main/lesson_list_form.md#podklyuchenie-shablonov-k-proektu).
{% endhint %}

Начнем с настройки прав доступа к новой функциональности.

### Права доступа

Перейдем в файл описания работы серверной части приложения (Template.xml) .

Добавим точку доступа **UserSettingsEditAccessPoint** для просмотра и редактирования пользовательских настроек.

Создадим разрешение **UserSettingsPermission**, в которое добавим эту точку доступа. Добавим это разрешение в таблицу **template.permission** и настроим права доступа для пользовательских групп:

```sql
INSERT INTO template.permission_block_item(permission_block_id, id_title, title)
SELECT
  PB.permission_block_id, 'user_settings', 'Пользовательские настройки'
FROM
  template.permission_block PB
WHERE
  PB.id_title = 'user_action'
ON CONFLICT (id_title) DO NOTHING;

INSERT INTO template.permission(name, permission_block_item_id)
SELECT
  'UserSettingsPermission', PBI.permission_block_item_id
FROM
  template.permission_block_item PBI
WHERE
  PBI.id_title = 'user_settings'
ON CONFLICT (name) DO NOTHING;

INSERT INTO template.group_permission(group_id, permission_id)
SELECT
  group_id,
  permission_id
FROM
  template.permission P,
  template."group" G
WHERE
  (G.name = 'AdministratorGroup' OR G.name ISNULL) AND
  P.name = 'UserSettingsPermission'
ON CONFLICT (group_id, permission_id) DO NOTHING;
```

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

## Часовой пояс

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

В приложении ["Временные зоны"](https://wfsys.gitbook.io/wt_knowledge_base/platforma-wt/time_zone) собрана полная информация о работе с датами со временем в платформе WT.

### Форма

<figure><img src="/files/7plPQqYVdspsfz6LvuXl" alt=""><figcaption></figcaption></figure>

Создайте форму для редактирования пользовательских настроек (TemplateUserSettings.xml):

<figure><img src="/files/C8TNXO20qJEnxgjkks0H" alt=""><figcaption></figcaption></figure>

На главной форме (TemplateStart.xml) в меню добавьте пункт **Опции -> Пользовательские настройки...**, по которому будет открываться новая форма. Не забудьте использовать UserSettingsEditAccessPoint, чтобы ограничить доступ к пункту меню.

![](/files/IofAw1QQxlH4FIVAIHp0)

#### Запросы

Добавим запрос на получение списка часовых поясов:

{% code title="Template.xml" %}

```xml
<SqlQuery Name="TimeZoneInfoSelectSqlQuery">
  <Text>
    SELECT
      time_zone_info_id AS "TimeZoneInfoId",
      string_value(id_title, {PublicUserId}) AS "Title",
      by_default AS "ByDefault"
    FROM
      public.time_zone_info;
  </Text>
</SqlQuery>
```

{% endcode %}

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

{% code title="Template.xml" %}

```xml
<SqlQuery Name="UserSettingsSelectSqlQuery">
  <Text>
    SELECT time_zone_info_id AS "TimeZoneInfoId"
    FROM 
      public."user"
    WHERE
      user_id = {PublicUserId};
  </Text>
</SqlQuery>

<SqlQuery Name="UserSettingsUpdateSqlQuery">
  <Text>
    UPDATE public."user"
    SET
      time_zone_info_id = {TimeZoneInfoId}
    WHERE
      user_id = {PublicUserId};
  </Text>
</SqlQuery>
```

{% endcode %}

Добавьте три новых запроса в **UserSettingsPermission**.

#### Формы

Если пользователь изменил свой часовой пояс в настройках, то необходимо перезапустить приложение, чтобы новые настройки применились.&#x20;

Добавим условие проверки изменения часового пояса:

{% code title="TemplateUserSettings.xml" %}

```xml
<Condition Name="TimeZoneInfoNotEqualPreviousValueCondition" Type="NotEqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="TimeZoneInfoComboBox" />
    </Item>
    <Item>
      <DataConnection SourceDataConnection="UserSettingsPrimaryGetDataConnection">
        <Fields>
          <Field Name="TimeZoneInfoId" />
        </Fields>
      </DataConnection>
    </Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

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

{% code title="TemplateUserSettings.xml" %}

```xml
<Command Name="UserTimeZoneInfoChangedMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Изменение часового пояса пользователя</Caption>
  <Text>ВНИМАНИЕ!\rВыполните перезапуск программы.</Text>
  <Icon Type="Information" />
  <Buttons Type="Ok" />
</Command>
```

{% endcode %}

Скорректируем команду SaveSequentialCommand:

{% code title="TemplateUserSettings.xml" %}

```xml
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands>
    <Command Name="UserSettingsSaveCommand" />
    <If>
      <When>
        <Condition Name="TimeZoneInfoNotEqualPreviousValueCondition" />
      </When>
      <Then>
        <Command Name="UserTimeZoneInfoChangedMessageBoxCommand" />
      </Then>
    </If>
    <Command Name="FormCloseCommand" />
  </Commands>
</Command>
```

{% endcode %}

Запустите приложение и проверьте смену часового пояса.

## Мультиязычность

Помимо выбора собственной часовой зоны пользователь может настраивать язык интерфейса своей клиентской части.

### Настройка языка

Переделайте форму пользовательских настроек, добавив выпадающий список "Язык":

<figure><img src="/files/fZvguoqmeBtq7gMOde6K" alt=""><figcaption></figcaption></figure>

Добавим запрос на получения списка поддерживаемых языков:

{% code title="Template.xml" %}

```xml
<SqlQuery Name="LanguageSelectSqlQuery">
  <Text>
    SELECT
      language_id AS "LanguageId",
      title AS "Title",
      by_default AS "ByDefault"
    FROM
      public.language;
  </Text>
</SqlQuery>
```

{% endcode %}

Не забудьте добавить этот запрос в UserSettingsPermission.

В запросы UserSettingsSelectSqlQuery и UserSettingsUpdateSqlQuery добавим поле LanguageId:

{% code title="Template.xml" %}

```xml
<SqlQuery Name="UserSettingsSelectSqlQuery">
  <Text>
    SELECT
      time_zone_info_id AS "TimeZoneInfoId",
      language_id AS "LanguageId"
    FROM
      public."user"
    WHERE
      user_id = {PublicUserId};
  </Text>
</SqlQuery>

<SqlQuery Name="UserSettingsUpdateSqlQuery">
  <Text>
    UPDATE public."user"
    SET
      language_id = {LanguageId},
      time_zone_info_id = {TimeZoneInfoId}
    WHERE
      user_id = {PublicUserId};
  </Text>
</SqlQuery>
```

{% endcode %}

Чтобы стартовая форма узнала об изменении языка пользователя, необходимо внести доработки.

Заменим параметр Updated на параметр LanguageUpdated - так будет более наглядно:

{% code title="TemplateUserSettings.xml" %}

```xml
<Parameter Name="LanguageUpdated">False</Parameter>
```

{% endcode %}

Скорректируйте команду типа ValueSetCommand, которая будет присваивать этому параметру значение True, если значение языка изменилось.

Добавим условие проверки изменения зыка:

{% code title="TemplateUserSettings.xml" %}

```xml
<Condition Name="LanguageNotEqualPreviousValueCondition" Type="NotEqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="LanguageComboBox" />
    </Item>
    <Item>
      <DataConnection SourceDataConnection="UserSettingsPrimaryGetDataConnection">
        <Fields>
          <Field Name="LanguageId" />
        </Fields>
      </DataConnection>
    </Item>
  </Items>
  <DataType Type="IntegerDataType" />
</Condition>
```

{% endcode %}

Скорректируем команду SaveSequentialCommand:

{% code title="TemplateUserSettings.xml" %}

```xml
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands>
    <Command Name="UserSettingsSaveCommand" />
    <If>
      <When>
        <Condition Name="TimeZoneInfoNotEqualPreviousValueCondition" />
      </When>
      <Then>
        <Command Name="UserTimeZoneInfoChangedMessageBoxCommand" />
      </Then>
    </If>
    <If>
      <When>
        <Condition Name="LanguageNotEqualPreviousValueCondition" />
      </When>
      <Then>
        <Command Name="LanguageUpdatedSetCommand" />
      </Then>
    </If>
    <Command Name="FormCloseCommand" />
  </Commands>
</Command>
```

{% endcode %}

### Изменение языка приложения

Скорректируем представление **template.user\_info**, расширив его новыми полями **language\_id** и **language\_code**:

```sql
CREATE OR REPLACE VIEW template.user_info AS 
  SELECT
    au.user_id,
    pu.user_name,
    pu.user_full_name,
    pu.person,
    NOT pu.enabled AS archive,
    pu.user_id AS public_user_id,
    pu.language_id,
    l.code AS language_code
  FROM
    template."user" au
    JOIN "user" pu ON au.public_user_id = pu.user_id
    JOIN public.language l ON l.language_id = pu.language_id;
```

В серверном xml-файле скорректируем запрос UserCurrentSelectSqlQuery, добавив поле LanguageCode:

{% code title="Template.xml" %}

```xml
<SqlQuery Name="UserCurrentSelectSqlQuery">
  <Text>
    SELECT
      user_id AS "UserId",
      user_name AS "UserName",
      user_full_name AS "UserFullName",
      language_code AS "LanguageCode"
    FROM
      template.user_info UI
    WHERE
      user_id = {UserId};
  </Text>
</SqlQuery>
```

{% endcode %}

Перейдем в файл стартовой формы и в UserCurrentPrimaryGetDataConnection добавим новое поле LanguageCode:

{% code title="TemplateStart.xml" %}

```xml
<DataConnection Name="UserCurrentPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <ManualLoad>True</ManualLoad>
  <SqlQuery Name="UserCurrentSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="UserId" />
      <Field Name="LanguageCode" />
    </Fields>
  </SqlQuery>
</DataConnection>
```

{% endcode %}

Создадим команду обновления для UserCurrentPrimaryGetDataConnection, удалив его из команды AllPrimaryGetDataConnectionRefreshCommand:   &#x20;

{% code title="TemplateStart.xml" %}

```xml
<Command Name="UserCurrentDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <DataConnections>
    <DataConnection Name="UserCurrentPrimaryGetDataConnection" />
  </DataConnections>
</Command>
```

{% endcode %}

Создадим команду типа [LocaleSetCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/locale_set_command), с помощью которой будем устанавливать локаль  и язык перевода для всего приложения:

{% code title="TemplateStart.xml" %}

```xml
<Command Name="LocaleSetCommand" Type="LocaleSetCommand" Assembly="Commands">
  <Locale>
    <DataConnection SourceDataConnection="UserCurrentPrimaryGetDataConnection">
      <Fields>
        <Field Name="LanguageCode" />
      </Fields>
    </DataConnection>
  </Locale>
</Command>
```

{% endcode %}

Соберем обе команды в одну последовательность:

{% code title="TemplateStart.xml" %}

```xml
<Command Name="LocaleSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands>
    <Command Name="UserCurrentDataConnectionRefreshCommand" />
    <Command Name="LocaleSetCommand" />
  </Commands>
</Command>
```

{% endcode %}

Добавим Execution на параметр LanguageUpdated команды UserSettingsFormShowCommand:

{% code title="TemplateStart.xml" %}

```xml
<Execution>
  <ConditionExpression>
    <Command Name="UserSettingsFormShowCommand" Parameter="LanguageUpdated" />
  </ConditionExpression>
  <Commands>
    <Command Name="LocaleSequentialCommand" />
  </Commands>
</Execution>
```

{% endcode %}

Добавим вызов команды **LocaleSequentialCommand** в Execution, которые срабатывают на команды **LoginFormShowCommand** и **ReloginFormShowCommand** - тем самым мы будем задавать настройки текущего пользователя при его аутентификации в программе.

### Доработка форм

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

#### Файлы строковых ресурсов

Скачайте архив с файлами строковых ресурсов и распакуйте его в папку с формами проекта (\Template\Projects\1. Template\Forms).

{% file src="/files/umha57RHJk8tlxOHyC3R" %}

В папке Language вы найдете файлы:

<figure><img src="/files/UshRqBxrXcxJzwP7hgpP" alt=""><figcaption></figcaption></figure>

По договоренности внутри компании: файлы ru.wxlf и en.wxlf используем для задания строковых ресурсов для форм, а файлы object-ru.wxlf и object-en.wxlf - для кастомных элементов (команд, объектов и т.д.), которые рассмотрим в одном из следующих уроков. На самом деле платформа считывает из папки Forms\Language все файлы одной локали и записывает их в один словарь. И файлов по одной локали может быть сколько угодно - отбор происходит по имени файла, оно должно оканчиваться именем локали (ru/en).

Структура файлов одинакова:

```xml
<Wxliff xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Context>
    <Text Id="">
      <Target></Target>
    </Text>
    <Text Id="">
      <Source></Source>
      <Target></Target>
    </Text>
  </Context>
  
  <Context File="">
    <Text Id="">
      <Target></Target>
    </Text>
    <Text Id="">
      <Source></Source>
      <Target></Target>
    </Text>
  </Context>
</Wxliff>
```

Тэг `<Wxliff>` содержит список вложенных тэгов `<Context>`. Каждый такой тэг привязывается к конкретному файлу, имя которого указывается в атрибуте `File`. Если атрибут отсутствует, то такой контекст считается общим для всех форм.

Каждый контекст содержит набор строковых ресурсов, представленных тэгом `<Text>`, у которого есть атрибуты:

* `Id` - уникальный (в рамках контекста) идентификатор ресурса, по которому форма будет его искать. Обязательный атрибут.

Тэг `<Text>` содержит тэги:

* `<Source>` - оригинальный текст, который выступает в качестве подсказки в файле перевода. Необязательный тэг;
* `<Target>` - целевая строка, которая будет использоваться на форме.

В файлах **ru.wxlf** и **en.wxlf** есть начальные данные, и указаны два тэга `<Context>`: общий контекст с ресурсами, которые используются на многих формах, и контекст для формы TemplateUserSettings.xml.

#### Использование строкового ресурса на форме

Давайте перейдем в файл формы пользовательских настроек (TemplateUserSettings.xml) и на его примере разберем использование строковых ресурсов.

В файле формы необходимо все строковые константы заменить тэгом `<Text>`, значением которого будет строка:

```xml
<Text Id=""></Text>
```

Например, на форме есть параметр Title:

{% code title="TemplateUserSettings.xml" %}

```xml
<Parameter Name="Title">Пользовательские настройки</Parameter>
```

{% endcode %}

Заменим его описание:

{% code title="TemplateUserSettings.xml" %}

```xml
<Parameter Name="Title">
  <Text Id="FormTitle">Пользовательские настройки</Text>
</Parameter>
```

{% endcode %}

В файлах ru.wxlf и en.wxlf в соответствующем контексте есть строковые ресурсы:

```xml
<!-- ru.wxlf -->
<Text Id="FormTitle">
  <Target>Пользовательские настройки</Target>
</Text>

<!-- en.wxlf -->
<Text Id="FormTitle">
  <Source>Пользовательские настройки</Source>
  <Target>User settings</Target>
</Text>
```

Замените текст объектов LanguageLabel и TimeZoneInfoLabel, используя идентификатор строковых ресурсов из контекста данной формы.

А для текста кнопки SaveButton используйте идентификатор строкового ресурса из общего контекста:

```xml
<Text Id="Save">Сохранить</Text>
```

Запустите приложение и проверьте смену языка.

<figure><img src="/files/wwksoHoEc0WLxiBPQW6A" alt=""><figcaption></figcaption></figure>

### Как это работает

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

Сначала форма ищет в общем контексте, если там нет нужного ресурса, то продолжает искать в собственном контексте.

Когда форма находит подходящий ресурс, то берет из него значение тэга \<Target> и использует это значение в своих элементах.

Если форма не находит подходящего ресурса в файле строковых ресурсов, то использует текст, который указали в качестве значения тэга `<Text>`.

### Другие замены

Продолжим работать с формой пользовательских настроек (TemplateUserSettings.xml).

На форме есть команды **SaveOnCloseMessageBoxCommand** и **CloseOnCloseMessageBoxCommand** - в них тоже необходимо подобным образом заменить значения тэгов `<Caption>` и `<Text>`:

{% code title="TemplateUserSettings.xml" %}

```xml
<Command Name="SaveOnCloseMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>
    <Text Id="SaveOnCloseMessageBox.Caption">Сохранение</Text>
  </Caption>
  <Text>
    <Text Id="SaveOnCloseMessageBox.Text">Форма содержит несохраненные изменения.\rСохранить их перед закрытием?</Text>
  </Text>
  <Icon Type="Question" />
  <Buttons Type="YesNoCancel" />
</Command>

<Command Name="CloseOnCloseMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>
    <Text Id="CloseOnCloseMessageBox.Caption">Закрытие</Text>
  </Caption>
  <Text>
    <Text Id="CloseOnCloseMessageBox.Text">При закрытии все несохраненные изменения будут утеряны.\rВы уверены, что хотите закрыть форму?</Text>
  </Text>
  <Icon Type="Question" />
  <Buttons Type="YesNo" />
</Command>
```

{% endcode %}

<figure><img src="/files/DnSJFHRPI5mdj8PO3hHu" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
Текст кнопок в диалоговом окне отображается на том языке, который задан в настройках операционной системы.
{% endhint %}

Аналогичным образом сделайте с командой UserTimeZoneInfoChangedMessageBoxCommand.&#x20;

#### Замена констант

На самом деле подобные подстановки ресурсов мы можем делать с любыми константами на форме.

В качестве примера мы можем привязать ширину кнопки SaveButton к локали.

Добавим в файлы строковых ресурсов значения:

```xml
<!-- ru.wxlf -->
<Text Id="SaveButton.Width">
  <Source>200</Source>
  <Target>200</Target>
</Text>

<!-- en.wxlf -->
<Text Id="SaveButton.Width">
  <Source>200</Source>
  <Target>100</Target>
</Text>
```

Запустите приложение и проверьте отображение кнопки при использовании разных языков:

![](/files/PisftTw4gwHScqvLaifl)

Эти изменения можно убрать - они носили демонстрационный характер.

#### ComboBox

Перейдем в файл карточки назначения платежа (TemplateOperationEdit.xml) и найдем там описание объекта CategoryComboBox.

В объекте используется вложенный тэг [`<NullValue>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/combobox#null_value) для настройки отображения NULL-значения выпадающего списка:

```xml
<NullValue Show="True" Title="[Не выбрано]" />
```

Использование атрибута `Title` в тэге `<NullValue>`не позволяет заменять его значение на строковый ресурс. Поэтому заменим атрибут `Title` на тэг [`<NullValueTitle>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/combobox#null_value_title). Это позволит динамически задавать отображение NULL-значения объекта, в том числе для поддержки мультиязычности.

В файлах строковых ресурсов в общем контексте уже есть нужное значение. Его и будем использовать:

```xml
<NullValue Show="True" />
<NullValueTitle>
  <Text Id="NotChosen">[Не выбрано]</Text>
</NullValueTitle>
```

На некоторых формах у нас есть фильтр архивных  актуальных записей (ArchiveFilterComboBox):

```xml
<MyObject Name="ArchiveFilterComboBox" Type="ComboBox" Assembly="BaseControls">
  <Top>...</Top>
  <Right>...</Right>
  <Width>100</Width>
  <TabIndex>1</TabIndex>
  <NullValue Show="True" Title="Все" />
  <AutoCompleteMode>SmartSuggest</AutoCompleteMode>
  <ValueList>
    <Structure Type="Table">
      <Row>
        <Item>False</Item>
        <Item>Актуальные</Item>
      </Row>
      <Row>
        <Item>True</Item>
        <Item>Архивные</Item>
      </Row>
    </Structure>
  </ValueList>
  <Value>False</Value>
</MyObject>
```

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

```xml
<MyObject Name="ArchiveFilterComboBox" Type="ComboBox" Assembly="BaseControls">
  <Top>...</Top>
  <Right>...</Right>
  <Width>100</Width>
  <TabIndex>1</TabIndex>
  <NullValue Show="True" />
  <NullValueTitle>
    <Text Id="ArchiveFilterComboBox.All">Все</Text>
  </NullValueTitle>
  <AutoCompleteMode>SmartSuggest</AutoCompleteMode>
  <ValueList>
    <Structure Type="Table">
      <Row>
        <Item>False</Item>
        <Item>
          <Text Id="ArchiveFilterComboBox.Actual">Актуальные</Text>
        </Item>
      </Row>
      <Row>
        <Item>True</Item>
        <Item>
          <Text Id="ArchiveFilterComboBox.Archival">Архивные</Text>
        </Item>
      </Row>
    </Structure>
  </ValueList>
  <Value>False</Value>
</MyObject>
```

### Поддержка языков в SQL-запросах

В некоторых запросах мы уже сталкивались с функцией **public.string\_value(character varying, integer)**, которая относительно настроек языка пользователя возвращала по ключу из таблицы **public.strings** соответствующую строку.

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

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

<figure><img src="/files/xwC14hR7GYAj7xlbVnxY" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
В своей практике мы используем функцию **public.check\_strings()**, которая проверяет наличие переводов для всех языков и вернет список ключей, у которых отсутствует перевод для какого-либо языка.

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

```plsql
CREATE OR REPLACE FUNCTION public.check_strings()
  RETURNS character varying AS
$BODY$
DECLARE
  _row record;
  _result varchar[];
BEGIN
  FOR _row IN
    (WITH min_value AS
      (SELECT MIN(language_id) AS val FROM language)
      SELECT
        val,
        language_id
      FROM
        min_value
        CROSS JOIN
        language
       WHERE
        language_id != val
       ORDER BY 2)
  LOOP
    EXECUTE 'SELECT
      array_agg(strings_id)
    FROM
      (SELECT strings_id FROM strings WHERE language_id = ' || _row.val || ') S1
      FULL JOIN
      (SELECT strings_id FROM strings WHERE language_id = ' || _row.language_id || ') S2
      USING(strings_id)
    WHERE
      S1.strings_id IS NULL OR S2.strings_id IS NULL'
    INTO
      _result;

    IF (_result IS NOT NULL) THEN
      RETURN _result::varchar;
    END IF;
  END LOOP;
  
  RETURN NULL;
END;  
$BODY$
  LANGUAGE plpgsql;
```

{% endhint %}

## Самостоятельно <a href="#self-work" id="self-work"></a>

Переведите все формы на поддержку языков.

Не забудьте перевести строковые константы в запросах для списка назначений платежей и отчета по бюджету. Добавьте строковые ресурсы в таблицу *public.strings* и используйте функцию *public.string\_value(character varying, integer)* для получения нужного значения, как это сделано в запросе TimeZoneInfoSelectSqlQuery и LoadModeSelectSqlQuery.

Также добавьте перевод для описания динамических прав доступа - запрос PermissionBlockItemSelectSqlQuery. Используйте значения в колонках id\_title таблиц template.permission\_block и template.permission\_block\_item в качестве ключа для строковых ресурсов.

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

<figure><img src="/files/IrilP6C65eBAReHIEHXs" alt=""><figcaption></figcaption></figure>

## Пользовательские настройки форм <a href="#user-form-settings" id="user-form-settings"></a>

Дополнительно к пользовательским настройкам можно отнести настройки видимости, порядка и ширины колонок таблицы [DatabaseTable](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable), сортировку строк и фильтры в таблице. Также можно запоминать состояние формы и значения переменных, а при следующем открытии формы восстанавливать их.

Все это можно сохранять для каждого пользователя отдельно.

### Настройки таблиц

Для сохранения настроек в таблице DatabaseTable реализован вложенный тэг [`<SaveOnFormClose>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#save_on_form_close):

```xml
<SaveOnFormClose Columns="True" Filter="True" Sort="True" />
```

Информация о настройках пишется в таблицу **public.user\_form\_info**. Для каждой формы и каждого пользователя отдельная запись.

Давайте перейдем в файл списка клиентов (TemplateClientList.xml) и в таблицу **ClientDatabaseTable** добавим тэг `<SaveOnFormClose>`.

Когда откроем и закроем окно списка клиентов, в базе данных в таблице появится запись:

<figure><img src="/files/YtnGDYGGBmQGy9Ok9mcs" alt=""><figcaption></figcaption></figure>

Значение в колонке **name** совпадает со значение в атрибуте `Name` тэга `<Form>`.

Так как мы просто открыли и закрыли форму, то в колонке **tables\_info** хранится информация только о колонках:

```
{
  "(
    ClientDatabaseTable,
    \"{
      \"\"(ClientId,0,0,f)\"\",
      \"\"(RowNumber,30,1,t)\"\",
      \"\"(Name,0,2,t)\"\",
      \"\"(CityTitle,0,3,t)\"\",
      \"\"(Phone,150,4,t)\"\",
      \"\"(Archive,0,5,f)\"\"
    }\",
    {},
    {}
  )"
}
```

Чтобы можно было менять размер колонок, необходимо в таблице атрибуту `Value` тэга [`<AllowResizeColumns>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#allow_resize_columns) поставить значение True, а для колонки атрибуту `Value` тэга [`<AutoSizeMode>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable/column#column_auto_size_mode) поставить значение None.

Давайте это сделаем для колонки CityTitle (Город). Откроем форму списка клиентов, изменим ширину колонки "Город" и закроем окно. При повторном открытии формы мы увидим, что размер колонки сохранился, а в базе будет запись вида:

```
{
  "(
    ClientDatabaseTable,
    \"{
      \"\"(ClientId,0,0,f)\"\",
      \"\"(RowNumber,30,1,t)\"\",
      \"\"(Name,0,2,t)\"\",
      \"\"(CityTitle,200,3,t)\"\",
      \"\"(Phone,150,4,t)\"\",
      \"\"(Archive,0,5,f)\"\"
    }\",
    {},
    {}
  )"
}
```

Добавим настраиваемый фильтр в таблицу через контекстное меню, кликнув ПКМ по заголовку таблицы:

<figure><img src="/files/7eW9qFODhAaxAQWcxVoI" alt=""><figcaption></figcaption></figure>

В таблицу сохранится запись вида:

```
{
  "(
    ClientDatabaseTable,
    \"{
      \"\"(ClientId,0,0,f)\"\",
      \"\"(RowNumber,30,1,t)\"\",
      \"\"(Name,0,2,t)\"\",
      \"\"(CityTitle,200,3,t)\"\",
      \"\"(Phone,150,4,t)\"\",
      \"\"(Archive,0,5,f)\"\"
    }\",
    \"{
      \"\"ClassName\"\": \"\"WorkflowForms.Controls.DatabaseTableFilterGroupOperation\"\",
      \"\"Condition\"\": \"\"AND\"\",
      \"\"OperationList\"\": [{
        \"\"Value\"\": \"\"че\"\",
        \"\"ClassName\"\": \"\"WorkflowForms.Controls.DatabaseTableFilterStartWithOperation\"\",
        \"\"ColumnName\"\": \"\"CityTitle\"\"
      }]
    }\",
    {}
  )"
}
```

Давайте сбросим фильтрацию и добавим настраиваемую сортировку:

<figure><img src="/files/vvq7GcEtBtu6LpxlNtZf" alt=""><figcaption></figcaption></figure>

Теперь в таблицу сохранится запись вида:

```
{
  "(
    ClientDatabaseTable,
    \"{
      \"\"(ClientId,0,0,f)\"\",
      \"\"(RowNumber,30,1,t)\"\",
      \"\"(Name,0,2,t)\"\",
      \"\"(CityTitle,200,3,t)\"\",
      \"\"(Phone,150,4,t)\"\",
      \"\"(Archive,0,5,f)\"\"
    }\",
    {},
    \"[
      {
        \"\"Order\"\": 0,
        \"\"SortOrder\"\": \"\"ASC\"\",
        \"\"ColumnName\"\": \"\"CityTitle\"\"
      },
      {
        \"\"Order\"\": 1,
        \"\"SortOrder\"\": \"\"DESC\"\",
        \"\"ColumnName\"\": \"\"Name\"\"
      }
    ]\"
  )"
}
```

### Состояние формы

Чтобы сохранять состояние формы, у тэга [`<Form>`](https://wfsys.gitbook.io/workflow-forms-syntax/) есть атрибут [`RestoreLastFormState`](https://wfsys.gitbook.io/workflow-forms-syntax/#set_restore_last_form_state). Если значение атрибута равно True, то состояние формы будет запоминаться в колонку **form\_state** таблицы **public.user\_form\_info**.

Поддерживается три состояния:

* 0 - окно с размерами по умолчанию (Normal);
* 1 - свернутое окно (Minimized);
* 2 - развернутое окно (Maximized).

### Значение Variable

Для сохранения значения переменной Variable реализован вложенный тэг [`<SaveOnFormClose>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/variable#save_on_form_close):

```xml
<SaveOnFormClose Value="True" />
```

Значения переменных сохраняются в колонку **variables\_info** таблицы **public.user\_form\_info**.

На форме списка клиентов создайте тестовую переменную, которой укажите тэг `<SaveOnFormClose>` со значением True и каким-нибудь значением. Откройте и закройте форму списка, а затем проверьте содержимое таблицы public.user\_form\_info.

<figure><img src="/files/EkrCoSYlrO7tB5pmrUW5" alt=""><figcaption></figcaption></figure>

## Итоги <a href="#results" id="results"></a>

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

## Ответы <a href="#answer" id="answer"></a>

В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson20-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson20-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson20-answer.zip</a></td></tr></tbody></table>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wfsys.gitbook.io/wt-practice/multiplayer_mode/lesson_user_settings.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
