> For the complete documentation index, see [llms.txt](https://wfsys.gitbook.io/wt-practice/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wfsys.gitbook.io/wt-practice/multiplayer_mode/lesson_user_settings.md).

# Урок 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>
